在哪种情况下动态CRT在调用用户提供的DllMain时尚未初始化?

时间:2018-01-17 13:21:54

标签: winapi visual-c++ msvcrt dllmain

序言:此问题与/MD使用的动态 CRT的行为有关,并且仅与此有关。它并不质疑任何其他建议的有效性。 DllMain

作为we've been told :(参考:动态链接库最佳实践,MSDN,2006年5月17日)

  

您永远不应该在DllMain中执行以下任务:

     
      
  • ...
  •   
  • 使用动态 C运行时(CRT)中的内存管理功能。如果未初始化CRT DLL,则对这些函数的调用可能导致进程崩溃。
  •   
  • ...
  •   

其他have questioned this已经(如:对论证的有效性提出质疑)并且由于我们在那里得到了答案,我们可以清楚地看到一个相当简单的案例,其中{{3>}可以{{} 3}}:

  

您的假设是DLL的入口点始终是_DllMainCRTStartup。情况并非如此,它只是链接器的默认值。它可以是程序员想要的任何东西,使用链接器的/ ENTRYPOINT选项可以快速轻松地更改。微软没有办法阻止这种情况发生。

所以这些是这个问题的要素:

  • 在链接potentially cause troubles而非提供自定义/ENTRYPOINT时,是否存在任何其他情况,其中动态 CRT应该不能完全初始化?

    • 具体来说,如果所有DLL加载仅通过“静态依赖”完成,即根本没有显式LoadLibrary调用,则只链接时间DLL依赖项。
  • 奖励:MS文档专门调出“内存管理功能”,但据我所知,如果CRT未初始化,可能任何 CRT功能应该是不安全的。为什么要以这种方式调用内存管理功能?

  • 3号:

    WRT。自定义ENTRYPOINT:我不太清楚这是一个如此重要的场景,它需要被包含在不做DllMain列表中而无需进一步限定。 IFF 我提供了一个自定义入口点,我负责正确初始化CRT,或者 CRT在我的程序的任何地方都无法正常工作,而不仅仅是DllMain。为什么要专门调用DllMain部分?

    这引导我回到Q.1,即如果这是动态 CRT的唯一问题。澄清或大开眼界为什么这对于DllMain来说更重要的是,对于DLL的其他部分,或者我可能会错过的内容,我们将不胜感激。

奖金链接:

基本原理:我觉得我应该为上下文添加它:我问这个是因为我们有大量的数量的代码通过全局C ++对象构造函数来做事。实际上破坏的事情多年来一直经过审查(如并发LoadLibrary,线程同步等),但所有代码都充满了std C ++和CRT函数,这些函数很乐意为在Windows XP,7和Windows 10上没有任何已知的打嗝。虽然我一个人哭但“它只是有效”,但我必须在这里做一个工程判断,试图“修复”这个是否有任何短到中值。因此,如果肥皂盒的答案可以留在他们的盒子里,我将不胜感激。

1 个答案:

答案 0 :(得分:1)

  

链接/MD和不提供时,是否还有其他情况?   自定义/ENTRYPOINT,其中动态CRT不应该完全   初始化?

首先是一些符号:

  • X 有静态导入(取决于) Y Z X[ Y, Z]
  • X 入口点:X_DllMain
  • X_DllMain致电LoadLibrary(Y)X<Y>

当我们使用/MD时 - 我们在单独的DLL中使用crt。在此上下文中初始化意味着已调用的crt DLL的入口点。所以问题可以更加笼统和明确:

来自X[Y]之前调用的Y_DllMain =&gt; X_DllMain

一般情况下没有。因为Y[X]Y[Z[X]]可以是循环依赖。

最知名的示例user32[gdi32]gdi32[user32]或win10取决于gdi32[gdi32full[user32]]。那么必须首先调用user32_DllMaingdi32_DllMain?不过很明显,任何crt DLL都不依赖于我们的自定义DLL。所以让我们排除循环依赖案例。

当加载器加载模块 X 时 - 它加载所有它的依赖模块(和它依赖 - 这是递归过程),如果它已经不在内存中,那么加载器构建调用图,并开始调用模块切入点。显而易见A[B],加载器总是在B_DllMain之前尝试调用A_DllMain(当调用顺序未定义时,循环依赖除外)。但哪些模块将在调用图中?所有 X 依赖模块?当然不。当我们开始加载 X 时,其中一些模块已经在内存中(已加载)。所以它已被调用的入口点,DLL_PROCESS_ATTACH,现在不能第二次调用。这个策略用于xp,vista,win7:

