从一开始:自2017年3月1日起,这是Microsoft确认的错误.最后阅读评论.
简短的介绍:
我在使用MFC,ATL的大型应用程序中随机崩溃.在所有这些情况下,ATL子类化用于窗口的简单操作窗口(移动,调整大小,设置焦点,绘画等)后,我在随机执行地址上崩溃.
首先它看起来像一个狂野的指针或堆损坏,但我将完整的场景缩小到一个非常简单的应用程序使用纯ATL和只有Windows API.
要求/我使用的场景:
该应用程序是使用VS 2015 Enterprise Update 3创建的.
该程序应编译为32位.
测试应用程序使用CRT作为共享DLL.
该应用程序在Windows 10 Build 14393.693 64bit下运行(但我们在Windows 8.1和Windows Server 2012 R2下都有repros,全部是64位)
atlthunk.dll的版本为10.0.14393.0
该应用程序的作用:
它只是创建一个框架窗口,并尝试使用Windows API创建许多静态窗口.创建静态窗口后,此窗口将使用ATL CWindowImpl :: SubclassWindow方法进行子类化.在子类操作之后,发送简单的窗口消息.
怎么了:
不是每次运行,但是应用程序经常会在SendMessage上崩溃到子类窗口.在257窗口(或256 + 1的另一个倍数)上,子类以某种方式失败.创建的ATL thunk无效.似乎新子类函数的存储执行地址不正确.将任何消息发送到窗口会导致崩溃.callstack总是一样的.callstack中最后一个可见和已知的地址位于atlthunk.dll中
atlthunk.dll!AtlThunk_Call(unsigned int,unsigned int,unsigned int,long) Unknown atlthunk.dll!AtlThunk_0x00(struct HWND__ *,unsigned int,unsigned int,long) Unknown user32.dll!__InternalCallWinProc@20() Unknown user32.dll!UserCallWinProcCheckWow() Unknown user32.dll!SendMessageWorker() Unknown user32.dll!SendMessageW() Unknown CrashAtlThunk.exe!WindowCheck() Line 52 C++
调试器中抛出的异常显示为:
Exception thrown at 0x0BF67000 in CrashAtlThunk.exe: 0xC0000005: Access violation executing location 0x0BF67000.
或另一个样本
Exception thrown at 0x2D75E06D in CrashAtlThunk.exe: 0xC0000005: Access violation executing location 0x2D75E06D.
我对atlthunk.dll的了解:
Atlthunk.dll似乎只是64位操作系统的一部分.我在Win 8.1和Win 10系统上找到了它.
如果atlthunk.dll可用(所有Windows 10计算机),此DLL关心thunking.如果DLL不存在,则以标准方式完成thunking:在堆上分配块,将其标记为可执行,添加一些加载和跳转语句.
如果DLL存在.它包含256个用于子类化的预定义槽.如果完成了256个子类,则DLL会再次将自身重新加载到内存中,并使用DLL中的下一个256个可用插槽.
据我所知,atlthunk.dll属于Windows 10,不可交换或可再发行.
检查事项:
防病毒系统已启用或打开,无需更改
数据执行保护无关紧要.(/ NXCOMPAT:NO和EXE被定义为系统设置中的排除,也崩溃了)
在子类之后对FlushInstructionCache或Sleep调用的其他调用没有任何影响.
堆完整性在这里不是问题,我用多个工具重新检查它.
还有成千上万(我可能已经忘记了我测试过的东西)......;)
重现:
问题在某种程度上是可重现的.它不会一直崩溃,随机崩溃.我有一台机器,每三次执行代码崩溃.
我可以使用i7-4770和i7-6700在两个桌面工作站上进行复制.
其他机器似乎根本没有受到影响(总是在笔记本电脑i3-3217或i7-870台式机上工作)
关于样本:
为简单起见,我使用SEH处理程序来捕获错误.如果您调试应用程序,调试器将显示上面提到的callstack.程序可以在命令行上使用整数启动.在这种情况下,程序再次启动,计数递减1.因此,如果启动CrashAtlThunk 100,它将启动应用程序100次.发生错误时,SEH处理程序将捕获错误并在消息框中显示文本"Crash".如果应用程序运行没有错误,应用程序会在消息框中显示"Succeeded".如果应用程序在没有参数的情况下启动,则只执行一次.
问题:
有没有人可以重复这个?
有没有人看到类似的效果?
有人知道或可以想象出这个原因吗?
有谁知道如何解决这个问题?
笔记:
2017-01-20微软支持案例开启.
代码
// CrashAtlThunk.cpp : Defines the entry point for the application. // // Windows Header Files: #include// C RunTime Header Files #include #include #include #include #define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit #include #include #include // Global Variables: HINSTANCE hInst; // current instance const int NUM_WINDOWS = 1000; //------------------------------------------------------ // The problematic code // After the 256th subclass the application randomly crashes. class CMyWindow : public CWindowImpl { public: virtual BOOL ProcessWindowMessage(_In_ HWND hWnd, _In_ UINT uMsg, _In_ WPARAM wParam, _In_ LPARAM lParam, _Inout_ LRESULT& lResult, _In_ DWORD dwMsgMapID) override { return FALSE; } }; void WindowCheck() { HWND ahwnd[NUM_WINDOWS]; CMyWindow subclass[_countof(ahwnd)]; HWND hwndFrame; ATLVERIFY(hwndFrame = ::CreateWindow(_T("Static"), _T("Frame"), SS_SIMPLE, 0, 0, 10, 10, NULL, NULL, hInst, NULL)); for (int i = 0; i<_countof(ahwnd); ++i) { ATLVERIFY(ahwnd[i] = ::CreateWindow(_T("Static"), _T("DummyWindow"), SS_SIMPLE|WS_CHILD, 0, 0, 10, 10, hwndFrame, NULL, hInst, NULL)); if (ahwnd[i]) { subclass[i].SubclassWindow(ahwnd[i]); ATLVERIFY(SendMessage(ahwnd[i], WM_GETTEXTLENGTH, 0, 0)!=0); } } for (int i = 0; i<_countof(ahwnd); ++i) { if (ahwnd[i]) ::DestroyWindow(ahwnd[i]); } ::DestroyWindow(hwndFrame); } //------------------------------------------------------ int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { hInst = hInstance; int iCount = _tcstol(lpCmdLine, nullptr, 10); __try { WindowCheck(); if (iCount==0) { ::MessageBox(NULL, _T("Succeeded"), _T("CrashAtlThunk"), MB_OK|MB_ICONINFORMATION); } else { TCHAR szFileName[_MAX_PATH]; TCHAR szCount[16]; _itot_s(--iCount, szCount, 10); ::GetModuleFileName(NULL, szFileName, _countof(szFileName)); ::ShellExecute(NULL, _T("open"), szFileName, szCount, nullptr, SW_SHOW); } } __except (EXCEPTION_EXECUTE_HANDLER) { ::MessageBox(NULL, _T("Crash"), _T("CrashAtlThunk"), MB_OK|MB_ICONWARNING); return FALSE; } return 0; }
Eugene回答后的评论(2017年2月24日):
我不想改变我原来的问题,但我想添加一些额外的信息如何进入100%的摄制此.
1,将主要功能更改为
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { // Get the load address of ATLTHUNK.DLL // HMODULE hMod = LoadLibrary(_T("atlThunk.dll")); // Now allocate a page at the prefered start address void* pMem = VirtualAlloc(reinterpret_cast(0x0f370000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); DWORD dwLastError = ::GetLastError(); hInst = hInstance; WindowCheck(); return 0; }
取消注释LoadLibrary调用.编译.
运行一次程序并在调试器中停止.请注意加载库的地址(hMod).
停止程序.现在,再次评论库调用和修改VirtualAlloc
调用先前HMOD值的地址,这是在这个窗口会话中的首选加载地址.
重新编译并运行.崩溃!
感谢eugene.
到现在.微软仍在调查此事.他们有转储和所有代码.但我没有最终答案.事实上,我们在某些Windows 64位操作系统中存在致命错误.
我目前做了以下更改来解决这个问题
打开VS-2015的atlstdthunk.h.
完全取消注释定义USE_ATL_THUNK2的#ifdef块.代码行25到27.
重新编译您的程序.
这使得从VC-2010,VC-2013中众所周知的旧的thunking机制......这对我来说是免费的.只要没有其他已编译的库可以通过ATL以任何方式子类化或使用256个窗口.
评论(2017年3月1日):
微软证实这是一个错误.它应该在Windows 10 RS2中修复.
Mircrosoft同意编辑atlstdthunk.h中的标头是解决问题的方法.
事实上,这说.只要没有稳定的补丁我就再也不能使用正常的ATL thunking,因为我永远不会知道世界上哪些Window版本会使用我的程序.因为RS2之前的Windows 8和Windows 8.1以及Windows 10会受到此错误的影响.
最后评论(2017年3月9日):
VS-2017的搭建也受到影响,VS-2015和VS-2017之间没有区别
对于这种情况,微软决定不会对旧操作系统进行修复.
Windows 8.1,Windows Server 2012 RC2或其他Windows 10版本都不会获得修补此问题的补丁.
问题很罕见,对我们公司的影响很小.我们方面的修复也很简单.有关此错误的其他报告尚不清楚.
案件已经结案.
我对所有程序员的建议:更改Visual Studio版本VS-2015,VS-2017中的atlstdthunk.h(参见上文).我不懂微软.这个bug是ATL thunking中的一个严重问题.它可能击中每个使用更多窗口和/或子类化的程序员.
我们只知道Windows 10 RS2中的修复程序.所以旧的操作系统都受到影响!所以我建议通过注释掉上面提到的定义来禁用atlthunk.dll的使用.
这是atlthunk.dll中的错误。当它第二次加载自身时,进一步会通过MapViewOfFile调用手动发生。在这种情况下,并非相对于模块库的每个地址都已正确更改(当LoadLibarary / LoadLibraryEx加载的DLL调用系统加载程序自动执行此操作时)。然后,如果第一次将DLL加载到首选的基地址上,那么一切都将正常工作,因为未更改的地址指向相似的代码或数据。但是,如果不是这样,当第257个子类窗口处理消息时,您将崩溃。
从Vista开始,我们具有“地址空间布局随机化”功能,这解释了为什么您的代码随机崩溃。每次崩溃时,您都必须在操作系统上发现atlthunk.dll基址(在不同的OS版本上有所不同),并在第一个子类之前使用VirtualAlloc调用在该地址处进行一个内存页面地址空间的保留。要查找基址,可以使用dumpbin /headers atlthunk.dll
命令或手动解析PE标头。
我的测试表明,在Windows 10内部版本14393.693 x32版本上会受到影响,而x64则不会。在具有最新更新的Server 2012R2上,x32和x64版本均会受到影响。
顺便说一句,atlthunk.dll代码每个thunk调用的CPU指令大约是以前实现的10倍。它可能不是很重要,但是会减慢消息处理速度。