共用方式為


動態連結程式庫最佳做法

**更新:**

  • 2006 年 5 月 17 日

重要 API

建立 DLL 會為開發人員提供一些挑戰。 DLL 沒有系統強制執行的版本設定。 當系統上存在多個 DLL 版本時,在缺少版本設定架構的情況下,覆寫的簡易性會建立相依性和 API 衝突。 開發環境中的複雜性、載入器實作和 DLL 相依性已建立負載順序和應用程式行為的脆弱性。 最後,許多應用程式都依賴 DLL,而且具有必須接受的複雜相依性集合,應用程式才能正常運作。 本檔提供 DLL 開發人員的指導方針,可協助建置更強固、可攜式和可延伸 DLL。

DllMain 內的 不當同步處理可能會導致應用程式死結或存取未初始化 DLL 中的資料或程式碼。 從 DllMain 呼叫特定函式會造成這類問題。

what happens when a library is loaded

一般最佳做法

在保留載入器鎖定時呼叫 DllMain 。 因此,會對 DllMain 呼叫的函式施加重大限制。 因此, DllMain 的設計目的是使用一小部分的 Microsoft® Windows® API 來執行最少的初始化工作。 您無法呼叫 DllMain 直接或間接嘗試取得載入器鎖定的任何函式。 否則,您將導入應用程式死結或當機的可能性。 DllMain 實作中的 錯誤可能會危及整個進程及其所有線程。

理想的 DllMain 只是空的存根。 不過,鑒於許多應用程式的複雜性,這通常太嚴格。 DllMain 的良好經驗 法則是盡可能延遲初始化。 延遲初始化會增加應用程式的健全性,因為載入器鎖定保留時不會執行此初始化。 此外,延遲初始化可讓您安全地使用更多 Windows API。

某些初始化工作無法延後。 例如,如果檔案格式不正確或包含垃圾,則相依于組態檔的 DLL 應該無法載入。 針對這種類型的初始化,DLL 應該嘗試動作並快速失敗,而不是藉由完成其他工作來浪費資源。

您不應該從 DllMain 執行下列工作:

  • 呼叫 LoadLibrary LoadLibraryEx (直接或間接)。 這可能會導致死結或當機。
  • 呼叫 GetStringTypeA GetStringTypeEx GetStringTypeW (直接或間接)。 這可能會導致死結或當機。
  • 與其他執行緒同步處理。 這可能會導致死結。
  • 取得正在等候取得載入器鎖定的程式碼所擁有的同步處理物件。 這可能會導致死結。
  • 使用 CoInitializeEx 初始化 COM 執行緒。 在某些情況下,此函式可以呼叫 LoadLibraryEx
  • 呼叫登錄函式。
  • 呼叫 CreateProcess 。 建立進程可以載入另一個 DLL。
  • 呼叫 ExitThread 。 在 DLL 卸離期間結束執行緒可能會導致載入器鎖定再次取得,導致死結或當機。
  • 呼叫 CreateThread 。 如果您未與其他執行緒同步處理,建立執行緒可能會有作用,但有風險。
  • 呼叫 ShGetFolderPathW 。 呼叫殼層/已知資料夾 API 可能會導致執行緒同步處理,因此可能會導致死結。
  • 建立具名管道或其他具名物件(僅限 Windows 2000)。 在 Windows 2000 中,終端機服務 DLL 會提供具名物件。 如果未初始化此 DLL,對 DLL 的呼叫可能會導致進程當機。
  • 使用動態 C 執行時間 (CRT) 中的記憶體管理功能。 如果未初始化 CRT DLL,對這些函式的呼叫可能會導致進程當機。
  • 在 User32.dll 或 Gdi32.dll 中呼叫函式。 某些函式會載入另一個 DLL,但可能無法初始化。
  • 使用 Managed 程式碼。

在 DllMain 執行下列工作是安全的:

  • 在編譯時期初始化靜態資料結構和成員。
  • 建立和初始化同步處理物件。
  • 配置記憶體並初始化動態資料結構(避免上述函式)。
  • 設定執行緒本機儲存體 (TLS)。
  • 開啟、讀取和寫入檔案。
  • 在 Kernel32.dll 中呼叫函式(但上述函式除外)。
  • 將全域指標設定為 Null,以推遲動態成員的初始化。 在 Microsoft Windows Vista™ 中,您可以使用一次性初始化函式來確保在多執行緒環境中只執行一次程式碼區塊。

鎖定順序反轉所造成的死結

當您實作使用多個同步處理物件的程式碼時,例如鎖定,請務必遵守鎖定順序。 一次取得多個鎖定時,您必須定義稱為鎖定階層或鎖定順序的明確優先順序。 例如,如果在程式碼中某處鎖定 B 之前取得鎖定 A,並在程式碼中其他地方鎖定 C 之前取得鎖定 B,則鎖定順序為 A、B、C,而且應該在整個程式碼中遵循此順序。 鎖定順序反轉會在未遵循鎖定順序時發生,例如,如果在鎖定 A 之前取得鎖定 B。鎖定順序反轉可能會導致難以偵錯的死結。 若要避免這類問題,所有線程都必須以相同順序取得鎖定。

