使用ATL子类

时间:2017-01-19 12:08:39

标签: c++ visual-studio-2015 windows-10 atl visual-studio-2017

从一开始:自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"在消息框中。如果应用程序运行没有错误,应用程序显示"成功"在消息框中。 如果应用程序在没有参数的情况下启动,则只执行一次。

问题:

  • 还有其他人可以重复这个吗?
  • 有人看到类似的效果吗?
  • 有人知道或可以想象出这个原因吗?
  • 有人知道如何解决这个问题吗?

备注:

2017-01-20微软支持案例开启。

代码

// 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,将主要功能更改为

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调用并将VirtualAlloc调用更改为先前hMod值的地址,这是此窗口会话中的首选加载地址。

  4. 重新编译并运行。 CRASH!

  5. 感谢eugene。

    到目前为止。微软仍在调查此事。他们有转储和所有代码。但我没有最终答案。 事实上,我们在某些Windows 64bit操作系统中存在致命错误

    我目前进行了以下更改以解决此问题

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

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

    3. 重新编译您的程序。

    4. 这使得从VC-2010,VC-2013中众所周知的旧的thunking机制......这对我来说是免费的。只要没有其他已编译的库可以以任何方式通过ATL子类化或使用256个窗口。

      评论(2017年3月1日):

      • Microsoft确认这是一个错误。它应该在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之间没有区别
      • 对于这种情况,Microsoft决定不对旧操作系统进行修复。
      • Windows 8.1,Windows Server 2012 RC2或其他Windows 10版本都不会得到补丁来解决此问题。
      • 问题很罕见,对我们公司的影响很小。我们方面的修复也很简单。有关此错误的其他报告尚不清楚。
      • 案件已经结案。

      我对所有程序员的建议:更改Visual Studio版本VS-2015,VS-2017中的atlstdthunk.h(见上文)。 我不会&#39了解微软。这个bug是ATL thunking中的一个严重问题。它可能击中每个使用更多窗口和/或子类化的程序员。

      我们只知道Windows 10 RS2中的修复程序。所以旧的操作系统都受到影响!所以我建议通过注释掉上面提到的定义来禁用atlthunk.dll。

2 个答案:

答案 0 :(得分:4)

这是atlthunk.dll中的错误。当它自己加载第二次并且进一步时,这通过MapViewOfFile调用手动发生。在这种情况下,并非每个相对于模块库的地址都已正确更改(当LoadLibarary / LoadLibraryEx加载的DLL调用系统加载程序时会自动执行此操作)。然后,如果首选时间DLL加载到首选基地,一切正常,因为未更改的地址指向相似的代码或数据。但是,如果没有,当第257个子类窗口处理消息时,你就会崩溃。

由于Vista具有“地址空间布局随机化”功能,因此可以解释为什么您的代码会随机崩溃。每次你必须在你的操作系统上发现atlthunk.dll基地址(它在不同的操作系统版本上有所不同)时崩溃,并使用VirtualAlloc在第一个子类之前调用​​在此地址进行一次内存页地址空间预留 。要查找基址,可以使用dumpbin /headers atlthunk.dll命令或手动解析PE头。

我的测试显示在Windows 10上构建14393.693 x32版本受影响但x64不受影响。在具有最新更新的Server 2012R2上,(x32和x64)版本都会受到影响。

BTW,atlthunk.dll代码的每个thunk调用的CPU指令大约是之前实现的10倍。它可能不是很重要,但它会减慢消息处理速度。

答案 1 :(得分:0)

已经描述过的形式略自动:

// 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();
}