/MD
在链接和不提供 custom时是否还有其他情况/ENTRYPOINT
,动态 CRT 不应该完全初始化?
首先是一些符号:
- X具有静态导入(取决于)Y和Z:
X[ Y, Z]
- X入口点:
X_DllMain
X_DllMain
致电LoadLibrary(Y)
:X<Y>
当我们使用/MD
- 我们在单独的 DLL 中使用 crt。在此上下文中初始化意味着 crt DLL(s) 的入口点已被调用。所以问题可以更笼统和清楚:
来自X[Y]
=> Y_DllMain
之前调用过X_DllMain
吗?
一般情况下没有。因为可以是循环依赖,whenY[X]
或Y[Z[X]]
.
最广为人知的例子user32[gdi32]
,和gdi32[user32]
或在win10中依赖gdi32[gdi32full[user32]]
。所以user32_DllMain
还是gdi32_DllMain
必须先调用?但是很明显,任何 crt DLL(s) 都不依赖于我们的自定义 DLL。所以让我们排除循环依赖情况。
当加载器加载模块X - 它加载所有它的依赖模块(和它的依赖 - 这是递归过程),如果它已经不在内存中,那么加载器构建调用图,并开始调用模块入口点。很明显,如果A[B]
,加载程序总是尝试调用B_DllMain
之前A_DllMain
(调用顺序未定义时的循环依赖除外)。但是哪些模块将在调用图中?所有X依赖模块?当然不。当我们开始加载X时,其中一些模块可能已经在内存中(已加载)。所以它的入口点已经被调用,DLL_PROCESS_ATTACH
现在不能第二次调用。这个策略在xp、vista、win7中使用:
当我们加载X时:
- 在内存中加载或定位所有它的依赖模块
- 仅调用新加载的(在X之后)模块的入口点。
- if
A[B]
-B_DllMain
之前调用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
soW_DllMain
之前会被调用Z_DllMain
,尽管如此W[Z]
。正是因为这不推荐LoadLibrary
从 DLL 入口点调用。
但来自动态链接库最佳实践
这可能会导致死锁或崩溃。
关于死锁的话不是真的——当然任何死锁都不能是基本的。在哪里 ?如何 ?我们已经在 DLL 入口点中持有加载器锁,并且可以递归地获取该锁。崩溃真的可以(win8之前)。
或另一个错误:
打电话ExitThread
。在 DLL 分离期间退出线程可能会导致再次获取加载程序锁,从而导致死锁或崩溃。
- 可能导致加载器锁再次被获取 -不能但总是
- 导致死锁 -错误- 我们已经持有这个锁
- 崩溃 - 不会发生任何崩溃,否则会发生错误
但这确实是-没有免费加载程序锁的线程退出。它变得永远忙碌。结果是任何新线程的创建或退出,任何新的 DLL 加载或卸载,或者只是ExitProcess
调用 - 挂起,当尝试获取加载程序锁时。所以这里真的会出现僵局,但不会在通话期间ExitThread
- 后者。
当然还有有趣的注意事项——windows 本身调用LoadLibrary
来自DllMain
——user32.dll总是从它的入口点调用imm32.dll (在 win10 上仍然如此)LoadLibrary
但从 win8(或 win8.1)开始,加载器在处理依赖模块方面变得更加智能。现在2变了
2.调用新加载的(在 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仅从kernel32和msvcrt导入 并导出
SomeFunc
- D1仅从kernel32导入并
LoadLibraryW(L"D2")
从它的入口点调用,然后调用D2.SomeFunc
- test.exe从kernel32,D1和msvcrt 导入
(完全按照这个顺序!这非常重要 -在导入时D1必须在 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
在这两种情况下,调用流程都是相同的 -在msvcrt入口点之前D2.DllMain
调用,尽管 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]
加载模块并且内存中没有未初始化的YY_DllMain
时 -将在之前 X_DllMain
调用。或者换句话说 - 如果没有人从 DLL 入口点调用LoadLibrary(X)
(或)。LoadLibrary(Z[X])
因此,如果您的 DLL 将以“正常”方式加载(而不是在某些 dll 加载事件中从驱动程序调用LoadLibrary
或DllMain
注入) - 您可以确定 crt 入口点已被调用(crt 已初始化)
more - 如果您在 win8.1+ 上运行 - 并且X[Y]
已加载 -Y_DllMain
将始终在之前 X_DllMain
调用。
现在关于/ENTRYPOINT
你的 dll 中的自定义。
即使您在单独的 DLL 中使用 crt - 一些小的 crt 代码将静态链接到您的模块DllMainCRTStartup
- 按名称调用您的函数DllMain
(这不是入口点)。所以在动态crt的情况下——我们真的有2个crt部分——主要部分在单独的DLL中,它将在你的DLL入口点被调用之前被初始化(如果不是我描述的更高和win7、vista、xp的特殊情况)。和小的静态部分(模块内的代码)。何时调用此静态部分已完全取决于您。这部分DllMainCRTStartup
做一些内部初始化,在你的代码中初始化全局initterm
对象DllMain
(
如果您在 DLL 中设置自定义入口点 - 此时在单独的 DLL 中的 crt 已经初始化,但您的静态 crt 没有(as 和全局对象)。从这个自定义入口点,您将需要调用DllMainCRTStartup