請務必注意,載入器會呼叫 已取得載入器鎖定的 DllMain ,因此載入器鎖定應該在鎖定階層中具有最高的優先順序。 另請注意,程式碼只需要取得它所需的鎖定才能進行適當的同步處理;它不需要取得階層中定義的每個鎖定。 例如,如果程式碼區段只需要鎖定 A 和 C 才能進行適當的同步處理,則程式碼應該在取得鎖定 C 之前取得鎖定 A;程式碼不需要也取得鎖定 B。此外,DLL 程式碼無法明確取得載入器鎖定。 如果程式碼必須呼叫可間接取得載入器鎖定的 GetModuleFileName 之類的 API,而且程式碼也必須取得私人鎖定,則程式碼應該先呼叫 GetModuleFileName ,再取得鎖定 P,以確保遵守載入順序。

圖 2 是說明鎖定順序反轉的範例。 請考慮主執行緒包含 DllMain DLL。 程式庫載入器會取得載入器鎖定 L,然後呼叫 DllMain 。 主執行緒會建立同步處理物件 A、B 和 G,以序列化其資料結構的存取權,然後嘗試取得鎖定 G。已成功取得鎖定 G 的背景工作執行緒接著會呼叫 GetModuleHandle 等函式,嘗試取得載入器鎖定 L。因此,背景工作執行緒在 L 上遭到封鎖,且主要執行緒在 G 上遭到封鎖,導致死結。

deadlock caused by lock order inversion

若要防止鎖定順序反轉所造成的死結,所有線程都應該嘗試在定義的載入順序中隨時取得同步處理物件。

同步處理的最佳做法

請考慮建立背景工作執行緒作為初始化一部分的 DLL。 在 DLL 清除時,必須與所有背景工作執行緒同步處理,以確保資料結構處於一致狀態,然後終止背景工作執行緒。 目前,在多執行緒環境中完全解決完全同步處理和關閉 DLL 的問題並沒有任何直接的方法。 本節說明 DLL 關機期間執行緒同步處理目前的最佳做法。

進程結束期間 DllMain 中的 執行緒同步處理

  • 在進程結束時呼叫 DllMain ,所有進程的執行緒都已強制清除,而且位址空間有可能不一致。 在此情況下,不需要同步處理。 換句話說,理想的DLL_PROCESS_DETACH處理常式是空的。
  • Windows Vista 可確保核心資料結構(環境變數、目前目錄、進程堆積等)處於一致的狀態。 不過,其他資料結構可能會損毀,因此清除記憶體不安全。
  • 需要儲存的持續性狀態必須排清到永久儲存體。

DLL 卸載期間DLL_THREAD_DETACH DllMain 中的 執行緒同步處理

  • 卸載 DLL 時,不會擲回位址空間。 因此,DLL 預期會執行清除關機。 這包括執行緒同步處理、開啟控制碼、永續性狀態,以及配置的資源。
  • 執行緒同步處理很棘手,因為等候執行緒在 DllMain 結束可能會導致死結。 例如,DLL A 會保留載入器鎖定。 它會發出線程 T 結束的訊號,並等候線程結束。 線程 T 結束,載入器會嘗試取得載入器鎖定,以使用 DLL_THREAD_DETACH 呼叫 DLL A 的 DllMain 。 這會導致死結。 若要將死結的風險降到最低:
    • DLL A 在其 DllMain取得DLL_THREAD_DETACH訊息,並設定線程 T 的事件,併發出其結束訊號。
    • 線程 T 會完成其目前的工作、使自己處於一致狀態、發出 DLL A 信號,並無限等候。 請注意,一致性檢查例程應該遵循與 DllMain 相同的限制,以避免死結。
    • DLL A 會終止 T,知道它處於一致狀態。

如果 DLL 在建立所有線程之後卸除,但在開始執行之前,線程可能會當機。 如果 DLL 在其 DllMain建立線程做為其初始化的一部分,某些線程可能尚未完成初始化,而且其DLL_THREAD_ATTACH訊息仍在等候傳遞至 DLL。 在此情況下,如果 DLL 已卸除,則會開始終止線程。 不過,某些線程可能會封鎖在載入器鎖定後方。 當 DLL 未對應之後,其DLL_THREAD_ATTACH訊息就會進行處理,導致進程當機。

建議

以下是建議的指導方針:

  • 使用應用程式驗證器來攔截 DllMain最常見的錯誤。
  • 如果在 DllMain使用私人鎖定,請定義鎖定階層並一致地使用它。 載入器鎖定必須位於這個階層的底部。
  • 確認沒有任何呼叫相依於可能尚未完全載入的另一個 DLL。
  • 在編譯階段以靜態方式執行簡單初始化,而不是在 DllMain執行。
  • 延遲 DllMain任何可以等到稍後的呼叫。
  • 延遲可以等到稍後的初始化工作。 必須儘早偵測某些錯誤狀況,讓應用程式可以正常處理錯誤。 不過,此早期偵測與強固性遺失之間有取捨,這可能會導致它。 延遲初始化通常是最好的。