仅当动态卸载 DLL 时,DLL 才应释放堆内存?

Posted

技术标签:

【中文标题】仅当动态卸载 DLL 时,DLL 才应释放堆内存?【英文标题】:A DLL should free heap memory only if the DLL is unloaded dynamically? 【发布时间】:2019-05-18 00:49:49 【问题描述】:

问题目的:对MS docs of DllMain进行现实检查。

常识”是你不应该在 DllMain 中做太多事情,有些事情绝对不能做,一些 best practises。

我现在偶然发现了文档中的一个新宝石,这对我来说毫无意义:(emph. mine)

在处理DLL_PROCESS_DETACH 时,DLL 应该释放资源,例如 仅当动态卸载 DLL 时才堆内存( lpReserved 参数为 NULL)。如果进程正在终止( lpvReserved 参数为非NULL),进程中的所有线程除了 当前线程已经退出或已明确退出 通过调用ExitProcess 函数终止,这可能会离开 某些进程资源,例如处于不一致状态。在这个 在这种情况下,DLL 清理资源是不安全的。反而, DLL 应该允许操作系统回收内存。

由于全局 C++ 对象在 DllMain/DETACH 期间被清理,这意味着全局 C++ 对象不得释放任何动态内存,因为堆可能处于不一致状态。 / 当 DLL “静态链接”到可执行文件时。 / 当然不是我在那里看到的 - 各种(我们的和第三方的)库的全局 C++ 对象(如果有)在它们的析构函数中分配和解除分配就好了。 (除非其他排序错误,o.c.)

那么,这个警告针对的是什么具体的技术问题?

既然段落提到了线程终止,那么当某些线程没有被正确清理时会不会出现堆损坏问题?

【问题讨论】:

当他们接到太多的支持电话时,就会添加这种措辞。记录未记录参数的唯一原因。一个示例场景是程序员使用 SetUnhandledExceptionFilter(),做一些有用的事情来帮助诊断崩溃,然后用 ExitProcess 而不是 TerminateProcess。在测试时总是有效,但在现实世界中崩溃是由堆损坏引起的并且堆锁仍然被持有时,这很糟糕。 【参考方案1】:

ExitProcess API 通常执行以下操作:

进入加载程序锁定临界区 锁定主进程堆(由GetProcessHeap()返回)通过HeapLock(GetProcessHeap())(好的,当然通过RtlLockHeap)(这是避免死锁的非常重要的一步) 然后终止进程中的所有线程,当前线程除外(通过调用NtTerminateProcess(0, 0)) 然后调用LdrShutdownProcess - 在这个api 加载器内部,通过加载的模块列表并发送DLL_PROCESS_DETACHlpvReserved 非空值。 最后调用NtTerminateProcess(NtCurrentProcess(), ExitCode ) 终止进程。

这里的问题是线程在任意位置终止。例如,线程可以在任何堆中分配或释放内存,并在它终止时位于堆临界区内。结果,如果DLL_PROCESS_DETACH 期间的代码试图从同一个堆中释放一个块,它会在尝试进入该堆的临界区时死锁(当然,如果堆实现使用它)。

请注意,这不会影响主进程堆,因为我们为它调用HeapLock 之前终止所有线程(当前除外)。这样做的目的:我们在这个调用中等待,直到所有其他线程退出进程堆临界区,并且在我们获得临界区之后,没有其他线程可以进入它 - 因为主进程堆被锁定。

因此,当我们在锁定主堆后终止线程时 - 我们可以确定没有其他被杀死的线程位于主堆临界区或处于不一致状态的堆结构内。感谢RtlLockHeap 来电。但这仅与主进程堆有关。进程中的任何其他堆都不会被锁定。所以这些可能DLL_PROCESS_DETACH 期间处于不一致的状态,或者可以被已经终止的线程独占获取。

所以 - 在这里使用 HeapFree 表示 GetProcessHeap 或说 LocalFree 是安全的(但未记录)。

如果在进程终止期间调用DllMain,则将HeapFree 用于任何其他堆安全。

此外,如果您由多个线程使用另一个自定义数据结构 - 它可能处于不一致状态,因为另一个线程(可以使用它)在任意点终止。

所以这个注释是警告当 lpvReserved 参数是 non-NULL 时(意味着在进程终止期间调用 DllMain)你需要在清理资源时要特别小心。无论如何,当进程死亡时,操作系统将释放所有内部内存分配。

【讨论】:

太棒了!你写的东西很有意义。请问你是怎么知道ExitProcess函数是怎么运作的?这是否记录在某处? @MartinBa - 在调试器下很容易查看。但确定没有来自 ms 的任何正式文件【参考方案2】:

作为 RbMm 出色答案的附录,我将添加来自 ExitProcess 的引用,它比 DllMain 文档做得更好 - 在解释为什么堆操作(或任何操作,真的)可能会受到损害:

如果进程中的一个终止线程持有锁并且 加载的 DLL 之一中的 DLL 分离代码尝试获取相同的 锁,然后调用ExitProcess 会导致死锁。相反,如果 进程通过调用 TerminateProcess 终止,该 DLL 是 附加到的进程不会收到进程终止的通知。 因此,如果你不知道你的所有线程的状态 进程,最好调用TerminateProcess而不是ExitProcess。笔记 从应用程序的主函数返回会导致 致电ExitProcess

所以,这一切都归结为:如果您的应用程序有“失控”线程可能持有 任何 锁,(CRT)堆锁就是一个突出的例子,您在关机期间遇到了一个大问题,当您需要访问“失控”线程正在使用的相同结构(例如堆)时。

这只是表明您应该以受控方式关闭所有线程。

【讨论】:

以上是关于仅当动态卸载 DLL 时,DLL 才应释放堆内存?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中动态加载和使用 DLL

C#动态加载dll 时程序集的卸载问题

C#中动态加载和卸载DLL

AppDomain 详解二-C#中动态加载和卸载DLL

C#.Net 如何动态加载与卸载程序集(.dll或者.exe)

DLL 被卸载或进程终止时如何释放资源