為什么動態鏈接庫以"錯誤"的方式被卸載?
?當程序啟動或加載 DLL 時,加載器將生成該程序/DLL 引用的所有 DLL、該 DLL 的依賴項等的依賴項樹。然后,它確定初始化這些 DLL 的正確順序,以便在初始化它所依賴的所有 DLL 之前,不會初始化任何 DLL。(當然,如果你有一個循環依賴關系,那么它就會崩潰。眾所周知,從 DLL 的DLL_PROCESS_ATTACH 通知中調用加載庫函數或 LoadLibraryEx 函數也會弄亂這些依賴項的計算過程。)
同樣,當卸載 DLL 或程序終止時,將進行反初始化,以便在 DLL 的所有依賴項之后反初始化該 DLL。
但是,當你手動加載 DLL 時,關鍵信息會丟失:即調用 LoadLibrary 的 DLL 取決于正在被加載的 DLL。因此,如果 A.DLL 手動加載 B.DLL,則不能保證 A.DLL 將在 B.DLL 之前卸載。這意味著,例如,像下面這樣的代碼是不可靠的:
在標記為 “oops” 的行中,不能保證 B.DLL 仍在內存中,因為 B.DLL 不會出現在 A.DLL 的依賴項列表中,即使存在由對 LoadLibrary 的調用導致的運行時生成的動態依賴項。
為什么加載器無法跟蹤此動態依賴項?換句話說,當A.DLL調用LoadLibrary(“B.DLL”) 時,為什么加載器不能自動說“好吧,現在A.DLL依賴于B.DLL”?
首先,因為正如我在之前的文章中所指出的,你不能相信返回地址。
其次,即使你可以相信返回地址,你仍然不能相信返回地址。我們看看下面的代碼:
在這種情況下,B.DLL 的加載不是直接從A.DLL發生的,而是通過中間(在本例中為中間函數)發生的。即使你可以相信返回地址,依賴項也會分配給 MID.DLL 而不是A.DLL。
你可能會問,”什么樣的人會寫出像中函數這樣的函數呢?”。這種中間函數在幫助函數/包裝器函數很常見,或者為了提供額外的生存期管理功能(盡管它現在不再這樣做了,但是它曾經這樣做過)。
第三,有調用 GetModuleHandle 函數的情況。我們看看下面的代碼:
我們對 GetModuleHandle 的調用,是否應創建依賴項呢?
另請注意,DLL 之間存在的依賴關系不僅僅是對 LoadLibrary 的調用。例如,如果將回調函數指針傳遞給另一個 DLL,則就會創建一個反向依賴項。
最后要注意的是,這種隱式依賴關系,就像上面寫的那樣很難看到,一旦你把全局析構函數扔進去,情況就更糟了。例如下面的代碼:
DLL 依賴項現在隱藏在 SomethingHolder 類中,當 A.DLL 卸載時,g_SomethingHolder的析構函數將運行并嘗試與 B.DLL 通信。隨之而來的,是程序的不可預知的行為。
總結
盡量不對系統運行行為做出過多的假設,嚴格按照 MSDN 所說的,老老實實寫你的代碼,基本不會有大錯誤。