当我们加载 X 时:

  1. 在内存中加载或定位所有依赖模块
  2. 仅限加载(仅限 X )模块的入口点。
  3. 如果A[B] - 请在B_DllMain
  4. 之前致电A_DllMain

    示例:已加载X[Y[W[Z]], Z]

    //++begin load X
    Z_DllMain
    W_DllMain
    Y_DllMain
    X_DllMain
    // --end load X
    

    但是这种情况下没有考虑下一种情况 - 某些模块可能已经在内存中,但它的入口点尚未被调用。怎么会发生这种情况? 如果某些模块入口点调用LoadLibrary,则会发生这种情况。

    示例 - 已加载X[Y<W[ Z]>, Z]

    //++begin load X
    Y_DllMain
      //++begin load W
      W_DllMain
      //--end load W
    Z_DllMain
    X_DllMain
    // --end load X
    

    所以W_DllMain会在Z_DllMain之前被调用,尽管W[Z]。完全是因为这不建议从DLL入口点调用LoadLibrary

    但来自动态链接库最佳实践

      

    这可能会导致死锁或崩溃。

    关于死锁的说法不正确 - 当然任何死锁都不能基本上解决。哪里?怎么样 ?我们已经在DLL入口点内部保存了加载程序锁,并且可以递归地获取此锁。崩溃真的可以(在win8之前)。

    或其他 false

      

    致电ExitThread。在DLL分离期间退出线程可能会导致   再次获取装载机锁,导致死锁或崩溃。

    • 可以导致再次获取加载程序锁定 - 不是可以,而是始终
    • 导致死锁 - false - 我们已经拥有此锁
    • 崩溃 - 没有任何崩溃,否则一个 false

    但实际上它将是 - 没有自由加载器锁的线程退出。它永远变得忙碌。因此,当尝试获取加载程序锁定时,任何新线程创建或退出,任何新的DLL加载或卸载,或只是ExitProcess调用挂起。所以这里的僵局确实会发生,但不会在致电ExitThread - 后期。

    当然有趣的注意事项 - 窗口本身从LoadLibrary调用DllMain - user32.dll 总是为{em> imm32.dll <调用LoadLibrary / em>从它的入口点(仍然是真的,在win10上)

    但是从win8(或win8.1)开始,加载器在句柄依赖模块上变得更加智能。现在 2 已更改

    2。调用加载(在X之后)模块的入口点,或者模块是否尚未初始化。

    所以在现代窗口(8+)中加载X[Y<W[Z]>, Z]

    //++begin load X
    Y_DllMain
      //++begin load W
      Z_DllMain
      W_DllMain
      //--end load W
    X_DllMain
    // -- end load X
    

    Z 初始化将移至 W 加载调用图。结果一切都会正确的。

    为了测试我们可以构建下一个解决方案:test.exe[ kernel32, D1< D2[kernel32, msvcrt] >, msvcrt ]

    • D2 仅从 kernel32 msvcrt 导入并导出SomeFunc
    • D1 仅从 kernel32 导入并从其入口点调用LoadLibraryW(L"D2"),然后致电D2.SomeFunc
    • test.exe kernel32 D1 msvcrt
    • 导入

    (完全按此顺序排列!这一点至关重要 - D1 必须 msvcrt 导入,此需要设置 D1 msvcrt 之前的链接器命令行中)

    结果在 msvcrt 之前调用 D1 入口点。这是正常的 - D1 不依赖于 msvcrt 但当 D1 从入口点加载 D2 时,变得有趣

    D2.dll /NODEFAULTLIB kernel32.lib msvcrt.lib

    的代码
    #include <Windows.h>
    
    extern "C"
    {
        __declspec(dllimport) int __cdecl sprintf(PSTR buf, PCSTR format, ...);
    }
    
    BOOLEAN WINAPI MyEp( HMODULE , DWORD ul_reason_for_call, PVOID )
    {
        if (ul_reason_for_call == DLL_PROCESS_ATTACH)
        {
            OutputDebugStringA("D2.DllMain\n");
        }
    
        return TRUE;
    }
    
    INT_PTR WINAPI SomeFunc()
    {
        __pragma(message(__FUNCDNAME__))
        char buf[32];
        // this is only for link to msvcrt.dll
        sprintf(buf, "D2.SomeFunc\n");
        OutputDebugStringA(buf);
        return 0;
    }
    
    #ifdef _WIN64
    #define FuncName "?SomeFunc@@YA_JXZ"
    #else
    #define FuncName "?SomeFunc@@YGHXZ"
    #endif
    
    __pragma(comment(linker, "/export:" FuncName ",@1,NONAME,PRIVATE"))
    

    D1.dll /NODEFAULTLIB kernel32.lib

    的代码
    #include <Windows.h>
    
    #pragma warning(disable : 4706)
    
    BOOLEAN WINAPI MyEp( HMODULE hmod, DWORD ul_reason_for_call, PVOID )
    {
        if (ul_reason_for_call == DLL_PROCESS_ATTACH)
        {
            OutputDebugStringA("D1.DllMain\n");
            if (hmod = LoadLibraryW(L"D2"))
            {
                if (FARPROC fp = GetProcAddress(hmod, (PCSTR)1))
                {
                    fp();
                }
            }
        }
    
        return TRUE;
    }
    
    INT_PTR WINAPI SomeFunc()
    {
        __pragma(message(__FUNCDNAME__))
        OutputDebugStringA("D1.SomeFunc\n");
        return 0;
    }
    
    #ifdef _WIN64
    #define FuncName "?SomeFunc@@YA_JXZ"
    #else
    #define FuncName "?SomeFunc@@YGHXZ"
    #endif
    
    __pragma(comment(linker, "/export:" FuncName ",@1,NONAME"))
    

    exe /NODEFAULTLIB kernel32.lib D1.lib msvcrt.lib

    的代码
    #include <Windows.h>
    
    extern "C"
    {
        __declspec(dllimport) int __cdecl sprintf(PSTR buf, PCSTR format, ...);
    }
    
    __declspec(dllimport) INT_PTR WINAPI SomeFunc();
    
    void ep()
    {
        char buf[32];
        // this is only for link to msvcrt.dll
        sprintf(buf, "exe entry\n");
        OutputDebugStringA(buf);
        ExitProcess((UINT)SomeFunc());
    }
    

    输出xp:

    LDR: D1.dll loaded - Calling init routine
    D1.DllMain
    Load: D2.dll
    LDR: D2.dll loaded - Calling init routine
    D2.DllMain
    D2.SomeFunc
    LDR: msvcrt.dll loaded - Calling init routine
    exe entry
    D1.SomeFunc
    

    for win7:

    LdrpRunInitializeRoutines - INFO: Calling init routine for DLL "D1.dll"
    D1.DllMain
    Load: D2.dll
    LdrpRunInitializeRoutines - INFO: Calling init routine for DLL "D2.DLL"
    D2.DllMain
    D2.SomeFunc
    LdrpRunInitializeRoutines - "msvcrt.dll"
    exe entry
    D1.SomeFunc
    

    在两种情况下,呼叫流程相同 - D2.DllMain msvcrt 入口点之前称为,尽管D2[msvcrt]

    但是在win8.1和win10上 - 呼叫流程是另一个:

    LdrpInitializeNode - INFO: Calling init routine for DLL "D1.dll"
    D1.DllMain
    LdrpInitializeNode - INFO: Calling init routine for DLL "msvcrt.dll"
    LdrpInitializeNode - INFO: Calling init routine for DLL "D2.DLL"
    D2.DllMain
    D2.SomeFunc
    exe entry
    D1.SomeFunc
    

    D2 入口点 msvcrt 初始化后称为

    那么结论是什么?

    如果加载模块X[Y]并且内存中没有未初始化 Y - Y_DllMain将在 X_DllMain之前调用。换句话说 - 如果没有人从DLL入口点调用LoadLibrary(X)(或LoadLibrary(Z[X]))。所以,如果您的DLL将被加载&#34;正常&#34;方式(不是从LoadLibrary调用DllMain或在某些dll加载事件中从驱动程序注入) - 你可以确定已经调用了crt入口点(crt初始化)

    更多 - 如果你在win8.1 +上运行 - 而X[Y]已加载 - Y_DllMain将始终在 X_DllMain之前调用

    现在关于你的dll中的自定义/ENTRYPOINT

    即使您在单独的DLL中使用crt - 一些小的crt代码将静态链接到您的模块DllMainCRTStartup - 它按名称调用您的函数DllMain(这不是入口点)。所以在动态crt的情况下 - 我们确实有2个crt部分 - 主要部分在单独的DLL中,它将在之前初始化你的DLL入口点被调用(如果不是特殊情况,我描述更高和WIN7,远景,XP )。和小的静态部分(模块内的代码)。当这个静态部分被称为已经完全依赖你。这部分DllMainCRTStartup执行一些内部初始化,在代码中初始化全局对象(initterm)并调用DllMain,在它返回后(在dll分离时)调用析构函数为globals ..

    如果你在DLL中设置自定义入口点 - 此时crt在已经初始化的单独DLL中,但是你的静态crt没有(as和全局对象)。从此自定义入口点,您需要致电DllMainCRTStartup