【问题标题】:In which cases is the dynamic CRT not already initialized on call to user supplied DllMain?在哪些情况下,动态 CRT 在调用用户提供的 DllMain 时尚未初始化?
【发布时间】:2018-01-17 13:21:53
【问题描述】:

序言:这个问题特别关注并且仅关注通过/MD 使用的动态 CRT 的行为。它不质疑任何其他建议的有效性。 DllMain.


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

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

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

其他人have questioned this 已经(如:质疑该论点的有效性),并且由于我们在那里得到了有用的答案,我们可以清楚地看到一个相当简单的情况,这可以potentially cause troubles

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

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

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

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

  • 第 3 号:

    写。到自定义ENTRYPOINT:我不太明白这怎么会是一个如此重要的场景,以至于它需要被包含在 not-do-in-DllMain 列表中而无需进一步限定。 IFF 我提供了一个自定义入口点,我负责正确初始化 CRT,否则 CRT 在我的程序中的任何地方都无法正常工作,而不仅仅是 DllMain。为什么要专门调出 DllMain 部分?

    这让我回到 Q.1,即如果这是唯一对 dynamic CRT 有问题的场景。澄清或大开眼界,为什么这对 DllMain 比对 DLL 的其他部分更重要,或者我可能在这里错过的内容,将不胜感激。


奖励链接:


基本原理:我觉得我应该为上下文添加这个:我之所以这么问是因为我们有 大量 数量的代码通过全局 C++ 对象构造函数来处理事情。多年来,实际发生故障的事情已经被审查了(如并发LoadLibrary、线程同步等),但所有代码都充满了std C++ 和 CRT 函数,这些函数已经在 Windows XP 上运行了多年, 7 和 Windows 10 没有任何已知的问题。虽然我不是一个哭“但它只是工作”的人,但我必须在这里对尝试“解决”这个问题是否有任何短期到中等价值进行工程判断。因此,如果肥皂盒的答案可以留在他们的盒子里,我将不胜感激。

【问题讨论】:

  • 如果您的 dll 加载不是递归的(不在另一个 dll 加载调用流中),crt dll 将保证在您的 dll 入口点之前初始化(调用它的入口点)(这里绝对不相关的入口点是什么你的dll)。问题可能仅在您的 dll 递归加载的情况下出现:在加载 dll 过程中,有人从 dll 入口点为您的 dll 调用 LoadLibrary,或者您的 dll 是外部注入的。如果 crt dll 已经加载,但此时尚未初始化,则取决于 os 版本(win8.1 之前)实际上可以是您在 crt ep 之前调用的 ep
  • 我在这里同意@RbMm。我试图在心理上构建一个场景,其中在初始化依赖于它的 dll 时未调用“msvcrt.dll”DllMain。当一个 dll 依赖于另一个 dll 时,排除循环依赖问题,另一个 dll 总是完全初始化(通过 dll main)。这当然假设 dll CRT 在其 DllMain 中对其自身进行初始化。
  • @ChrisBecke - 在另一个 dll 入口点内调用 dll 的LoadLibrary 时确实存在这种情况(例如AppInit_DLLs,dll 通过 shimengine 用作 shim dll,当另一个 dll 映射时从驱动程序注入..) - 在这种情况下,msvcrt.dll(例如)可能已经在进程中加载​​,但它的入口点尚未调用。此时 - 您的 dll,它依赖于 msvcrt.dll 被加载.. 然后不同的窗口 - 不同的处理这种情况。在 win7 和早期版本 - 在这种情况下,您的 dll 入口点将被调用 before msvcrt.dll
  • 但这只是 dll 特殊设计的递归加载的问题,通常在进程启动的早期阶段(如 AppInit_DLLs)。对于以通常方式(非递归)加载且不是为注入而设计的 dll - crt dlls 入口点保证将在我们的 dll 入口点之前调用,我们可以在自己的 dll 入口点中使用它
  • @MartinBa - 是的,如果您的 dll 没有通过 LoadLibrary (LdrLoadDll) 加载,而另一个加载 dll 处于活动状态(或初始化进程阶段,当 exe decencies 加载时) - 它(crt dll 尚未未初始化)这不会发生。在最新的 Windows 版本中,加载程序的实现也发生了变化——现在它根本不会在任何流程下发生。一个DllMain:一个恐怖故事已经无法复制

