【问题标题】:How to deal with the fact that dynamically loaded DLL can be unloaded by Windows even if reference count is not zero?即使引用计数不为零,如何处理动态加载的DLL也可以被Windows卸载?
【发布时间】:2021-08-31 22:31:38
【问题描述】:

文档说“当模块的引用计数达到零或进程终止时(无论引用计数如何),系统都会卸载模块。”

这会导致严重的问题,目前尚不清楚如何解决。现在关于问题。假设我们有一个可执行文件E,它显式依赖于一个DLL 库L1,它通过LoadLibrary 动态加载一个DLL 库L2。 当 E 完成并且进程开始终止时,L2 可以在 L1 之前卸载。如果 L1 有一个具有静态存储持续时间的对象,其中包含从 L2 获得的一些资源,这可能会出现问题,因为这些资源将在 L1 开始销毁em> 卸载时间,这意味着调用已经卸载的 L2 将导致崩溃。

可能的解决方案之一(甚至是唯一的解决方案)是不破坏具有静态存储持续时间的对象。此解决方案的缺点是 L1 可能会被动态加载/卸载多次,这会增加内存泄漏。这可以通过使用 DllMain 来缓解,在这里我们可以检查进程是否终止或库是否通过 FreeLibrary 卸载。

到目前为止,一切似乎都运行良好。但还有更多。假设我们希望 L1 是一个静态库,它与名为 L_SHIM 的 DLL 库链接。所以现在可执行 EL_SHIM 链接,整个 DllMain 技巧不再起作用。实际上,如果我们不允许修改 L_SHIM 库,似乎没有任何效果。

有没有人必须解决这样的问题?如果您对可能的解决方案有任何想法,我将不胜感激。

【问题讨论】:

    标签: c++ windows dll


    【解决方案1】:

    在处理DLL_PROCESS_DETACH 时,DLL 应该释放资源,例如 仅当动态卸载 DLL( lpReserved 参数为 NULL)。如果进程正在终止(lpvReserved 参数为 non-NULL),则进程中的所有线程 当前线程以外的进程要么已经退出,要么已经退出 通过调用ExitProcess 函数显式终止, 这可能会留下一些进程资源,例如堆 不一致的状态。在这种情况下,DLL 清理是不安全的 上资源。相反,DLL 应该允许操作系统 回收内存。

    所以你需要检查 lpvReserved 里面的 DllMain:

    让你拥有全局变量:

    bool g_is_terminating = false;
    

    DllMain

    case DLL_PROCESS_DETACH: g_is_terminating = (reserved != nullptr);
    

    DllMain 之后调用的具有全局/静态对象的析构函数。所以你可以签入析构函数g_is_terminating;

    if (!g_is_terminating) { do_something(); }
    

    另一种总是有用且非常简单的方法,使用 undocumented api

    //This routine returns the status of process shutdown.
    
    EXTERN_C
    DECLSPEC_IMPORT
    BOOLEAN
    NTAPI
    RtlDllShutdownInProgress();
    

    ntdll.dll 导出的这个 api - 所以你需要使用或 ntdll.libntdllp.lib (以防你不使用crt)

    只需从析构函数调用RtlDllShutdownInProgress();,如果返回true - 不访问另一个dll - 这意味着进程正在终止

    if (!RtlDllShutdownInProgress()) { do_something(); }
    

    在内部,在ntdll中存在全局变量

    BOOLEAN LdrpShutdownInProgress = FALSE;
    

    ExitProcess 被调用时 - 比LdrShutdownProcessntdll.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 em> - DLL_PROCESS_ATTACHL2 之前在 L1 中调用 - L1InInitializationOrderLinks 列表中 L2 之前。并在进程退出时 - 此列表 (InInitializationOrderLinks) 以相反的顺序处理。

    所以 L2 ,尽管仍在内存中,但已经处于“uninit”状态(在 DLL_PROCESS_DETACH 之后)调用 L2 中的某些 api 可能会导致未定义的结果。它可能没有任何问题,或者崩溃或随机结果。这更糟糕,比较稳定的崩溃

    【讨论】:

    • OP 已经声明他不能使用任何依赖于DllMain() 的解决方案。
    • @user207421 - 抱歉,我一开始没有注意到 L1 是一个与 DLL 链接的静态库 - 添加另一个解决方案,使用 RtlDllShutdownInProgress() 调用。它甚至更简单,但未记录 api(但是因为它非常简单 - 永远不会被更改或删除)
    • @RbMm,根据blog,动态加载的DLL不会添加到依赖列表中,因此它可以独立于其他依赖项卸载。
    • @Nobody - 与博客作者相比,我对这个主题的了解并不少。你尝试说什么?
    • 我明白了,谢谢你的详细解释。
    猜你喜欢
    • 1970-01-01
    • 2011-09-30
    • 1970-01-01
    • 2012-01-24
    • 1970-01-01
    • 2010-11-12
    • 2010-11-23
    • 2012-11-07
    • 1970-01-01
    相关资源
    最近更新 更多