gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-12-02 09:34:02 53 4
gpt4 key购买 nike

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



作为we've been told :(参见:Dynamic-Link库最佳实践,MSDN,2006年5月17日)


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


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



其他人已经have questioned this(如:对参数的有效性提出质疑),并且由于我们在那里有帮助地得到了答案,因此我们可以清楚地看到一个相当简单的情况,其中可以potentially cause troubles


您是基于DLL的入口点始终为_DllMainCRTStartup的假设进行工作的。事实并非如此,这只是链接器的默认设置。它可以是程序员希望通过链接器的/ ENTRYPOINT选项快速,轻松地进行更改的任何内容。 Microsoft无法采取任何措施来防止这种情况。


因此,这些是此问题的要素:


链接/MD而不提供自定义/ENTRYPOINT时,是否还有其他情况需要动态初始化CRT?


具体来说,如果所有DLL加载仅通过“静态依赖项”完成,即根本没有任何显式的LoadLibrary调用,则只需链接时DLL依赖项即可。

奖励:MS文档专门称呼“内存管理功能”,但据我所知,如果未初始化CRT,则任何CRT功能都可能不安全。为什么要用这种方式调出内存管理功能?
第三名:

Wrt。到自定义ENTRYPOINT的方式:我不太清楚这怎么可能是一个如此重要的情况,以至于需要将其包含在not-do-in-DllMain列表中而无需进一步的限定。 IFF我提供了一个自定义的入口点,我负责正确初始化CRT,否则CRT不能在程序的任何地方正常工作,而不仅仅是DllMain。为什么要专门调出DllMain部分?

这使我回到Q.1,即如果这是唯一对动态CRT有问题的情况。对于这对于DllMain比DLL其他部分更重要的原因,或者我在这里可能会错过的内容,我们将不胜感激或大开眼界。




奖金链接:


When are global objects constructed and destructed by Visual C++?
DllMain : a horror story
Calling LoadLibrary from DllMain




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

最佳答案

链接/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_DllMainX_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时:


加载或定位所有依赖模块在内存中
仅调用新加载的模块(在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_DllMainZ_DllMain将在 W[Z]之前被调用。正是因为这不建议从DLL入口点调用 LoadLibrary



但摘自Dynamic-Link库最佳实践


这可能导致死锁或崩溃。


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

或另一个错误:


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



可能导致再次获取装载机锁-不能但总是
导致死锁-错误-我们已经持有此锁
崩溃-没有崩溃,否则为假


但实际上是-没有免费加载程序锁的线程退出。它永远变得忙碌。结果是在尝试获取加载程序锁时,任何新的线程创建或退出,任何新的DLL加载或卸载或只是 ExitProcess调用-挂起。因此确实会出现死锁,但不会在呼叫 ExitThread-后者期间发生。

当然,有趣的是-Windows本身从 LoadLibrary调用 DllMain-user32.dll始终从其入口点为imm32.dll调用 LoadLibrary(仍然为true,并且在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


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

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


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


(正好按此顺序!这是至关重要的,非常重要-D1必须在导入的msvcrt之前,为此需要在链接器命令行的msvcrt之前设置D1)

结果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,在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将以“正常”方式加载(而不是通过 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,vista,xp)。和小的静态部分(模块内部的代码)。该静态部分何时被称为已满取决于您。此部分 DllMainCRTStartup进行一些内部初始化,在代码( initterm)中初始化全局对象,然后调用 DllMain,然后返回(通过DLL分离)为全局变量调用析构函数。

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

关于winapi - 在哪种情况下,动态CRT在调用用户提供的DllMain时尚未初始化?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48302191/

53 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com