标签: winapi visual-c++ msvcrt dllmain


【解决方案1】:

在链接/MD 并且不提供 自定义/ENTRYPOINT,其中动态 CRT 不应该完全 初始化了吗?

首先是一些符号:

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

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

来自X[Y] => Y_DllMainX_DllMain 之前调用?

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

最知名的例子 user32[gdi32]gdi32[user32] 或在 win10 中取决于 gdi32[gdi32full[user32]] 。所以必须先调用user32_DllMaingdi32_DllMain 吗?但是很明显,任何 crt DLL(s) 都不依赖于我们的自定义 DLL。所以让我们排除循环依赖情况。

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

当我们加载X时:

  1. 在内存中加载或定位所有依赖模块
  2. 仅调用加载(在X之后)模块的入口点。
  3. 如果A[B] - 在A_DllMain 之前调用B_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


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

这可能导致死锁或崩溃。

关于deadlock not true的话——当然任何deadlock基本上都不可能。在哪里 ?如何 ?我们已经在 DLL 入口点中持有加载器锁,并且可以递归地获取该锁。崩溃真的可以(win8之前)。

或另一个错误

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

  • 可以导致再次获取加载程序锁 - 不是可以,而是总是
  • 导致死锁 - false - 我们已经持有这个锁
  • 崩溃 - 不会有任何崩溃,否则会出现 false

但实际上会是 - 没有空闲加载程序锁的线程退出。它变得永远忙碌。结果是任何新线程的创建或退出,任何新的 DLL 加载或卸载,或者只是 ExitProcess 调用 - 挂起,当尝试获取加载程序锁时。所以这里确实会出现僵局,但不会在 Call ExitThread 期间发生 - 后者。

当然还有有趣的注意事项 - Windows 本身从 DllMain 调用 LoadLibrary - user32.dll 总是从它的条目中为 imm32.dll 调用 LoadLibrary点(在win10上仍然正确)


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

2. 调用new 加载(X 之后)模块的入口点,或者如果模块尚未初始化。

所以在现代 Windows (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 仅从 kernel32msvcrt 导入并导出 SomeFunc
  • D1 仅从 kernel32 导入并从其入口点调用LoadLibraryW(L"D2"),然后调用D2.SomeFunc
  • test.exekernel32D1msvcrt 导入

(完全按照这个顺序!这非常重要 - D1 必须在导入时 before msvcrt,为此需要设置 D1 在链接器命令行中 msvcrt 之前)

结果 D1 入口点将在 msvcrt 之前调用。这是正常的 - 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

对于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 调用 before 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

msvcrt 初始化之后调用的 D2 入口点。

那么结论是什么?

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

更多 - 如果你在 win8.1+ 上运行 - 并且 X[Y] 已加载 - Y_DllMain 将始终被称为 before X_DllMain


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

即使您在单独的 DLL 中使用 crt - 一些小的 crt 代码将静态链接到您的模块 DllMainCRTStartup - 按名称调用您的函数 DllMain(这不是入口点)。因此,如果动态 crt - 我们确实有 2 个 crt 部分 - 主要部分位于单独的 DLL 中,它将在 调用您的 DLL 入口点之前被初始化(如果不是我描述的特殊情况,并且win7,vista,xp)。和小的静态部分(模块内的代码)。何时调用此静态部分已完全取决于您。这部分DllMainCRTStartup 进行一些内部初始化,在代码中初始化全局对象(initterm)并调用DllMain,在它返回(在 dll 分离时)调用全局变量的析构函数..

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

【讨论】:

    猜你喜欢
    • 2015-02-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-08
    相关资源
    最近更新 更多