【发布时间】: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 完成。包括。它想要子类化的方式,这不是旧方式。
标签: c++ visual-studio-2015 windows-10 atl visual-studio-2017