Вчера обнаружил весьма своеобразную проблему с AFX_MODULE_THREAD_STATE.
Предусловия
Проблема может возникать при выполнении следующих условий:
Код приложения не определяет макросы _WIN32_IE или _WIN32_WINNT, или их значения меньше чем 0x0300 и 0x0501 соответственно.
И
Либо MFC используется в виде статической библиотеки и приложение явно или неявно использует CThreadLocal<AFX_MODULE_THREAD_STATE>::GetData(). Например:
AFX_MODULE_STATE* moduleState = _AFX_CMDTARGET_GETSTATE();
AFX_MODULE_THREAD_STATE* threadState = moduleState->m_thread;
в данном случае CThreadLocal<>::GetData() вызывается неявно через CThreadLocal<>::operator TYPE*().
либо (вне зависимости от того используется MFC как статическая библиотека или как DLL) код приложения использует члены m_nLastStatus или m_pLastStatus класса AFX_MODULE_THREAD_STATE.
В будущем эта же самая проблема может возникать, если структуры TTTOOLINFOA и TTTOOLINFOW в следующих версиях <commctl.h> приобретут новые члены, возможно "включающиеся" соответствующими макросами, и пользователь начнет использовать новую версию PlatformSDK.
Симптомы
Непосредственно наблюдавшееся проявление заключалось в том, что приложение "падало" в функции CWnd::CancelToolTips(BOOL bKeys) при обращении к AFX_MODULE_THREAD_STATE::m_pLastStatus (...\atlmfc\src\mfc\wincore.cpp):
1062 // check for active control bar fly-by status
1063 CControlBar* pLastStatus = pModuleThreadState->m_pLastStatus;
1064 if (bKeys && pLastStatus != NULL && GetKeyState(VK_LBUTTON) >= 0)
1065 pLastStatus->SetStatusText(static_cast<INT_PTR>(-1));
Также я полагаю, что эта же проблема может проявляться в похожих "падениях" или "стрельбе по памяти" в других местах при доступе к AFX_MODULE_THREAD_STATE::m_pLastStatus:
...\atlmfc\src\mfc\barcore.cpp(203): if (pModuleThreadState->m_pLastStatus == this)
...\atlmfc\src\mfc\barcore.cpp(205): pModuleThreadState->m_pLastStatus = NULL;
...\atlmfc\src\mfc\barcore.cpp(336): pModuleThreadState->m_pLastStatus = NULL;
...\atlmfc\src\mfc\barcore.cpp(350): pModuleThreadState->m_pLastStatus = this;
...\atlmfc\src\mfc\barcore.cpp(576): if (pModuleThreadState->m_pLastStatus == this)
...\atlmfc\src\mfc\barcore.cpp(579): ASSERT(pModuleThreadState->m_pLastStatus == NULL);
или AFX_MODULE_THREAD_STATE::m_nLastStatus:
...\atlmfc\src\mfc\afxstate.cpp(149): m_nLastStatus = static_cast<INT_PTR>(-1);
...\atlmfc\src\mfc\barcore.cpp(206): pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
...\atlmfc\src\mfc\barcore.cpp(279): pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
...\atlmfc\src\mfc\barcore.cpp(291): pModuleThreadState->m_nLastStatus = static_cast<INT_PTR>(-1);
...\atlmfc\src\mfc\barcore.cpp(298): if (pModuleThreadState->m_nLastStatus == static_cast<INT_PTR>(-1))
...\atlmfc\src\mfc\barcore.cpp(348): if (!(m_nStateFlags & statusSet) || pModuleThreadState->m_nLastStatus != nHit)
...\atlmfc\src\mfc\barcore.cpp(394): nHit = pModuleThreadState->m_nLastStatus;
...\atlmfc\src\mfc\barcore.cpp(416): else if (nHit != pModuleThreadState->m_nLastStatus)
...\atlmfc\src\mfc\barcore.cpp(420): pModuleThreadState->m_nLastStatus = nHit;
Причина
Т.к. AFX_MODULE_THREAD_STATE включает TOOLINFO m_lastInfo по значению, и sizeof(TOOLINFO) зависит от макросов _WIN32_IE и _WIN32_WINNT, очевидно, что при несовпадении значений этих макро-определений с теми, которые использовались при компиляции MFC, могут начинаться сюрпризы. В частности, любой непосредственный доступ к членам m_nLastStatus и m_pLastStatus класса AFX_MODULE_THREAD_STATE из кода приложения может приводить к получению неверных значений и/или порче памяти, т.к. эти члены будут расположены по другим смещениям, чем в версии AFX_MODULE_THREAD_STATE, которую "видит" MFC.
Но проблема даже хуже, т.к. даже если пользовательский код не содержит обращений к упомянутым членам класса AFX_MODULE_THREAD_STATE, данные могут быть запорчены прямо
изнутри кода MFC. Чтобы это произошло, достаточно, чтобы приложение было прилинковано к MFC статически, и чтобы пользовательский код содержал вызов CThreadLocal<AFX_MODULE_THREAD_STATE>::GetData() явно или неявно, через члены operator TYPE*() или operator->() шаблона CThreadLocal<>.
Когда компилятор встречает подобный вызов в коде пользователя, инстанциируются члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE>. Будучи инстанциирована из пользовательского кода со значениями _WIN32_IE или _WIN32_WINNT отличающимися от тех, что были использованы при компиляции MFC, CreateObject() будет создавать объекты AFX_MODULE_THREAD_STATE другого размера, нежели это ожидается MFC.
Т.к. в приложении может быть только один экземпляр одного и того же члена специализации шаблона класса, компилятор вполне может выберать специализацию, порожденную из пользовательского кода. Более того, это даже более вероятно чем то, что будет выбрана специализация, содержащаяся в коде MFC.
В свою очередь это приведет к тому, что CNoTrackObject::operator new(size_t nSize) будет вызван с nSize < sizeof(AFX_MODULE_THREAD_STATE), ожидаемого MFC. Конструктор AFX_MODULE_THREAD_STATE полагается на то, что CNoTrackObject::operator new заполнит память нулями, поэтому членm_pLastStatus останется неинициализированным. Если и _WIN32_IE < 0x0300 и _WIN32_WINNT < 0x0501, то конструкторAFX_MODULE_THREAD_STATE осуществит запись за пределами выделенного блока при инициализации члена m_nLastStatus.
Решение
Собственно говоря, текущий дизайн класса AFX_MODULE_THREAD_STATE можно считать дефектным, т.к. он слишком слабо устойчив к смене версии PlatformSDK и значений макросов _WIN32_IE и _WIN32_WINNT, используемых приложением. Так что настоящим решением был бы редизайн класса AFX_MODULE_THREAD_STATE так, чтобы члены не были открыты, и, например, член m_lastInfo хранился бы на куче.
Т.к. это не в наших силах, лучшее, что можно сделать сегодня, — не трогать член AFX_MODULE_STATE::m_thread, а вместо этого пользоваться функцией AfxGetModuleThreadState(). В этом случае члены GetData() и CreateObject() класса CThreadLocal<AFX_MODULE_THREAD_STATE> из пользовательского кода инстанциироваться не будут.
Если же приложение содержит обращения к членам m_nLastStatus или m_pLastStatus, то ничего, кроме как следить за синхронностью значений _WIN32_IE и _WIN32_WINNT с теми, что использовались для сборки MFC, не остается.
Легче одурачить людей, чем убедить их в том, что они одурачены. — Марк Твен
Здравствуйте, Павел Кузнецов, Вы писали:
ПК>Вчера обнаружил весьма своеобразную проблему с AFX_MODULE_THREAD_STATE.
Спасибо за инфу, Павел!
Как я понял, ты 7-ку терзал (или 7.1)... интересно, в 6-ке та же проблема есть? ИМХО _WIN32_IE вряд ли кто-то определит меньше чем 0x0300, а вот _WIN32_WINNT — это уже хуже. Хотя MS, небось, отмажется тем, что AFX_MODULE_STATE и AFX_MODULE_THREAD_STATE — это "внутренние" структуры и нечего туда лазать.
P.S.
Увидев в форуме
MFC пост от "Павел Кузнецов" в первый момент подумал, что у меня Янус заглючил.
[ posted via RSDN@Home 1.1.2 stable ]