使用 ATL 子类化的 Windows 10 64 位随机崩溃
Posted
技术标签:
【中文标题】使用 ATL 子类化的 Windows 10 64 位随机崩溃【英文标题】:Random crashes on Windows 10 64bit with ATL subclassing 【发布时间】:2017-06-04 03:09:53 【问题描述】:刚开始:自 2017 年 3 月 1 日起,这是 Microsoft 确认的错误。最后阅读 cmets。
简短说明:
我在使用 MFC、ATL 的大型应用程序中发生随机崩溃。在所有这些情况下,在对窗口进行简单操作(移动、调整大小、设置焦点、绘画等)后,将 ATL 子类化用于窗口后,我在随机执行地址上崩溃。
首先它看起来像一个野指针或堆损坏,但我将整个场景缩小到一个非常简单的应用程序,它使用纯 ATL 和仅 Windows API。
要求/我使用的场景:
应用程序是使用 VS 2015 Enterprise Update 3 创建的。 程序应编译为 32 位。 测试应用程序使用 CRT 作为共享 DLL。 该应用程序在 Windows 10 Build 14393.693 64 位下运行(但我们在 Windows 8.1 和 Windows Server 2012 R2 下运行,均为 64 位) atlthunk.dll 的版本为 10.0.14393.0应用程序的作用:
它只是创建一个框架窗口并尝试使用 windows API 创建许多静态窗口。 创建静态窗口后,此窗口将使用 ATL CWindowImpl::SubclassWindow 方法进行子类化。 在子类操作之后发送一个简单的窗口消息。
会发生什么:
不是在每次运行时,应用程序都会在 SendMessage 到子类窗口时崩溃。 在 257 窗口(或 256+1 的另一个倍数)上,子类以某种方式失败。创建的 ATL thunk 无效。新子类函数的存储执行地址似乎不正确。 将任何消息发送到窗口会导致崩溃。 调用堆栈始终相同。调用堆栈中最后一个可见且已知的地址在 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 的了解:
Atlhunk.dll 似乎只是 64 位操作系统的一部分。我在 Win 8.1 和 Win 10 系统上找到它。
如果 atlhunk.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 处理程序来捕获错误。如果您调试应用程序,调试器将显示上述调用堆栈。 该程序可以在命令行上使用整数启动。在这种情况下,程序会再次启动自身,计数减 1。因此,如果您启动 CrashAtlThunk 100,它将启动应用程序 100 次。发生错误时,SEH 处理程序将捕获错误并在消息框中显示文本“Crash”。如果应用程序运行时没有错误,应用程序会在消息框中显示“成功”。 如果应用程序在没有参数的情况下启动,它只会执行一次。
问题:
还有其他人可以复制吗? 有人看到过类似的效果吗? 有谁知道或能想象出这种情况的原因吗? 有人知道如何解决这个问题吗?注意事项:
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% Repro。
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 调用并将 VirtualAlloc
调用更改为先前 hMod 值的地址,这是此窗口会话中的首选加载地址。
重新编译并运行。崩溃!
感谢尤金。
到现在为止。微软仍在对此进行调查。他们有转储和所有代码。但我没有最终答案。 事实是我们在某些 Windows 64 位操作系统中存在致命错误。
我目前做了以下更改来解决这个问题
打开 VS-2015 的 atlstdthunk.h。
完全取消注释定义 USE_ATL_THUNK2 的 #ifdef 块。代码行 25 到 27。
重新编译你的程序。
这启用了 VC-2010、VC-2013 中众所周知的旧 thunking 机制......并且这对我来说不会崩溃。只要不涉及其他已编译的库,它们可能通过 ATL 以任何方式子类化或使用 256 个窗口。
评论(2017 年 3 月 1 日):
Microsoft 确认这是一个错误。它应该在 Windows 10 RS2 中得到修复。 Mircrosoft 同意编辑 atlstdthunk.h 中的标头是解决该问题的一种解决方法。事实上这就是说。只要没有稳定的补丁,我就再也不能使用正常的 ATL thunking,因为我永远不知道世界上哪些 Window 版本会使用我的程序。因为 Windows 8 和 Windows 8.1 以及 RS2 之前的 Windows 10 会受到此错误的影响。
最终评论(2017 年 3 月 9 日):
使用 VS-2017 的构建也会受到影响,VS-2015 和 VS-2017 之间没有区别 对于这种情况,Microsoft 决定不会对旧操作系统进行修复。 Windows 8.1、Windows Server 2012 RC2 或其他 Windows 10 版本都不会获得修复此问题的补丁。 该问题很少见,对我们公司的影响很小。我们这边的解决方法也很简单。此错误的其他报告未知。 案件已结案。我对所有程序员的建议:更改 Visual Studio 版本 VS-2015、VS-2017 中的 atlstdthunk.h(见上文)。我没有了解微软。此错误是 ATL thunking 中的一个严重问题。它可能会影响每个使用更多窗口和/或子类的程序员。
我们只知道 Windows 10 RS2 中的修复程序。所以所有旧操作系统都会受到影响!所以我建议通过注释掉上面提到的定义来禁用 atlthunk.dll 的使用。
【问题讨论】:
您从未提及引发了哪个 SEH 异常。哪一个?此外,您在从未初始化 COM 的线程上调用ShellExecute
。这也不完全是谨慎的。
一个潜在的问题,您正在破坏窗口 (::DestroyWindow
) - 这会将消息发布到窗口 - 然后让您的 subclass
数组立即超出范围。这将意味着窗口销毁消息将无处可处理。此外,如果有任何待处理的消息,它们也会有同样的问题。
@RichardCritten:两者都不是潜在问题。 DestroyWindow
是严格序列化的。当它返回时,所有消息都已发送(未发布)并已处理。如果确实有待处理的消息,DispatchMessage
将无法找到目标窗口,什么都不会发生。
@RichardCritten:在正常情况下,崩溃与破坏阶段无关。崩溃发生在 SendWindow 行的循环中。销毁子类窗口也是完全安全的。这适用于 MFC 和 ATL 子类化。同样在我的情况下,任何消息队列中都没有消息......正如你所看到的,我什至根本没有消息循环。
@Mgetz:你说的传统 thunking 是什么意思?我只是使用 ATL 子类化。其余的由 ATL 完成。包括。它想要子类化的方式,这不是旧方式。
【参考方案1】:
这是 atlthunk.dll 中的错误。当它第二次和进一步加载自身时,这会通过 MapViewOfFile 调用手动发生。在这种情况下,并不是每个与模块基址相关的地址都被正确更改(当 LoadLibarary/LoadLibraryEx 加载的 DLL 调用系统加载程序时会自动执行此操作)。然后,如果 第一次 DLL 在 首选基地址 上加载,则一切正常,因为未更改的地址指向相似的代码或数据。但如果不是,当第 257 个子类窗口处理消息时,您会崩溃。
自 Vista 以来,我们具有“地址空间布局随机化”功能,这解释了您的代码随机崩溃的原因。每次你必须在你的操作系统上发现 atlthunk.dll 基地址时都会崩溃(它在不同的操作系统版本上有所不同)并在这个地址使用 VirtualAlloc 调用在第一个子类之前在这个地址上做一个内存页面地址空间预留.要查找基地址,您可以使用dumpbin /headers atlthunk.dll
命令或手动解析 PE 标头。
我的测试显示在 Windows 10 build 14393.693 x32 版本受到影响,但 x64 不受影响。在具有最新更新的 Server 2012R2 上,两个(x32 和 x64)版本都会受到影响。
顺便说一句,atlhunk.dll 代码的每个 thunk 调用的 CPU 指令大约是之前实现的 10 倍。它可能不是很重要,但会减慢消息处理速度。
【讨论】:
所以你可以重现这个问题?我只在 Windows 10 x64 机器上发生崩溃。还是你说的是 DLL 版本? 是的,我的意思是 x64 操作系统上的 x64/x32 DLL。 是的,我稍微修改了您的代码,以便在第一个子类之前在首选 DLL 位置保留内存。这提供了 100% 的崩溃重现性。 最后 MS 确认了这个错误。我对问题添加了评论。 微软关闭了案例。不会有任何修复。请参阅我的建议和问题底部的最终评论。【参考方案2】:已经描述的稍微自动化的形式:
// 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();
【讨论】:
请注意,尽管较早发布,但该错误“仅”影响 Windows 8.1 上的 32 位代码。不幸的是,Windows 8.1 在 2018 年退出了主流支持,因此很难修复该错误(是的,该错误是在 2017 年报告的)。以上是关于使用 ATL 子类化的 Windows 10 64 位随机崩溃的主要内容,如果未能解决你的问题,请参考以下文章
[WTL/ATL]_[初级]_[关于窗口子类析构时崩溃的原因]
[WTL/ATL]_[初级]_[关于窗口子类析构时崩溃的原因]