如何处理动态加载的 DLL 可以被 Windows 卸载的事实,即使引用计数不为零?
How to deal with the fact that dynamically loaded DLL can be unloaded by Windows even if reference count is not zero?
文档说“当模块的引用计数达到零或进程终止时(无论引用计数如何),系统都会卸载模块。”
这导致了一个严重的问题,目前还不清楚如何解决它。现在关于这个问题。假设我们有一个可执行文件 E 明确依赖于动态加载 DLL 库 L2 的 DLL 库 L1 ] 通过 LoadLibrary。
当 E 完成并且进程开始终止时 L2 可以在 L1 之前卸载。如果 L1 有一个具有静态存储持续时间的对象,其中包含从 L2 获得的一些资源,这可能会有问题,因为这些资源将在L1 卸载时间,这意味着调用已卸载的 L2 将导致崩溃。
一种可能的解决方案(甚至是唯一的一种)是不销毁具有静态存储持续时间的对象。此解决方案的缺点是 L1 可能会 loaded/unloaded 动态多次,这会增加内存泄漏。这可以通过使用 DllMain 来缓解,我们可以在其中检查进程是否终止或库是否通过 FreeLibrary 卸载。
目前看来一切正常。但还有更多。假设我们希望 L1 是一个静态库,它与名为 L_SHIM 的 DLL 库链接。所以现在可执行文件 E 与 L_SHIM 链接并且整个 DllMain 技巧不再起作用。实际上,如果我们不允许修改 L_SHIM 库,似乎没有任何效果。
有人解决过这样的问题吗?如果您对可能的解决方案有任何想法,我将不胜感激。
When handling DLL_PROCESS_DETACH
, a DLL should free resources such
as heap memory only if the DLL is being unloaded dynamically (the
lpReserved
parameter is NULL). If the process is terminating (the lpvReserved
parameter is non-NULL), all threads in the
process except the current thread either have exited already or have
been explicitly terminated by a call to the ExitProcess
function,
which might leave some process resources such as heaps in an
inconsistent state. In this case, it is not safe for the DLL to clean
up the resources. Instead, the DLL should allow the operating system
to reclaim the memory.
所以你需要在 DllMain
:
中检查 lpvReserved
让你拥有全局变量:
bool g_is_terminating = false;
并在 DllMain
case DLL_PROCESS_DETACH: g_is_terminating = (reserved != nullptr);
具有 global/static 的对象的析构函数在 DllMain
之后调用。所以你可以签入析构函数 g_is_terminating
;
if (!g_is_terminating) { do_something(); }
另一种总是有用且非常简单的方法,使用 未记录 api
//This routine returns the status of process shutdown.
EXTERN_C
DECLSPEC_IMPORT
BOOLEAN
NTAPI
RtlDllShutdownInProgress();
this api 从 ntdll.dll 导出 - 所以你需要使用或 ntdll.lib 或ntdllp.lib(如果你不使用crt)
只需从析构函数中调用 RtlDllShutdownInProgress();
,如果 return 为真 - 不访问另一个 dll - 这意味着进程正在终止
if (!RtlDllShutdownInProgress()) { do_something(); }
在内部,在ntdll中存在全局变量
BOOLEAN LdrpShutdownInProgress = FALSE;
并且当 ExitProcess
调用时 - 比 LdrShutdownProcess
从 ntdll.dll
调用并在开始时设置 LdrpShutdownInProgress = TRUE;
RtlDllShutdownInProgress()
只是 LdrShutdownProcess
的 return 值
大约
“系统在进程终止时卸载模块(无论引用计数如何)。”
这是错误的。进程开始终止后,系统不会卸载任何 dll。即使此时直接调用 FreeLibrary/LdrUnloadDll
(在 LdrpShutdownInProgress 设置为 TRUE
之后)- dll 也不会被卸载。真的 - 这样做没有多大意义 - 因为很快所有进程都会被销毁。
..这意味着调用已卸载的 L2 将导致崩溃。
真的 L2 永远不会在调用 ExitProcess
后卸载 eb。 但是 DLL_PROCESS_DETACH
会在L2里面调用。这将始终是 before L1 DLL_PROCESS_DETACH
通知(此通知按 LIFO 顺序发送。所以如果 L1 加载 L2 - DLL_PROCESS_ATTACH
在 L1 之前调用 L2 - L1 在 InInitializationOrderLinks
列表中的 L2 之前。在进程退出时 - 此列表 (InInitializationOrderLinks
) 以相反的顺序处理。
so L2 尽管仍在内存中,但已经处于“未初始化”状态(在 DLL_PROCESS_DETACH
之后)在 [=68 中调用一些 api =]L2 会导致未定义的结果。它可能没有任何问题、崩溃或随机结果。这个更惨,比较稳定崩溃
文档说“当模块的引用计数达到零或进程终止时(无论引用计数如何),系统都会卸载模块。”
这导致了一个严重的问题,目前还不清楚如何解决它。现在关于这个问题。假设我们有一个可执行文件 E 明确依赖于动态加载 DLL 库 L2 的 DLL 库 L1 ] 通过 LoadLibrary。 当 E 完成并且进程开始终止时 L2 可以在 L1 之前卸载。如果 L1 有一个具有静态存储持续时间的对象,其中包含从 L2 获得的一些资源,这可能会有问题,因为这些资源将在L1 卸载时间,这意味着调用已卸载的 L2 将导致崩溃。
一种可能的解决方案(甚至是唯一的一种)是不销毁具有静态存储持续时间的对象。此解决方案的缺点是 L1 可能会 loaded/unloaded 动态多次,这会增加内存泄漏。这可以通过使用 DllMain 来缓解,我们可以在其中检查进程是否终止或库是否通过 FreeLibrary 卸载。
目前看来一切正常。但还有更多。假设我们希望 L1 是一个静态库,它与名为 L_SHIM 的 DLL 库链接。所以现在可执行文件 E 与 L_SHIM 链接并且整个 DllMain 技巧不再起作用。实际上,如果我们不允许修改 L_SHIM 库,似乎没有任何效果。
有人解决过这样的问题吗?如果您对可能的解决方案有任何想法,我将不胜感激。
When handling
DLL_PROCESS_DETACH
, a DLL should free resources such as heap memory only if the DLL is being unloaded dynamically (thelpReserved
parameter is NULL). If the process is terminating (thelpvReserved
parameter is non-NULL), all threads in the process except the current thread either have exited already or have been explicitly terminated by a call to theExitProcess
function, which might leave some process resources such as heaps in an inconsistent state. In this case, it is not safe for the DLL to clean up the resources. Instead, the DLL should allow the operating system to reclaim the memory.
所以你需要在 DllMain
:
lpvReserved
让你拥有全局变量:
bool g_is_terminating = false;
并在 DllMain
case DLL_PROCESS_DETACH: g_is_terminating = (reserved != nullptr);
具有 global/static 的对象的析构函数在 DllMain
之后调用。所以你可以签入析构函数 g_is_terminating
;
if (!g_is_terminating) { do_something(); }
另一种总是有用且非常简单的方法,使用 未记录 api
//This routine returns the status of process shutdown.
EXTERN_C
DECLSPEC_IMPORT
BOOLEAN
NTAPI
RtlDllShutdownInProgress();
this api 从 ntdll.dll 导出 - 所以你需要使用或 ntdll.lib 或ntdllp.lib(如果你不使用crt)
只需从析构函数中调用 RtlDllShutdownInProgress();
,如果 return 为真 - 不访问另一个 dll - 这意味着进程正在终止
if (!RtlDllShutdownInProgress()) { do_something(); }
在内部,在ntdll中存在全局变量
BOOLEAN LdrpShutdownInProgress = FALSE;
并且当 ExitProcess
调用时 - 比 LdrShutdownProcess
从 ntdll.dll
调用并在开始时设置 LdrpShutdownInProgress = TRUE;
RtlDllShutdownInProgress()
只是 LdrShutdownProcess
大约 “系统在进程终止时卸载模块(无论引用计数如何)。”
这是错误的。进程开始终止后,系统不会卸载任何 dll。即使此时直接调用 FreeLibrary/LdrUnloadDll
(在 LdrpShutdownInProgress 设置为 TRUE
之后)- dll 也不会被卸载。真的 - 这样做没有多大意义 - 因为很快所有进程都会被销毁。
..这意味着调用已卸载的 L2 将导致崩溃。
真的 L2 永远不会在调用 ExitProcess
后卸载 eb。 但是 DLL_PROCESS_DETACH
会在L2里面调用。这将始终是 before L1 DLL_PROCESS_DETACH
通知(此通知按 LIFO 顺序发送。所以如果 L1 加载 L2 - DLL_PROCESS_ATTACH
在 L1 之前调用 L2 - L1 在 InInitializationOrderLinks
列表中的 L2 之前。在进程退出时 - 此列表 (InInitializationOrderLinks
) 以相反的顺序处理。
so L2 尽管仍在内存中,但已经处于“未初始化”状态(在 DLL_PROCESS_DETACH
之后)在 [=68 中调用一些 api =]L2 会导致未定义的结果。它可能没有任何问题、崩溃或随机结果。这个更惨,比较稳定崩溃