序言:此问题与/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应该不能完全初始化?
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上没有任何已知的打嗝。虽然我不一个人哭但“它只是有效”,但我必须在这里做一个工程判断,试图“修复”这个是否有任何短到中值。因此,如果肥皂盒的答案可以留在他们的盒子里,我将不胜感激。
答案 0 :(得分:1)
链接
/MD
和不提供时,是否还有其他情况? 自定义/ENTRYPOINT
,其中动态CRT不应该完全 初始化?
首先是一些符号:
X[ Y, Z]
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_DllMain
或gdi32_DllMain
?不过很明显,任何crt DLL都不依赖于我们的自定义DLL。所以让我们排除循环依赖案例。
当加载器加载模块 X 时 - 它加载所有它的依赖模块(和它依赖 - 这是递归过程),如果它已经不在内存中,那么加载器构建调用图,并开始调用模块切入点。显而易见A[B]
,加载器总是在B_DllMain
之前尝试调用A_DllMain
(当调用顺序未定义时,循环依赖除外)。但哪些模块将在调用图中?所有 X 依赖模块?当然不。当我们开始加载 X 时,其中一些模块已经在内存中(已加载)。所以它已被调用的入口点,DLL_PROCESS_ATTACH
,现在不能第二次调用。这个策略用于xp,vista,win7:
当我们加载 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
但是这种情况下没有考虑下一种情况 - 某些模块可能已经在内存中,但它的入口点尚未被调用。怎么会发生这种情况?
如果某些模块入口点调用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分离期间退出线程可能会导致 再次获取装载机锁,导致死锁或崩溃。
但实际上它将是 - 没有自由加载器锁的线程退出。它永远变得忙碌。因此,当尝试获取加载程序锁定时,任何新线程创建或退出,任何新的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 ]
SomeFunc
LoadLibraryW(L"D2")
,然后致电D2.SomeFunc
(完全按此顺序排列!这一点至关重要 - 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