Mar*_* Ba 5 winapi msvcrt visual-c++ dllmain
序言:这个问题特别关注动态 CRT 的行为,并且仅与之相关/MD
.它并不质疑任何其他建议的有效性.DllMain
.
当我们被告知:(参考:动态链接库的最佳实践,MSDN,2006年5月17日)
您永远不应该在DllMain中执行以下任务:
- ...
- 使用动态 C运行时(CRT)中的内存管理功能.如果未初始化CRT DLL,则对这些函数的调用可能导致进程崩溃.
- ...
其他人已经对此提出质疑(如:质疑论证的有效性),既然我们在那里得到了答案,我们可以清楚地看到一个相当简单的案例,这可能会引起麻烦:
您正在假设DLL的入口点始终是_DllMainCRTStartup.情况并非如此,它只是链接器的默认值.它可以是程序员想要的任何东西,使用链接器的/ ENTRYPOINT选项可以快速轻松地更改.微软没有办法阻止这一点.
所以这些是这个问题的要素:
链接和不提供自定义时是否还有其他情况,动态 CRT应该不能完全初始化?/MD
/ENTRYPOINT
LoadLibrary
调用,只需链接时间DLL依赖项.额外:MS文档专门调用"内存管理功能",但据我所知,如果CRT未初始化,可能任何 CRT功能都应该是不安全的.为什么以这种方式调用内存管理功能?
三:
WRT.对于习惯ENTRYPOINT
:我不太明白这是一个如此重要的场景,它需要被包含在不做DllMain列表中而无需进一步限定.IFF我提供了一个自定义入口点,我负责正确初始化CRT,或者CRT在我的程序中的任何地方都不能正常工作,而不仅仅是DllMain.为什么要专门调用DllMain部分?
这导致我回到Q.1,即如果这是动态 CRT 存在问题的唯一情况.澄清或大开眼界为什么这对于DllMain来说更重要的是,对于DLL的其他部分,或者我可能会错过这里,我们将不胜感激.
额外链接:
理由:我觉得我应该为上下文添加这个:我问这个是因为我们有大量的代码通过全局C++对象构造函数来做事.实际上破坏的事情多年来一直经过审查(如并发LoadLibrary
,线程同步等),但所有代码都充满了std
C++和CRT功能,这些功能已经在Windows XP,7和Windows 10上运行多年了任何已知的打嗝.虽然我不是一个哭泣"但它只是有效",我必须在这里做一个工程判断,试图"修复"这个是否有任何短期到中等价值.因此,如果肥皂盒的答案可以留在他们的盒子里,我将不胜感激.
/MD
在链接但不提供自定义时是否存在其他情况/ENTRYPOINT
,动态 CRT 不应该完全初始化?
首先一些符号:
X[ Y, Z]
X_DllMain
X_DllMain
称呼LoadLibrary(Y)
:X<Y>
当我们使用/MD
- 时,我们在单独的 DLL 中使用 crt。在此上下文中初始化意味着已经调用了 crt DLL 的入口点。所以问题可以更笼统和清晰:
来自X[Y]
=> Y_DllMain
之前调用过X_DllMain
吗?
一般情况下没有。因为可以是循环依赖,当Y[X]
或Y[Z[X]]
时。
最著名的例子user32[gdi32]
,和gdi32[user32]
或在win10取决于gdi32[gdi32full[user32]]
。如此user32_DllMain
或gdi32_DllMain
必须先调用?但很明显,任何 crt DLL 都不依赖于我们的自定义 DLL。所以让我们排除循环依赖的情况。
当加载程序加载模块X - 它加载所有依赖模块(及其依赖项 - 这是递归过程),如果它已经不在内存中,则加载程序构建调用图,并开始调用模块入口点。显然,如果加载程序总是在之前A[B]
尝试调用(除了调用顺序未定义时的循环依赖)。但哪些模块将出现在调用图中?所有X依赖模块?当然不。当我们开始加载X时,其中一些模块可能已经在内存中(已加载)。所以它的入口点已经被调用,并且现在不能被调用第二次。此策略在xp、vista、win7中使用:B_DllMain
A_DllMain
DLL_PROCESS_ATTACH
当我们加载X时:
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
Run Code Online (Sandbox Code Playgroud)
但这种情况没有考虑下一种情况 - 某些模块可能已经在内存中,但它的入口点尚未被调用。怎么会发生这种事?如果某些模块入口点调用,则可能会发生这种情况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
Run Code Online (Sandbox Code Playgroud)
所以W_DllMain
才会被称为之前Z_DllMain
,尽管W[Z]
。正是因为不建议LoadLibrary
从 DLL 入口点调用。
但来自动态链接库最佳实践
这可能会导致死锁或崩溃。
关于死锁的说法并不正确——当然任何死锁基本上都不可能发生。在哪里 ?如何 ?我们已经在 DLL 入口点内持有加载器锁,并且可以递归地获取该锁。死机确实可以(win8之前)。
或另一个错误:
称呼
ExitThread
。在 DLL 分离期间退出线程可能会导致再次获取加载程序锁,从而导致死锁或崩溃。
但实际上是 - 线程退出而没有释放加载器锁。它变得永远忙碌。因此,ExitProcess
当尝试获取加载程序锁时,任何新线程的创建或退出、任何新的 DLL 加载或卸载,或者只是调用挂起。所以这里确实会出现死锁,但在 Call 期间不会出现死锁ExitThread
- 后者。
当然,有趣的是 - Windows 本身调用LoadLibrary
- DllMain
user32.dll总是从它的入口点调用LoadLibrary
imm32.dll (在 win10 上仍然如此)
但从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
Run Code Online (Sandbox Code Playgroud)
Z初始化将移至W加载调用图。结果现在一切都会正确了。
为了测试这个,我们可以构建下一个解决方案:test.exe[ kernel32, D1< D2[kernel32, msvcrt] >, msvcrt ]
SomeFunc
LoadLibraryW(L"D2")
从其入口点调用,然后调用D2.SomeFunc
(完全按照这个顺序!这非常重要 - 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"))
Run Code Online (Sandbox Code Playgroud)
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"))
Run Code Online (Sandbox Code Playgroud)
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());
}
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
对于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
Run Code Online (Sandbox Code Playgroud)
在这两种情况下,调用流程是相同的 -在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
Run Code Online (Sandbox Code Playgroud)
msvcrt初始化后调用的D2入口点。
那么结论是什么?
如果X[Y]
加载模块时并且内存中没有未初始化的Y - 将在之前Y_DllMain
调用。或者换句话说 - 如果没有人从 DLL 入口点调用(或)。因此,如果您的 DLL 将以“正常”方式加载(不是通过在某些 dll 加载事件上从驱动程序调用或注入),您可以确定 crt 入口点已被调用(crt 已初始化) X_DllMain
LoadLibrary(X)
LoadLibrary(Z[X])
LoadLibrary
DllMain
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 中设置自定义入口点 - 此时单独的 DLL 中的 crt 已经初始化,但您的静态 crt 没有(作为全局对象)。从这个自定义入口点您将需要调用DllMainCRTStartup
归档时间: |
|
查看次数: |
361 次 |
最近记录: |