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

A DLL should free heap memory only if the DLL is unloaded dynamically?

问题目的:MS docs of DllMain.

的真实性检查

在 DllMain 中不应该做太多是 "common" 知识,有些事情绝对不能做,有些 best practises

我现在在文档中偶然发现了一个新的 gem,这对我来说意义不大:(emph。我的)

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/DETACH 期间清理了全局 C++ 对象,这意味着全局 C++ 对象不得释放任何动态内存,因为堆可能处于不一致状态。 / 当 DLL 为 "linked statically" 时可执行。 / 当然不是我在那里看到的 - 各种(我们的和第三方的)库的全局 C++ 对象(如果有的话)在它们的析构函数中分配和解除分配就很好。 (除了其他排序错误,o.c。)

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

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

ExitProcess API 通常会执行以下操作:

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

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

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

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

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

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

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

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

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

If one of the terminated threads in the process holds a lock and the DLL detach code in one of the loaded DLLs attempts to acquire the same lock, then calling ExitProcess results in a deadlock. In contrast, if a process terminates by calling TerminateProcess, the DLLs that the process is attached to are not notified of the process termination. Therefore, if you do not know the state of all threads in your process, it is better to call TerminateProcess than ExitProcess. Note that returning from the main function of an application results in a call to ExitProcess.

所以,这一切都归结为:IFF 你的应用程序有 "runaway" 线程可能持有 any 锁,(CRT)堆锁是一个突出的例子,你在关闭期间遇到了一个大问题,当你需要访问你的 "runaway" 线程正在使用的相同结构(例如堆)时。

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