Коварная ошибка в коде или баг компилятора MinGW g++?
Я тут новенький на сайте, по-этому очень прошу не удалять мой пост сразу, как это делают в англоязычном сегменте. А где ещё искать решения моей проблемы? Я сам испробовал все доступные мне пути отладки.
В моей программе имеется класс, и в нём метод. Так вот, если этот метод объявлен как __attribute__((noinline)), т.е. принудительно компилируется как обычная не встраиваемая функция, то всё работает как надо.
А вот без этого атрибута компилятор делает его по умолчанию встраиваемым (inline) и делает это похоже некорректно, т.к. приложение в определённый момент виснет.
Также всё вдруг начинает хорошо работать, если в месте вызова этого метода вставить вызов какой-нибудь ничего не значащей функции, наподобие printf() или Beep().
Подскажите пожалуйста, в чём может быть проблема? Если это я где-то напортачил, то не могу понять где. Но пока это очень похоже на баг самого компилятора, который неверно оптимизирует inline-функцию (случай редчайший и маловероятный, но всё же). Использую пакет MinGW 8.2.0 2018-го года, ранее с ним проблем никогда не было. Исходный код вместе с бинарником и bat-файлом для компиляции прилагаю тут.
Учитывая рекомендации в ответах, привожу исходный код целиком непосредственно здесь:
/*
* mingw_bug.cpp
*
* This program demonstrates the serious MinGW g++ compiler optimization BUG!
* The ISSUE: compiler does incorrectly inlining of the given method (lines 257-275).
* See the lines 131..147 and 257-275 for detailes!
* Version of the compiler: 8.2.0 (year 2018)
* Compilation command line:
* C:\MinGW\bin\g++ -c -fno-rtti -O2 -march=native mingw_bug.cpp
* C:\MinGW\bin\g++.exe -static-libstdc++ -o mingw_bug.exe mingw_bug.o
* Found by Kondryukov D.V.
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <assert.h>
#ifdef __GNUC__
#define INLINE __attribute__ ((always_inline)) inline
#else
#define INLINE inline
#endif //__GNUC__
#ifdef __GNUC__
#define NOINLINE __attribute__ ((noinline))
#else
#define NOINLINE inline
#endif //__GNUC__
#define TIME_INTERVAL 1.0
#define INPUT_POOL_SIZE 8
#define INPUT_KEYDOWN 0x00001000
#define INPUT_KEYUP 0x00002000
class CInput
{
struct EVENT_RECORD
{
EVENT_RECORD* pNext;
UINT uEvent;
};
HANDLE m_hIn;
EVENT_RECORD m_uEventPool[INPUT_POOL_SIZE];
EVENT_RECORD* m_pFreeEventRec;
EVENT_RECORD* m_pFirstEventRec;
public:
CInput();
void Update();
bool CheckEvent(UINT uEvent);
void FlushEvents();
};
#define INITIAL_FPS 30.0
#define MAX_TIME_DELTA 0.05
class CTimer
{
LARGE_INTEGER m_liStartCounter;
LARGE_INTEGER m_liCurrentCounter;
double m_lfInvFreq;
double m_lfTime;
float m_fTimeDelta;
bool m_bPaused;
public:
CTimer();
void Update();
void TogglePause();
INLINE double GetTime() {return m_lfTime;}
INLINE float GetTimeDelta() {return m_fTimeDelta;}
INLINE bool IsPaused() {return m_bPaused;}
};
#define MAX_COUNTER 1
class CTest
{
UINT m_uCounter;
public:
CTest();
void ToggleCounter();
};
void ExitFatal(UINT uErrorCode, const char* pszMessage);
CInput g_In;
CTimer g_Timer;
CTest g_Test;
int main()
{
double fNextTime = g_Timer.GetTime()+TIME_INTERVAL;
printf(
"This application demonstrates the MinGW g++ compiler optimization bug!\n"
"ESC - exit; SPACE - toggle timer pause; TAB - the BUG demonstration...\n");
while (true)
{
//update timer and input state
g_Timer.Update();
g_In.Update();
//check if ESCAPE was pressed for exit
if (g_In.CheckEvent(VK_ESCAPE|INPUT_KEYDOWN))
{
printf("<exiting...>>\n");
break;
}
//check if SPACE was pressed to toggle pause
if (g_In.CheckEvent(VK_SPACE|INPUT_KEYDOWN))
{
g_Timer.TogglePause();
printf("\n<toggle timer pause>\n");
}
/* The code causing compiler bug!
* If method CInput::FlushEvents() is forced to be not inline evrything works good!
* Also, If line 142 is uncommented evrything works good well!
* HOWEVER! If method CInput::FlushEvents() is INLINE (by default)...
* ...and line 142 is commented, the APPLICATION will HUNG when you will press TAB!!!
*/
if (g_In.CheckEvent(VK_TAB|INPUT_KEYDOWN))
{
g_Test.ToggleCounter();
//uncomment following line for bug hiding...
//printf("\nPress TAB again, and again... Ups!.. Where is the bug? Nothing hungs... o:)))\n"); //line 142
}
//BUG is here! The application will hung!
//But uncomment line 142 and the bug will hide!
g_In.FlushEvents();
if (g_Timer.GetTime()>=fNextTime)
{
printf("*");
fNextTime+= TIME_INTERVAL;
}
}
return 0;
}
void ExitFatal(UINT uErrorCode, const char* pszMessage)
{
printf("FATAL ERROR #%4.0X (%s), terminating!\n\n\n",uErrorCode,pszMessage);
Beep(1500,300);
Sleep(6000);
exit(1);
}
CInput::CInput()
{
m_hIn = GetStdHandle(STD_INPUT_HANDLE);
if (m_hIn==INVALID_HANDLE_VALUE) ExitFatal(0x0000,"input initialization");
if (!FlushConsoleInputBuffer(m_hIn)) ExitFatal(0x0001,"input initialization");
EVENT_RECORD* pLastRec = m_uEventPool+INPUT_POOL_SIZE-1;
EVENT_RECORD* pRec = m_uEventPool;
m_pFreeEventRec = pRec;
m_pFirstEventRec = NULL;
while(pRec<pLastRec)
{
pRec->pNext = pRec+1;
++pRec;
}
pRec->pNext = NULL;
}
void CInput::Update()
{
INPUT_RECORD ir[INPUT_POOL_SIZE];
DWORD nEvents;
while (true)
{
if (!PeekConsoleInput(m_hIn,ir,INPUT_POOL_SIZE,&nEvents)) ExitFatal(0x0002,"input peeking");
if (!nEvents) return;
if (!ReadConsoleInput(m_hIn,ir,nEvents,&nEvents)) ExitFatal(0x0003,"input reading");
INPUT_RECORD* p_ir_end = ir+nEvents;
for (INPUT_RECORD* p_ir = ir; p_ir<p_ir_end; ++p_ir)
{
switch (p_ir->EventType)
{
case KEY_EVENT:
if (m_pFreeEventRec)
{
EVENT_RECORD* pRec = m_pFreeEventRec;
m_pFreeEventRec = pRec->pNext;
pRec->pNext = m_pFirstEventRec;
m_pFirstEventRec = pRec;
pRec->uEvent = (p_ir->Event.KeyEvent.wVirtualKeyCode)&0xFFF;
if (p_ir->Event.KeyEvent.bKeyDown) pRec->uEvent|= INPUT_KEYDOWN;
else pRec->uEvent|= INPUT_KEYUP;
continue;
}
goto Flush;
}
}
}
Flush:
Beep(896,200);
if (!FlushConsoleInputBuffer(m_hIn)) ExitFatal(0x0004, "flushing input");
//force free the last event to unlock subsequent input
m_pFreeEventRec = m_pFirstEventRec;
m_pFirstEventRec = m_pFirstEventRec->pNext;
m_pFreeEventRec->pNext = NULL;
}
bool CInput::CheckEvent(UINT uEvent)
{
EVENT_RECORD* pCur = m_pFirstEventRec;
EVENT_RECORD* pPrev = (EVENT_RECORD*)&m_pFirstEventRec;
while (pCur)
{
if (pCur->uEvent==uEvent)
{
pPrev->pNext = pCur->pNext;
pCur->pNext = m_pFreeEventRec;
m_pFreeEventRec = pCur;
return true;
}
pPrev = pCur;
pCur = pCur->pNext;
}
return false;
}
/*
* This method cause to be the application hung if it is INLINE or line 142 is commented!
* It works correctly if it forced to be NOT INLINE or line 142 is uncommented
* MinGW g++ does it's optimization uncorrectly!!!
*/
NOINLINE void CInput::FlushEvents()
{
EVENT_RECORD* pCur = m_pFirstEventRec;
while (pCur)
{
EVENT_RECORD* pNext = pCur->pNext;
pCur->pNext = m_pFreeEventRec;
m_pFreeEventRec = pCur;
pCur = pNext;
}
m_pFirstEventRec = NULL;
}
CTimer::CTimer()
{
LARGE_INTEGER liFreq;
if (!QueryPerformanceFrequency(&liFreq)) ExitFatal(0x5000,"timer initialization");
if (!QueryPerformanceCounter(&m_liStartCounter)) ExitFatal(0x5001,"timer initialization");
m_liCurrentCounter = m_liStartCounter;
m_lfInvFreq = 1/((double)(liFreq.QuadPart));
m_lfTime = 0.0;
m_fTimeDelta = 1/INITIAL_FPS;
}
void CTimer::Update()
{
if (!m_bPaused)
{
if (!QueryPerformanceCounter(&m_liCurrentCounter)) ExitFatal(0x5002,"timer update");
double lfTime = ((double)(m_liCurrentCounter.QuadPart-m_liStartCounter.QuadPart))*m_lfInvFreq;
m_fTimeDelta = lfTime-m_lfTime;
if (m_fTimeDelta>MAX_TIME_DELTA) m_fTimeDelta = MAX_TIME_DELTA;
m_lfTime = lfTime;
return;
}
m_fTimeDelta = 0.0;
}
void CTimer::TogglePause()
{
LARGE_INTEGER liCounter;
if (!m_bPaused)
{
m_bPaused = true;
return;
}
if (!QueryPerformanceCounter(&liCounter)) ExitFatal(0x5003,"timer toggle pause");
m_liStartCounter.QuadPart+= liCounter.QuadPart-m_liCurrentCounter.QuadPart;
m_liCurrentCounter = liCounter;
m_bPaused = false;
}
CTest::CTest()
{
m_uCounter = 0;
}
void CTest::ToggleCounter()
{
++m_uCounter;
if (m_uCounter>MAX_COUNTER) m_uCounter = 0;
}
Полный текст программы с бинарником - по ссылке.
Ответы (1 шт):
В общем, я докопался до истины и понял, где именно кроется ошибка. Приведу сначала разобранный мною код тела функции main() полностью
.text:00404070 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00404070 public _main
.text:00404070 _main proc near ; CODE XREF: sub_4011A0+91↑p
.text:00404070
.text:00404070 argc = dword ptr 8
.text:00404070 argv = dword ptr 0Ch
.text:00404070 envp = dword ptr 10h
.text:00404070
.text:00404070 ; __unwind {
.text:00404070 push ebp
.text:00404071 mov ebp, esp
.text:00404073 push edi
.text:00404074 push esi
.text:00404075 push ebx
.text:00404076 xor ebx, ebx
.text:00404078 and esp, 0FFFFFFF0h
.text:0040407B sub esp, 20h
.text:0040407E call ___main
.text:00404083 mov dword ptr [esp], offset aThisApplicatio ; "This application demonstrates the MinGW"...
.text:0040408A fld1
.text:0040408C fadd ds:dbl_408058
.text:00404092 fstp qword ptr [esp+18h]
.text:00404096 call _puts
.text:0040409B
.text:0040409B Do_MainApplicationLoop: ; CODE XREF: _main+146↓j
.text:0040409B ; _main+166↓j
.text:0040409B mov ecx, offset _g_Timer
.text:004040A0 call __ZN6CTimer6UpdateEv ; CTimer::Update(void)
.text:004040A5 mov ecx, offset _g_In
.text:004040AA call __ZN6CInput6UpdateEv ; CInput::Update(void)
.text:004040AF mov edx, ds:m_pFirstEventRec ; EDX (m_pFirstEventRec) = m_pFirstEventRec,
.text:004040AF ; т.е., регистру EDX компилятор ставит в соответствие поле m_pFirstEventRec!
.text:004040B5 mov esi, edx ; ESI (pCur) = m_pFirstEventRec
.text:004040B7 test edx, edx
.text:004040B9 jz Put_Star ; вывести очередной '*' по таймеру и продолжить цикл...
.text:004040BF mov edi, [edx+4] ; EDI = pCur->uEvent
.text:004040C2 cmp edi, 101Bh ; is first event VK_ESC?
.text:004040C8 jz RemoveFirstEvent_And_Exit
.text:004040CE mov ecx, edx ; ECX (pCur) = m_pFirstEventRec
.text:004040D0 jmp short FollowLoop_CheckEvent_ESC ; EAX (pCur) = pCur->pNext; ECX (pPrev)
.text:004040D0 ; ---------------------------------------------------------------------------
.text:004040D2 align 10h
.text:004040E0
.text:004040E0 Loop_CheckEvent_ESC: ; CODE XREF: _main+83↓j
.text:004040E0 cmp dword ptr [eax+4], 101Bh ; is current event VK_ESC?
.text:004040E7 jz RemoveCurrentEvent_And_Exit ; (EAX==pCur, ECX==pPrev)
.text:004040ED mov ecx, eax ; ECX (pCur) = EAX (pCur)
.text:004040EF
.text:004040EF FollowLoop_CheckEvent_ESC: ; CODE XREF: _main+60↑j
.text:004040EF mov eax, [ecx] ; EAX (pCur) = pCur->pNext; ECX (pPrev)
.text:004040F1 test eax, eax
.text:004040F3 jnz short Loop_CheckEvent_ESC ; is current event VK_ESC?
.text:004040F5 cmp edi, 1020h ; is first event VK_SPACE?
.text:004040FB jnz short FollowLoop_CheckEvent_SPACE ; EAX (pCur) = pCur->pNext; ESI (pPrev)
.text:004040FD jmp RemoveFirstEvent_And_TogglePause ; EAX (pCur) = EDX (m_pFirstEventRec)
.text:004040FD ; ---------------------------------------------------------------------------
.text:00404102 align 10h
.text:00404110
.text:00404110 Loop_CheckEvent_SPACE: ; CODE XREF: _main+B3↓j
.text:00404110 cmp dword ptr [eax+4], 1020h ; is next event VK_SPACE?
.text:00404117 jz RemoveCurrentEvent_And_TogglePause ; EDX (pCur->pNext) = pCur->pNext
.text:0040411D mov esi, eax
.text:0040411F
.text:0040411F FollowLoop_CheckEvent_SPACE: ; CODE XREF: _main+8B↑j
.text:0040411F mov eax, [esi] ; EAX (pCur) = pCur->pNext; ESI (pPrev)
.text:00404121 test eax, eax
.text:00404123 jnz short Loop_CheckEvent_SPACE ; is next event VK_SPACE?
.text:00404125
.text:00404125 CheckFirstEvent_TAB: ; CODE XREF: _main+1CF↓j
.text:00404125 cmp dword ptr [edx+4], 1009h ; is first event VK_TAB?
.text:0040412C jz short RemoveFirstEvent_And_ToggleCounter ; EAX (pCur) = m_pFirstEventRec
.text:0040412E mov ecx, edx
.text:00404130 jmp short FollowLoop_CheckEvent_TAB ; EAX (pCur) = pCur->pNext; ECX (pPrev)
.text:00404130 ; ---------------------------------------------------------------------------
.text:00404132 align 10h
.text:00404140
.text:00404140 Loop_CheckEvent_TAB: ; CODE XREF: _main+DF↓j
.text:00404140 cmp dword ptr [eax+4], 1009h
.text:00404147 jz short RemoveCurrentEvent_And_ToggleCounter ; ESI = pCur->pNext
.text:00404149 mov ecx, eax
.text:0040414B
.text:0040414B FollowLoop_CheckEvent_TAB: ; CODE XREF: _main+C0↑j
.text:0040414B mov eax, [ecx] ; EAX (pCur) = pCur->pNext; ECX (pPrev)
.text:0040414D test eax, eax
.text:0040414F jnz short Loop_CheckEvent_TAB
.text:00404151 mov ecx, ds:m_pFreeEventRec ; ECX = m_pFreeEventRec
.text:00404157 jmp short Start_FlushEvents ; Вход в метод FlushEvents(),
.text:00404157 ; в регистре EDX, как думает компилятор, содержится поле m_pFirstEventRec, однако это не так!
.text:00404157 ; EAX (pNext) = pCur->pNext
.text:00404159 ; ---------------------------------------------------------------------------
.text:00404159
.text:00404159 RemoveFirstEvent_And_ToggleCounter: ; CODE XREF: _main+BC↑j
.text:00404159 mov eax, edx ; EAX (pCur) = m_pFirstEventRec
.text:0040415B mov ecx, offset m_pFirstEventRec ; ecx (pPrev) = &m_pFirstEventRec
.text:00404160
.text:00404160 RemoveCurrentEvent_And_ToggleCounter: ; CODE XREF: _main+D7↑j
.text:00404160 mov esi, [eax] ; ESI = pCur->pNext
.text:00404162 mov edi, 0
.text:00404167 mov [ecx], esi ; pPrev->pNext = pCur->pNext
.text:00404167 ; здесь поле m_pFirstEventRec неявно меняется, однако в EDX - по-прежнему старое значение!
.text:00404169 mov ecx, ds:m_pFreeEventRec ; ecx (m_pFreeEventRec) = m_pFreeEventRec
.text:0040416F mov [eax], ecx ; pCur->pNext = m_pFreeEventRec
.text:00404171 mov ecx, eax ; ECX (m_pFreeEventRec) = pCur
.text:00404173 mov ds:m_pFreeEventRec, eax ; m_pFreeEventRec = pCur
.text:00404178 mov eax, ds:_g_Test
.text:0040417D inc eax
.text:0040417E cmp eax, 1
.text:00404181 cmova eax, edi
.text:00404184 mov ds:_g_Test, eax
.text:00404189 jmp short Start_FlushEvents ; компилятор ошибочно считает, что m_pFirstEventRec не изменилось,
.text:00404189 ; по-этому передаёт коду FlushEvents() уже невалидный EDX вместо m_pFirstEventRec!
.text:00404189 ; ---------------------------------------------------------------------------
.text:0040418B align 10h
.text:00404190
.text:00404190 Loop_FlushEvents: ; CODE XREF: _main+12A↓j
.text:00404190 mov edx, eax ; edx (pCur) = pNext
.text:00404192
.text:00404192 Start_FlushEvents: ; CODE XREF: _main+E7↑j
.text:00404192 ; _main+119↑j
.text:00404192 mov eax, [edx] ; Вход в метод FlushEvents(),
.text:00404192 ; в регистре EDX, как думает компилятор, содержится поле m_pFirstEventRec, однако это не так!
.text:00404192 ; EAX (pNext) = pCur->pNext
.text:00404194 mov [edx], ecx ; pCur->pNext = m_pFreeEventRec
.text:00404196 mov ecx, edx ; m_pFreeEventRec = pCur
.text:00404198 test eax, eax ; pNext==NULL?
.text:0040419A jnz short Loop_FlushEvents ; edx (pCur) = pNext
.text:0040419C mov ds:m_pFreeEventRec, edx
.text:004041A2
.text:004041A2 Put_Star: ; CODE XREF: _main+49↑j
.text:004041A2 ; _main+1D5↓j
.text:004041A2 fld qword ptr [esp+18h] ; вывести очередной '*' по таймеру и продолжить цикл...
.text:004041A6 fld ds:dbl_408058
.text:004041AC mov ds:m_pFirstEventRec, ebx
.text:004041B2 fcomip st, st(1)
.text:004041B4 ffreep st
.text:004041B6 jb Do_MainApplicationLoop
.text:004041BC mov dword ptr [esp], '*' ; int
.text:004041C3 call _putchar
.text:004041C8 fld qword ptr [esp+18h]
.text:004041CC fadd ds:flt_40619C
.text:004041D2 fstp qword ptr [esp+18h]
.text:004041D6 jmp Do_MainApplicationLoop
.text:004041DB ; ---------------------------------------------------------------------------
.text:004041DB
.text:004041DB RemoveFirstEvent_And_Exit: ; CODE XREF: _main+58↑j
.text:004041DB mov eax, edx
.text:004041DD mov ecx, offset m_pFirstEventRec
.text:004041E2
.text:004041E2 RemoveCurrentEvent_And_Exit: ; CODE XREF: _main+77↑j
.text:004041E2 mov edx, [eax] ; (EAX==pCur, ECX==pPrev)
.text:004041E4 mov [ecx], edx
.text:004041E6 mov edx, ds:m_pFreeEventRec
.text:004041EC mov [eax], edx
.text:004041EE mov dword ptr [esp], offset aExiting ; "<exiting...>>"
.text:004041F5 mov ds:m_pFreeEventRec, eax
.text:004041FA call _puts
.text:004041FF lea esp, [ebp-0Ch]
.text:00404202 xor eax, eax
.text:00404204 pop ebx
.text:00404205 pop esi
.text:00404206 pop edi
.text:00404207 pop ebp
.text:00404208 retn
.text:00404209 ; ---------------------------------------------------------------------------
.text:00404209
.text:00404209 RemoveFirstEvent_And_TogglePause: ; CODE XREF: _main+8D↑j
.text:00404209 mov eax, edx ; EAX (pCur) = EDX (m_pFirstEventRec)
.text:0040420B mov esi, offset m_pFirstEventRec ; ESI (pPrev) = &m_pFirstEventRec
.text:00404210
.text:00404210 RemoveCurrentEvent_And_TogglePause: ; CODE XREF: _main+A7↑j
.text:00404210 mov edx, [eax] ; EDX (pCur->pNext) = pCur->pNext
.text:00404212 mov ecx, offset _g_Timer
.text:00404217 mov [esi], edx ; pPrev->pNext = pCur->pNext
.text:00404219 mov edx, ds:m_pFreeEventRec
.text:0040421F mov [eax], edx ; pCur->pNext = m_pFreeEventRec
.text:00404221 mov ds:m_pFreeEventRec, eax ; m_pFreeEventRec = pCur
.text:00404226 call __ZN6CTimer11TogglePauseEv ; CTimer::TogglePause(void)
.text:0040422B mov dword ptr [esp], offset aToggleTimerPau ; "\n<toggle timer pause>"
.text:00404232 call _puts
.text:00404237 mov edx, ds:m_pFirstEventRec ; EDX (m_pFirstEventRec) = m_pFirstEventRec
.text:0040423D test edx, edx
.text:0040423F jnz CheckFirstEvent_TAB ; is first event VK_TAB?
.text:00404245 jmp Put_Star ; вывести очередной '*' по таймеру и продолжить цикл...
.text:00404245 ; } // starts at 404070
.text:00404245 _main endp
А теперь рассмотрю подробнее главные моменты. Первый:
.text:004040AF mov edx, ds:m_pFirstEventRec ; EDX (m_pFirstEventRec) = m_pFirstEventRec,
.text:004040AF ; т.е., регистру EDX компилятор ставит в соответствие поле m_pFirstEventRec!
Здесь компилятор заносит значение поля m_pFirstEventRec в регистр EDX и ставит его в соответствие с этим полем.
Следующий момент - ключевой:
.text:00404159 RemoveFirstEvent_And_ToggleCounter: ; CODE XREF: _main+BC↑j
.text:00404159 mov eax, edx ; EAX (pCur) = m_pFirstEventRec
.text:0040415B mov ecx, offset m_pFirstEventRec ; ecx (pPrev) = &m_pFirstEventRec
Здесь мы видим, что в регистр EAX записывается значение EDX, т.е. m_pFirstEventRec. Это переменная pCur. Следующая инструкция в регистр ECX заносит указатель на m_pFirstEventRecт. Тут у нас переменная pPrev. Этим двум инструкциям соответствуют следующие строки в исходнике:
EVENT_RECORD* pCur = m_pFirstEventRec;
EVENT_RECORD* pPrev = (EVENT_RECORD*)&m_pFirstEventRec; //the source of optimizer's BUG!!!
Рассмотрим последующие три инструкции:
.text:00404160 RemoveCurrentEvent_And_ToggleCounter: ; CODE XREF: _main+D7↑j
.text:00404160 mov esi, [eax] ; ESI = pCur->pNext
.text:00404162 mov edi, 0
.text:00404167 mov [ecx], esi ; pPrev->pNext = pCur->pNext
.text:00404167 ; здесь поле m_pFirstEventRec неявно меняется, однако в EDX - по-прежнему старое значение!
Конкретно нас интересуют первая и третья из них. Они соответствуют строке:
pPrev->pNext = pCur->pNext;
И в них значение поля m_pFirstEventRec НЕЯВНО меняется! Однако компилятор это не просёк, он по-прежнему считает, что регистр EDX идентичен содержимому m_pFirstEventRec!
И как результат:
jmp short Start_FlushEvents ; компилятор ошибочно считает, что m_pFirstEventRec не изменилось,
.text:00404189 ; по-этому передаёт коду FlushEvents() уже невалидный EDX вместо m_pFirstEventRec!
......................................................................................
.text:00404192 Start_FlushEvents: ; CODE XREF: _main+E7↑j
.text:00404192 ; _main+119↑j
.text:00404192 mov eax, [edx] ; Вход в метод FlushEvents(),
.text:00404192 ; в регистре EDX, как думает компилятор, содержится поле m_pFirstEventRec, однако это не так!
.text:00404192 ; EAX (pNext) = pCur->pNext
.text:00404194 mov [edx], ecx ; pCur->pNext = m_pFreeEventRec
.text:00404196 mov ecx, edx ; m_pFreeEventRec = pCur
.text:00404198 test eax, eax ; pNext==NULL?
.text:0040419A jnz short Loop_FlushEvents ; edx (pCur) = pNext
Т.е., переменной pCur метода FlushEvents() на старте вместо значения поля m_pFirstEventRec подсовывается уже невалидный регистр EDX (который, по сути - уже m_pFreeEventRec)! И всё, программа сломана!
Подведу краткий итог выше приведенному разбору. Компилятор в целях оптимизации кэширует значения переменных и полей классов в регистрах. Однако в данном примере он не понял, что фактическое значение поля в памяти изменилось и более не соответствует его аналогу в регистре. Как итог - сгенерировал поломанный код.
Как это можно исправить? Ну во-первых, скомпилировать с опцией -fno-strict-aliasing, как мне посоветовали в комментариях. Это работает. Однако страдает оптимизация, так что не вариант.
Второй способ: объявить поле m_pFirstEventRec в классе как volatile:
EVENT_RECORD* volatile m_pFirstEventRec;
Этот модификатор как раз отключает кеширование в регистрах для заданной переменной, чтобы гарантировать её корректную синхронизацию между потоками. Но и в нашем случае он работает. Тем не мнее это выглядит скорее костылём, нежели решением.
Так что, скрепя сердцем, я выбрал для себя третий вариант - переписать метод CheckEvents():
//non-buggy version of method
bool CInput::CheckEvent(UINT uEvent)
{
EVENT_RECORD* pCur = m_pFirstEventRec;
if (pCur)
{
if (pCur->uEvent==uEvent)
{
m_pFirstEventRec = pCur->pNext;
pCur->pNext = m_pFreeEventRec;
m_pFreeEventRec = pCur;
return true;
}
while (true)
{
EVENT_RECORD* pPrev = pCur;
pCur = pCur->pNext;
if (!pCur) return false;
if (pCur->uEvent==uEvent)
{
pPrev->pNext = pCur->pNext;
pCur->pNext = m_pFreeEventRec;
m_pFreeEventRec = pCur;
return true;
}
}
}
return false;
}
Проблема решена. Всем ответившим в комментариях большое спасибо! А также спасибо, что досмотрели мой опус до конца)))