Windows 10 64 位 ATL 子类随机崩溃
Random crashes on Windows 10 64bit with ATL subclassing
刚开始:自 2017 年 3 月 1 日起,这是 Microsoft 确认的错误。阅读最后的评论。
简短描述:
我在使用 MFC、ATL 的大型应用程序中遇到随机崩溃。在所有这些情况下,在 ATL 子类化用于 window 后,使用 window 进行简单操作(移动、调整大小、设置焦点、绘画等),我在随机执行地址上崩溃。
首先它看起来像一个野指针或堆损坏,但我将整个场景缩小到一个非常简单的应用程序,使用纯 ATL 且仅 Windows API。
需求/我使用的场景:
- 该应用程序是使用 VS 2015 Enterprise Update 3 创建的。
- 程序应编译为 32 位。
- 测试应用程序使用 CRT 作为共享 DLL。
- Windows10 Build 14393.693 64 位下的应用程序 运行s(但我们在 Windows8.1 和 WindowsServer 2012 R2 下有重现,均为 64 位)
- atlthunk.dll 有版本 10.0.14393.0
应用程序的作用:
它只是创建一个框架 window 并尝试使用 windows API 创建许多静态 windows。
创建静态 window 后,此 window 将使用 ATL CWindowImpl::SubclassWindow 方法进行子类化。
子类操作后发送一个简单的 window 消息。
发生了什么:
并非在每个 运行 上,但应用程序经常在向子类 window 发送消息时崩溃。
在 257 window(或 256+1 的另一个倍数)上,子类以某种方式失败。创建的 ATL thunk 无效。新的 subclass-function 存储的执行地址似乎不正确。
将任何消息发送到 window 都会导致崩溃。
调用堆栈始终相同。调用堆栈中最后可见和已知的地址位于 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好像只是64bit的一部分OS。我在 Win 8.1 和 Win 10 系统上找到它。
如果 atlthunk.dll 可用(所有 Windows 10 台机器),此 DLL 会关心 thunking。如果 DLL 不存在,thunk 将以标准方式完成:在堆上分配一个块,将其标记为可执行,添加一些加载和跳转语句。
如果 DLL 存在。它包含 256 个用于子类化的预定义插槽。如果完成了 256 个子类,DLL 会再次将自身重新加载到内存中,并使用 DLL 中接下来的 256 个可用插槽。
据我所知,atlthunk.dll 属于 Windows 10,不可交换或再分发。
已检查的内容:
- 杀毒系统开启或开启,无变化
- 数据执行保护无关紧要。 (/NXCOMPAT:NO 并且 EXE 在系统设置中被定义为排除项,也崩溃)
- 在子类之后对 FlushInstructionCache 或 Sleep 调用的额外调用没有任何效果。
- 这里堆完整性不是问题,我用不止一种工具重新检查了它。
- 还有数千个(我可能已经忘记我测试了什么)... ;)
再现性:
这个问题在某种程度上是可以重现的。它不会一直崩溃,它会随机崩溃。我有一台机器,代码每执行三次就会崩溃。
我可以用 i7-4770 和 i7-6700 在两个桌面站上重现它。
其他机器似乎完全不受影响(始终在笔记本电脑 i3-3217 或台式机 i7-870 上工作)
关于样本:
为简单起见,我使用 SEH 处理程序来捕获错误。如果您调试应用程序,调试器将显示上面提到的调用堆栈。
该程序可以在命令 line.In 上使用整数启动,在这种情况下,如果您启动 CrashAtlThunk 100,程序将再次启动,计数会减去 1.So,它将启动应用程序 100 次。出现错误时,SEH 处理程序将捕获错误并在消息框中显示文本 "Crash"。如果应用程序 运行 没有错误,应用程序会在消息框中显示 "Succeeded"。
如果应用程序在没有参数的情况下启动,它只执行一次。
问题:
- 还有其他人可以复制吗?
- 有没有人看到类似的效果?
- 有没有人知道或可以想象这是什么原因?
- 有人知道如何解决这个问题吗?
备注:
2017-01-20 Microsoft 支持案例打开。
代码
// CrashAtlThunk.cpp : Defines the entry point for the application.
//
// Windows Header Files:
#include <windows.h>
// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit
#include <atlbase.h>
#include <atlstr.h>
#include <atlwin.h>
// 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<CMyWindow>
{
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、将main函数改成
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<void*>(0x0f370000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
DWORD dwLastError = ::GetLastError();
hInst = hInstance;
WindowCheck();
return 0;
}
取消注释 LoadLibrary 调用。编译.
运行 编程一次并在调试器中停止。记下加载库的地址 (hMod)。
停止程序。现在再次注释Library call,将VirtualAlloc
call改成之前hMod值的地址,这是本window session.
[=中的首选加载地址173=]
重新编译并运行。崩溃!
感谢尤金。
到目前为止。微软仍在对此进行调查。他们有转储和所有代码。但我没有最终的答案。 事实上我们在某些 Windows 64 位 OS.
中有一个致命错误
我目前做了以下更改来解决这个问题
打开VS-2015的atlstdthunk.h。
完全取消注释定义 USE_ATL_THUNK2 的 #ifdef 块。代码行 25 到 27.
重新编译你的程序。
这启用了从 VC-2010、VC-2013... 众所周知的旧 thunking 机制,这对我来说没有崩溃。只要不涉及其他已编译的库,它们可以通过 ATL 以任何方式子类化或使用 256 windows。
评论(2017 年 3 月 1 日):
- Microsoft 确认这是一个错误。应该固定在Windows 10 RS2.
- Microsoft 同意编辑 atlstdthunk.h 中的 headers 是解决该问题的方法。
其实是这样说的。只要没有稳定的补丁,我就再也不能使用正常的 ATL thunking,因为我永远不知道世界上有哪些 Window 版本会使用我的程序。因为 RS2 之前的 Windows 8 和 Windows 8.1 和 Windows 10 会受到此错误的影响。
最终评论(2017 年 3 月 9 日):
- 使用 VS-2017 的构建也会受到影响,VS-2015 和 VS-2017 之间没有区别
- 对于这种情况,Microsoft 决定不修复旧 OS。
- Windows 8.1、Windows Server 2012 RC2 或其他 Windows 10 版本都不会获得修复此问题的补丁。
- 问题比较少见,对我们公司影响不大。我们这边的修复也很简单。此错误的其他报告未知。
- 案件结案。
我对所有程序员的建议:在 Visual Studio 版本 VS-2015、VS-2017(见上文)中更改 atlstdthunk.h。 我不懂微软。此错误是 ATL thunking 中的一个严重问题。它可能会打击每个使用更多 windows and/or 子类化的程序员。
我们只知道 Windows 10 RS2 中的一个修复程序。所以所有年长的 OS 都会受到影响!所以我建议通过注释掉上面提到的定义来禁用 atlthunk.dll 的使用。
这是 atlthunk.dll 中的错误。当它加载自身 第二次并进一步 时,这是通过 MapViewOfFile 调用手动发生的。在这种情况下,并非每个相对于模块基址的地址都被正确更改(当 LoadLibarary/LoadLibraryEx 调用系统加载程序加载 DLL 时会自动执行此操作)。然后,如果 第一次 时间 DLL 加载到 首选基地址 上,一切正常,因为未更改的地址指向相似的代码或数据。但如果不是,当第 257 个子类 window 处理消息时你会崩溃。
自 Vista 以来,我们有 "address space layout randomization" 功能,这解释了为什么您的代码会随机崩溃。每次必须在 OS 上发现 atlthunk.dll 基地址(它在不同的 OS 版本上有所不同)并在该地址做一个内存页地址 space 保留时都会崩溃在第一个子类 之前使用 VirtualAlloc 调用 。要查找基地址,您可以使用 dumpbin /headers atlthunk.dll
命令或手动解析 PE headers。
我的测试显示 Windows 10 build 14393.693 x32 版本受到影响,但 x64 没有。在具有最新更新的 Server 2012R2 上,两个(x32 和 x64)版本都受到影响。
顺便说一句,atlthunk.dll 代码的每个 thunk 调用的指令数量是以前实现的大约 10 倍 CPU 。它可能不是很重要,但会减慢消息处理速度。
已经描述的稍微更自动化的形式:
// A minimum ATL program with more than 256 windows. In practise they would not be toplevel, but e.g. buttons.
// Thanks to https://www.codeguru.com/cpp/com-tech/atl/article.php/c3605/Using-the-ATL-Windowing-Classes.htm
// for helping with ATL.
// You need to be up to date, like have KB3030947 or KB3061512. Otherwise asserts will fail instead.
#undef _DEBUG
#include <atlbase.h>
ATL::CComModule _Module;
#include <atlwin.h>
#include <assert.h>
#include <string>
BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP()
struct CMyWindow : CWindowImpl<CMyWindow>
{
BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP()
};
int __cdecl wmain()
{
// Exacerbate the problem, which can happen more like if by chance.
PROCESS_INFORMATION process = { 0 };
{
// Be sure another process has atlthunk loaded.
WCHAR cmd[] = L"rundll32 atlthunk,x";
STARTUPINFOW startup = { sizeof(startup) };
BOOL success = CreateProcessW(0, cmd, 0, 0, 0, 0, 0, 0, &startup, &process);
assert(success && process.hProcess);
CloseHandle(process.hThread);
// Get atlthunk's usual address.
HANDLE file = CreateFileW((std::wstring(_wgetenv(L"SystemRoot")) + L"\system32\atlthunk.dll").c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
assert(file != INVALID_HANDLE_VALUE);
HANDLE mapping = CreateFileMappingW(file, 0, PAGE_READONLY | SEC_IMAGE, 0, 0, 0);
assert(mapping);
void* view = MapViewOfFile(mapping, 0, 0, 0, 0);
assert(view);
UnmapViewOfFile(view);
VirtualAlloc(view, 1, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS);
}
_Module.Init(0, 0);
const int N = 300;
CMyWindow wnd[N];
for (int i = 0; i < N; ++i)
{
wnd[i].Create(0, CWindow::rcDefault, L"Hello", (i < N - 1) ? 0 : (WS_OVERLAPPEDWINDOW | WS_VISIBLE));
wnd[i].DestroyWindow();
}
TerminateProcess(process.hProcess, 0);
CloseHandle(process.hProcess);
MSG msg;
while (GetMessageW(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
_Module.Term();
}
刚开始:自 2017 年 3 月 1 日起,这是 Microsoft 确认的错误。阅读最后的评论。
简短描述:
我在使用 MFC、ATL 的大型应用程序中遇到随机崩溃。在所有这些情况下,在 ATL 子类化用于 window 后,使用 window 进行简单操作(移动、调整大小、设置焦点、绘画等),我在随机执行地址上崩溃。
首先它看起来像一个野指针或堆损坏,但我将整个场景缩小到一个非常简单的应用程序,使用纯 ATL 且仅 Windows API。
需求/我使用的场景:
- 该应用程序是使用 VS 2015 Enterprise Update 3 创建的。
- 程序应编译为 32 位。
- 测试应用程序使用 CRT 作为共享 DLL。
- Windows10 Build 14393.693 64 位下的应用程序 运行s(但我们在 Windows8.1 和 WindowsServer 2012 R2 下有重现,均为 64 位)
- atlthunk.dll 有版本 10.0.14393.0
应用程序的作用:
它只是创建一个框架 window 并尝试使用 windows API 创建许多静态 windows。 创建静态 window 后,此 window 将使用 ATL CWindowImpl::SubclassWindow 方法进行子类化。 子类操作后发送一个简单的 window 消息。
发生了什么:
并非在每个 运行 上,但应用程序经常在向子类 window 发送消息时崩溃。 在 257 window(或 256+1 的另一个倍数)上,子类以某种方式失败。创建的 ATL thunk 无效。新的 subclass-function 存储的执行地址似乎不正确。 将任何消息发送到 window 都会导致崩溃。 调用堆栈始终相同。调用堆栈中最后可见和已知的地址位于 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好像只是64bit的一部分OS。我在 Win 8.1 和 Win 10 系统上找到它。
如果 atlthunk.dll 可用(所有 Windows 10 台机器),此 DLL 会关心 thunking。如果 DLL 不存在,thunk 将以标准方式完成:在堆上分配一个块,将其标记为可执行,添加一些加载和跳转语句。
如果 DLL 存在。它包含 256 个用于子类化的预定义插槽。如果完成了 256 个子类,DLL 会再次将自身重新加载到内存中,并使用 DLL 中接下来的 256 个可用插槽。
据我所知,atlthunk.dll 属于 Windows 10,不可交换或再分发。
已检查的内容:
- 杀毒系统开启或开启,无变化
- 数据执行保护无关紧要。 (/NXCOMPAT:NO 并且 EXE 在系统设置中被定义为排除项,也崩溃)
- 在子类之后对 FlushInstructionCache 或 Sleep 调用的额外调用没有任何效果。
- 这里堆完整性不是问题,我用不止一种工具重新检查了它。
- 还有数千个(我可能已经忘记我测试了什么)... ;)
再现性:
这个问题在某种程度上是可以重现的。它不会一直崩溃,它会随机崩溃。我有一台机器,代码每执行三次就会崩溃。
我可以用 i7-4770 和 i7-6700 在两个桌面站上重现它。
其他机器似乎完全不受影响(始终在笔记本电脑 i3-3217 或台式机 i7-870 上工作)
关于样本:
为简单起见,我使用 SEH 处理程序来捕获错误。如果您调试应用程序,调试器将显示上面提到的调用堆栈。 该程序可以在命令 line.In 上使用整数启动,在这种情况下,如果您启动 CrashAtlThunk 100,程序将再次启动,计数会减去 1.So,它将启动应用程序 100 次。出现错误时,SEH 处理程序将捕获错误并在消息框中显示文本 "Crash"。如果应用程序 运行 没有错误,应用程序会在消息框中显示 "Succeeded"。 如果应用程序在没有参数的情况下启动,它只执行一次。
问题:
- 还有其他人可以复制吗?
- 有没有人看到类似的效果?
- 有没有人知道或可以想象这是什么原因?
- 有人知道如何解决这个问题吗?
备注:
2017-01-20 Microsoft 支持案例打开。
代码
// CrashAtlThunk.cpp : Defines the entry point for the application.
//
// Windows Header Files:
#include <windows.h>
// C RunTime Header Files
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <tchar.h>
#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit
#include <atlbase.h>
#include <atlstr.h>
#include <atlwin.h>
// 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<CMyWindow>
{
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、将main函数改成
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<void*>(0x0f370000), 0x10000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
DWORD dwLastError = ::GetLastError();
hInst = hInstance;
WindowCheck();
return 0;
}
取消注释 LoadLibrary 调用。编译.
运行 编程一次并在调试器中停止。记下加载库的地址 (hMod)。
停止程序。现在再次注释Library call,将
[=中的首选加载地址173=]VirtualAlloc
call改成之前hMod值的地址,这是本window session.重新编译并运行。崩溃!
感谢尤金。
到目前为止。微软仍在对此进行调查。他们有转储和所有代码。但我没有最终的答案。 事实上我们在某些 Windows 64 位 OS.
中有一个致命错误我目前做了以下更改来解决这个问题
打开VS-2015的atlstdthunk.h。
完全取消注释定义 USE_ATL_THUNK2 的 #ifdef 块。代码行 25 到 27.
重新编译你的程序。
这启用了从 VC-2010、VC-2013... 众所周知的旧 thunking 机制,这对我来说没有崩溃。只要不涉及其他已编译的库,它们可以通过 ATL 以任何方式子类化或使用 256 windows。
评论(2017 年 3 月 1 日):
- Microsoft 确认这是一个错误。应该固定在Windows 10 RS2.
- Microsoft 同意编辑 atlstdthunk.h 中的 headers 是解决该问题的方法。
其实是这样说的。只要没有稳定的补丁,我就再也不能使用正常的 ATL thunking,因为我永远不知道世界上有哪些 Window 版本会使用我的程序。因为 RS2 之前的 Windows 8 和 Windows 8.1 和 Windows 10 会受到此错误的影响。
最终评论(2017 年 3 月 9 日):
- 使用 VS-2017 的构建也会受到影响,VS-2015 和 VS-2017 之间没有区别
- 对于这种情况,Microsoft 决定不修复旧 OS。
- Windows 8.1、Windows Server 2012 RC2 或其他 Windows 10 版本都不会获得修复此问题的补丁。
- 问题比较少见,对我们公司影响不大。我们这边的修复也很简单。此错误的其他报告未知。
- 案件结案。
我对所有程序员的建议:在 Visual Studio 版本 VS-2015、VS-2017(见上文)中更改 atlstdthunk.h。 我不懂微软。此错误是 ATL thunking 中的一个严重问题。它可能会打击每个使用更多 windows and/or 子类化的程序员。
我们只知道 Windows 10 RS2 中的一个修复程序。所以所有年长的 OS 都会受到影响!所以我建议通过注释掉上面提到的定义来禁用 atlthunk.dll 的使用。
这是 atlthunk.dll 中的错误。当它加载自身 第二次并进一步 时,这是通过 MapViewOfFile 调用手动发生的。在这种情况下,并非每个相对于模块基址的地址都被正确更改(当 LoadLibarary/LoadLibraryEx 调用系统加载程序加载 DLL 时会自动执行此操作)。然后,如果 第一次 时间 DLL 加载到 首选基地址 上,一切正常,因为未更改的地址指向相似的代码或数据。但如果不是,当第 257 个子类 window 处理消息时你会崩溃。
自 Vista 以来,我们有 "address space layout randomization" 功能,这解释了为什么您的代码会随机崩溃。每次必须在 OS 上发现 atlthunk.dll 基地址(它在不同的 OS 版本上有所不同)并在该地址做一个内存页地址 space 保留时都会崩溃在第一个子类 之前使用 VirtualAlloc 调用 。要查找基地址,您可以使用 dumpbin /headers atlthunk.dll
命令或手动解析 PE headers。
我的测试显示 Windows 10 build 14393.693 x32 版本受到影响,但 x64 没有。在具有最新更新的 Server 2012R2 上,两个(x32 和 x64)版本都受到影响。
顺便说一句,atlthunk.dll 代码的每个 thunk 调用的指令数量是以前实现的大约 10 倍 CPU 。它可能不是很重要,但会减慢消息处理速度。
已经描述的稍微更自动化的形式:
// A minimum ATL program with more than 256 windows. In practise they would not be toplevel, but e.g. buttons.
// Thanks to https://www.codeguru.com/cpp/com-tech/atl/article.php/c3605/Using-the-ATL-Windowing-Classes.htm
// for helping with ATL.
// You need to be up to date, like have KB3030947 or KB3061512. Otherwise asserts will fail instead.
#undef _DEBUG
#include <atlbase.h>
ATL::CComModule _Module;
#include <atlwin.h>
#include <assert.h>
#include <string>
BEGIN_OBJECT_MAP(ObjectMap) END_OBJECT_MAP()
struct CMyWindow : CWindowImpl<CMyWindow>
{
BEGIN_MSG_MAP(CMyWindow) END_MSG_MAP()
};
int __cdecl wmain()
{
// Exacerbate the problem, which can happen more like if by chance.
PROCESS_INFORMATION process = { 0 };
{
// Be sure another process has atlthunk loaded.
WCHAR cmd[] = L"rundll32 atlthunk,x";
STARTUPINFOW startup = { sizeof(startup) };
BOOL success = CreateProcessW(0, cmd, 0, 0, 0, 0, 0, 0, &startup, &process);
assert(success && process.hProcess);
CloseHandle(process.hThread);
// Get atlthunk's usual address.
HANDLE file = CreateFileW((std::wstring(_wgetenv(L"SystemRoot")) + L"\system32\atlthunk.dll").c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
assert(file != INVALID_HANDLE_VALUE);
HANDLE mapping = CreateFileMappingW(file, 0, PAGE_READONLY | SEC_IMAGE, 0, 0, 0);
assert(mapping);
void* view = MapViewOfFile(mapping, 0, 0, 0, 0);
assert(view);
UnmapViewOfFile(view);
VirtualAlloc(view, 1, MEM_COMMIT | MEM_RESERVE, PAGE_NOACCESS);
}
_Module.Init(0, 0);
const int N = 300;
CMyWindow wnd[N];
for (int i = 0; i < N; ++i)
{
wnd[i].Create(0, CWindow::rcDefault, L"Hello", (i < N - 1) ? 0 : (WS_OVERLAPPEDWINDOW | WS_VISIBLE));
wnd[i].DestroyWindow();
}
TerminateProcess(process.hProcess, 0);
CloseHandle(process.hProcess);
MSG msg;
while (GetMessageW(&msg, 0, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
_Module.Term();
}