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。

需求/我使用的场景:

应用程序的作用:

它只是创建一个框架 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,不可交换或再分发。

已检查的内容:

再现性:

这个问题在某种程度上是可以重现的。它不会一直崩溃,它会随机崩溃。我有一台机器,代码每执行三次就会崩溃。

我可以用 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;
}
  1. 取消注释 LoadLibrary 调用。编译.

  2. 运行 编程一次并在调试器中停止。记下加载库的地址 (hMod)。

  3. 停止程序。现在再次注释Library call,将VirtualAlloc call改成之前hMod值的地址,这是本window session.

    [=中的首选加载地址173=]
  4. 重新编译并运行。崩溃!

感谢尤金。

到目前为止。微软仍在对此进行调查。他们有转储和所有代码。但我没有最终的答案。 事实上我们在某些 Windows 64 位 OS.

中有一个致命错误

我目前做了以下更改来解决这个问题

  1. 打开VS-2015的atlstdthunk.h。

  2. 完全取消注释定义 USE_ATL_THUNK2 的 #ifdef 块。代码行 25 到 27.

  3. 重新编译你的程序。

这启用了从 VC-2010、VC-2013... 众所周知的旧 thunking 机制,这对我来说没有崩溃。只要不涉及其他已编译的库,它们可以通过 ATL 以任何方式子类化或使用 256 windows。

评论(2017 年 3 月 1 日):

其实是这样说的。只要没有稳定的补丁,我就再也不能使用正常的 ATL thunking,因为我永远不知道世界上有哪些 Window 版本会使用我的程序。因为 RS2 之前的 Windows 8 和 Windows 8.1 和 Windows 10 会受到此错误的影响。

最终评论(2017 年 3 月 9 日):

我对所有程序员的建议:在 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();
}