1

Я тут новенький на сайте, по-этому очень прошу не удалять мой пост сразу, как это делают в англоязычном сегменте. А где ещё искать решения моей проблемы? Я сам испробовал все доступные мне пути отладки.

В моей программе имеется класс, и в нём метод. Так вот, если этот метод объявлен как __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;
}

Полный текст программы с бинарником - по ссылке.

LShadow77
  • 2,157
  • 1
    Таким темпом и тут удалят. Исходный код вместе с параметрами компиляции должен присутствовать в самом вопросе. [mcve] – user7860670 Apr 04 '20 at 16:31
  • Я как раз сейчас пытаюсь отыскать ответ в дизассемблере - моё крайнее средство, когда я совсем отчаялся... – LShadow77 Apr 04 '20 at 16:38
  • 2
    Рекомендация была привести [mcve]. Не надо вываливать сюда весь свой код, тем более, что большая его часть явно ни каким образом не относится к проблеме. – user7860670 Apr 04 '20 at 16:58
  • я бы переписал класс CInput с использованием https://en.cppreference.com/w/cpp/container/queue и https://en.cppreference.com/w/cpp/container/stack – Maxim Timakov Apr 04 '20 at 17:05
  • 2
    и смотрите в метод bool CInput::CheckEvent(UINT uEvent): как мне кажется ошибка в EVENT_RECORD* pPrev = (EVENT_RECORD*)&m_pFirstEventRec; – Maxim Timakov Apr 04 '20 at 17:08
  • user7860670, это и есть минимальный воспроизводимый пример, почищенный от всего лишнего. В оригинале - почти 1500 строк кода. – LShadow77 Apr 04 '20 at 17:11
  • Maxim Timakov, это не ошибка, а трюк, которым я пользуюсь уже давно и никогда не имел с ним проблем. адрес указателя m_pFirstEventRec физически совпадает с полем EVENT_RECORD::pNext. Соответственно, когда удаляемый в цикле из списка элемент является первым, поле m_pFirstEventRec автоматически перезаписывается на адрес следующего элемента в списке. – LShadow77 Apr 04 '20 at 17:15
  • Я погонял код, не зависает. Добавил пару незначащих функций - программа перестает работать. Вы где-то память простреливаете, наверное, или с указателями перемудрили. – Croessmah stands with Russia Apr 04 '20 at 17:20
  • Интересно было бы на MSVC++ погонять, такая же будет беда или нет? – LShadow77 Apr 04 '20 at 17:41
  • Попробуйте скомпилировать с -fno-strict-aliasing – avp Apr 04 '20 at 18:24
  • 1
    avp, попробовал - баг исчез!!! Неужели таки косяк компилятора? – LShadow77 Apr 04 '20 at 18:30
  • Нет. Скорее косяки стандартов (или мозгов). Просто подумайте об экономике, грантах, 25 млн. программерах и энтропии. – avp Apr 04 '20 at 19:17
  • Иногда проверяйте адреса объекта класса и первого элемента. Вы очень удивитесь. – AlexGlebe Apr 04 '20 at 19:28
  • 1
    Кажись нашёл! Почти весь день убил, распутывая ассемблерный винегрет, который обычно создаёт GCC, но вроде бы докопался до истины. Это действительно оказалась некорректная работа оптимизатора! Вот отдохну, на ясную голову ещё раз всё проверю и детально напишу ответ... – LShadow77 Apr 04 '20 at 19:35
  • AlexGlebe нет, тут дело не в этом. Чуть позже опишу, что обнаружил. – LShadow77 Apr 04 '20 at 19:43
  • От (EVENT_RECORD*)&m_pFirstEventRec; пахнет нарушением strict aliasing-а. Тогда дело таки не в компиляторе. – HolyBlackCat Apr 04 '20 at 19:52
  • Попробуйте EVENT_RECORD* pPrev = std::launder((EVENT_RECORD*)&m_pFirstEventRec);. – HolyBlackCat Apr 04 '20 at 20:13
  • HolyBlackCat, да! Как оказалось, уши действительно от этой строки растут. Грубо говоря, компилятор этому полю поставил в соответствие регистр. А затем в цикле это поле неявно изменилось (как и задумывалось). Однако компилятор этого не просёк, по-прежнему считая значение в регистре идентичным тому, что в памяти, вот и пошло всё в разнос. К слову, объявление поля m_pFirstEventRec как volatile также решает эту проблему. – LShadow77 Apr 04 '20 at 20:13
  • А launder работает? – HolyBlackCat Apr 04 '20 at 20:19
  • HolyBlackCat, не люблю std, если честно. Забочусь о производительности, плюс склоне к нестандартным решением)) Но порой случается, что выскакивают вот такие сюрпризы от компилятора (этот - не первый, если честно) – LShadow77 Apr 04 '20 at 20:23
  • launder - "магический", его нельзя реализовать самому (по крайней мере без встроенных в компилятор функций). По идее, он должен исправлять как раз такие ошибки оптимизатора - когда указатель на объект получается через одно место. – HolyBlackCat Apr 04 '20 at 20:27
  • У меня вообще пишет "error: 'launder' is not a member of 'std'". Ну да и ладно, volatile вроде прекрасно работает! – LShadow77 Apr 04 '20 at 20:30
  • @LShadow77 Даже с #include <memory>? – HolyBlackCat Apr 04 '20 at 20:35
  • 2
    Вам стоит ознакомиться с Что такое strict aliasing?, а вот пример минимального воспроизводимого примера (до которого можно было бы сократить полотно в этом посте). volatile в такой ситуации никак не помогает. @HolyBlackCat И launder по идее тоже, ведь динамический тип объекта же не правильный. – user7860670 Apr 04 '20 at 20:46
  • @user7860670 Честно говоря, даже не разобрался, что там за колдунство с указателями происходит. – HolyBlackCat Apr 04 '20 at 20:48
  • 1
    @HolyBlackCat и с , и с - not a member – LShadow77 Apr 04 '20 at 20:49
  • для lauderer нужно перейти на С++17 используя опцию --std=c++17 – user7860670 Apr 04 '20 at 20:51
  • @user7860670 почему же volatile не помогает? Насколько я знаю, в этом и заключается предназначение этого модификатора - отключение всякой оптимизации для отдельно взятой переменной, чтобы данные всегда корректно в неё попадали (во всяком случае, примерно так был написано в одной из книг по теме). Как и в этом случае - с volatile всё работает. Про strict aliasing почитаю подробнее. Если честно, то в первый раз столкнулся вот так, лицом к лицу с этим зверем)) – LShadow77 Apr 04 '20 at 20:58
  • 1
    У volatile может быть разная семантика, но вообще-то это просто одно из 4 действий с побочными эффектами на выполнение которых потоком управления может рассчитывать компилятор. Оно ни разу не подразумевает "отключение всякой оптимизации" и никак не виляет на алиасинг. – user7860670 Apr 04 '20 at 21:03

1 Answers1

3

В общем, я докопался до истины и понял, где именно кроется ошибка. Приведу сначала разобранный мною код тела функции 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;
}

Проблема решена. Всем ответившим в комментариях большое спасибо! А также спасибо, что досмотрели мой опус до конца)))

LShadow77
  • 2,157
  • 3
    Это не баг компилятора. Компилятор сгененрировал такой код законно. Вы сами нарушили правила strict-aliasing'а, поэтому получили проблему. – Croessmah stands with Russia Apr 05 '20 at 09:30
  • @Croessmah OK, признаю что написал код не по канону. Из благих побуждений всё)) До этого случая подобные трюки прокатывали. Больше так делать не буду)) – LShadow77 Apr 05 '20 at 09:33
  • вот, кстати, статьи на тему: https://habr.com/ru/company/otus/blog/442554/ и https://habr.com/ru/post/114117/ хотя я могу и ошибаться. Надо будет разобраться получше с примером. ) – Croessmah stands with Russia Apr 05 '20 at 09:45
  • Наверное более соответсвующи истине было бы вот такое объявление volatile EVENT_RECORD *pPrev = (volatile EVENT_RECORD *)&m_pFirstEventRec; – avp Apr 05 '20 at 09:55