Share via


DLL 和 Visual C++ 執行階段程式庫行為

當您使用 Visual Studio 建置動態連結程式庫 (DLL) 時,連結器預設會包含 Visual C++ 執行時間程式庫 (VCRuntime)。 VCRuntime 包含初始化和終止 C/C++ 可執行檔所需的程式碼。 連結至 DLL 時,VCRuntime 程式碼會提供稱為 的內部 DLL 進入點函式 _DllMainCRTStartup ,此函式會處理 Windows OS 訊息至 DLL 以附加至進程或執行緒或執行緒。 函 _DllMainCRTStartup 式會執行基本工作,例如已設定堆疊緩衝區安全性、C 執行時間程式庫 (CRT) 初始化和終止,以及對靜態和全域物件的建構函式和解構函式的呼叫。 _DllMainCRTStartup 也會針對 WinRT、MFC 和 ATL 等其他程式庫呼叫攔截函式,以執行自己的初始化和終止。 如果沒有此初始化,CRT 和其他程式庫以及您的靜態變數都會處於未初始化的狀態。 不論 DLL 使用靜態連結的 CRT 還是動態連結的 CRT DLL,都會呼叫相同的 VCRuntime 內部初始化和終止常式。

預設 DLL 進入點_DllMainCRTStartup

在 Windows 中,所有 DLL 都可以包含選擇性的進入點函式,通常稱為 DllMain ,該函式會針對初始化和結束通話。 這可讓您視需要配置或釋放其他資源。 Windows 會在四種情況下呼叫進入點函式:進程附加、進程中斷連結、執行緒連結和執行緒卸離。 當 DLL 載入進程位址空間時,當使用 DLL 的應用程式載入時,或當應用程式在執行時間要求 DLL 時,作業系統會建立個別的 DLL 資料複本。 這稱為 進程附加 當載入 DLL 的進程建立新的執行緒時,就會發生執行緒附加 執行緒卸離 會線上程終止時發生,而 進程卸離 是不再需要 DLL 且由應用程式釋放時。 作業系統會針對每個事件分別呼叫 DLL 進入點,並 傳遞每個事件種類的 reason 引數。 例如,OS 會將 作為 原因 引數傳送 DLL_PROCESS_ATTACH 給訊號進程附加。

VCRuntime 程式庫提供稱為 _DllMainCRTStartup 的進入點函式,以處理預設初始化和終止作業。 在進程附加時,函 _DllMainCRTStartup 式會設定緩衝區安全性檢查、初始化 CRT 和其他程式庫、初始化執行時間類型資訊、初始化和呼叫靜態和非本機資料的建構函式、初始化執行緒本機儲存體、遞增每個附加的內部靜態計數器,然後呼叫使用者或程式庫提供的 DllMain 。 在進程中斷連結上,函式會反向執行這些步驟。 它會呼叫 DllMain 、遞減內部計數器、呼叫解構函式、呼叫 CRT 終止函式和已註冊 atexit 的函式,並通知任何其他終止程式庫。 當附件計數器移至零時,函式會傳回 FALSE ,以指示 Windows 可以卸載 DLL。 此 _DllMainCRTStartup 函式也會線上程附加和執行緒中斷連結期間呼叫。 在這些情況下,VCRuntime 程式碼不會自行進行額外的初始化或終止,而只會呼叫 DllMain 來傳遞訊息。 如果 DllMain 從進程附加傳回 FALSE 、發出訊號失敗、 _DllMainCRTStartup 再次呼叫 DllMain 並傳遞 DLL_PROCESS_DETACH reason 引數,則會經歷終止程式的其餘部分。

在 Visual Studio 中建置 DLL 時,VCRuntime 提供的預設進入點 _DllMainCRTStartup 會自動連結在 中。 您不需要使用 /ENTRY (進入點符號) 連結器選項來指定 DLL 的進入點函式。

注意

雖然您可以使用 /ENTRY: 連結器選項為 DLL 指定另一個進入點函式,但我們不建議這麼做,因為您的進入點函式必須以相同順序複製 _DllMainCRTStartup 所有作業。 VCRuntime 提供可讓您複製其行為的函式。 例如,您可以在進程附加時立即呼叫 __security_init_cookie,以支援 /GS (緩衝區安全性檢查) 緩衝區檢查選項。 您可以呼叫 _CRT_INIT 函式,傳遞與進入點函式相同的參數,以執行 DLL 初始化或終止函式的其餘部分。

初始化 DLL

您的 DLL 可能有在 DLL 載入時必須執行的初始化程式碼。 為了讓您執行自己的 DLL 初始化和終止函式, _DllMainCRTStartup 請呼叫稱為 的函式,以提供該函 DllMain 式。 您必須 DllMain 具有 DLL 進入點所需的簽章。 預設進入點函 _DllMainCRTStartup 式會使用 Windows 所傳遞的相同參數來呼叫 DllMain 。 根據預設,如果您未提供函 DllMain 式,Visual Studio 會為您提供函式,並將它連結在 中, _DllMainCRTStartup 讓一律有要呼叫的內容。 這表示,如果您不需要初始化 DLL,在建置 DLL 時就不需要執行任何特殊動作。

這是 用於 DllMain 的簽章:

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved); // reserved

某些程式庫會為您包裝函 DllMain 式。 例如,在一般 MFC DLL 中,實 CWinApp 作 物件的 InitInstanceExitInstance 成員函式,以執行 DLL 所需的初始化和終止。 如需詳細資訊,請參閱 初始化一般 MFC DLL 一節。

警告

您可以在 DLL 進入點安全地執行哪些動作有重大限制。 如需在 中 DllMain 呼叫不安全的特定 Windows API 詳細資訊,請參閱 一般最佳做法 。 如果您需要最簡單的初始化以外的任何專案,請在 DLL 的初始化函式中執行此動作。 您可以要求應用程式在執行 之後 DllMain 呼叫初始化函式,以及呼叫 DLL 中任何其他函式之前。

初始化一般 (非 MFC) DLL

若要在使用 VCRuntime 提供的 _DllMainCRTStartup 進入點的一般 (非 MFC) DLL 中執行您自己的初始化,您的 DLL 原始程式碼必須包含稱為 DllMain 的函式。 下列程式碼會呈現基本基本架構,其中顯示 的定義 DllMain 可能看起來像這樣:

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved)  // reserved
{
    // Perform actions based on the reason for calling.
    switch (reason)
    {
    case DLL_PROCESS_ATTACH:
        // Initialize once for each new process.
        // Return FALSE to fail DLL load.
        break;

    case DLL_THREAD_ATTACH:
        // Do thread-specific initialization.
        break;

    case DLL_THREAD_DETACH:
        // Do thread-specific cleanup.
        break;

    case DLL_PROCESS_DETACH:
        // Perform any necessary cleanup.
        break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

注意

舊版 Windows SDK 檔指出,必須在連結器命令列上指定 DLL 進入點函式的實際名稱與 /ENTRY 選項。 使用 Visual Studio 時,如果您的進入點函式名稱是 DllMain ,則不需要使用 /ENTRY 選項。 事實上,如果您使用 /ENTRY 選項並將進入點函式命名為 以外的 DllMain 專案點函式,除非進入點函式進行相同的初始化呼叫, _DllMainCRTStartup 否則 CRT 不會正確初始化。

初始化一般 MFC DLL

因為一般 MFC DLL 有 CWinApp 物件,所以它們應該在與 MFC 應用程式相同的位置執行其初始化和終止工作:在 InitInstance DLL 衍生類別的 CWinAppExitInstance 成員函式中。 由於 MFC 提供 DllMain 針對 DLL_PROCESS_ATTACHDLL_PROCESS_DETACH 呼叫 _DllMainCRTStartup 的函式,因此您不應該撰寫自己的 DllMain 函式。 載入 DLL 時,MFC 提供的 DllMain 函式會呼叫 InitInstance ,並在卸載 DLL 之前呼叫 ExitInstance 它。

一般 MFC DLL 可以藉由在其函式中 InitInstance 呼叫 TlsAlloc TlsGetValue 來追蹤多個執行緒。 這些函式可讓 DLL 追蹤執行緒特定資料。

在動態連結至 MFC 的一般 MFC DLL 中,如果您使用任何 MFC OLE、MFC 資料庫(或 DAO)或 MFC 通訊端支援,則分別支援偵錯 MFC 擴充 DLL MFCO D.dll 版本 D.dll、MFCD 版本 D.dll 和 MFCN 版本 D.dll(其中 version 是版本 號碼)會自動連結。 您必須針對您在一般 MFC DLL CWinApp::InitInstance 中所使用的每個 DLL 呼叫下列其中一個預先定義的初始化函式。

MFC 支援類型 要呼叫的初始化函式
MFC OLE (MFCO D.dll 版 AfxOleInitModule
MFC 資料庫 (MFCD D.dll 版 AfxDbInitModule
MFC 通訊端 (MFCN D.dll 版 AfxNetInitModule

初始化 MFC 擴充 DLL

因為 MFC 擴充 DLL 沒有 CWinApp 衍生的物件(如同一般 MFC DLL),因此您應該將初始化和終止程式碼新增至 DllMain MFC DLL 精靈產生的函式。

精靈提供下列 MFC 擴充功能 DLL 的程式碼。 在程式碼中, PROJNAME 是專案名稱的預留位置。

#include "pch.h" // For Visual Studio 2017 and earlier, use "stdafx.h"
#include <afxdllx.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static AFX_EXTENSION_MODULE PROJNAMEDLL;

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      TRACE0("PROJNAME.DLL Initializing!\n");

      // MFC extension DLL one-time initialization
      AfxInitExtensionModule(PROJNAMEDLL,
                                 hInstance);

      // Insert this DLL into the resource chain
      new CDynLinkLibrary(Dll3DLL);
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      TRACE0("PROJNAME.DLL Terminating!\n");
   }
   return 1;   // ok
}

初始化期間建立新的 CDynLinkLibrary 物件可讓 MFC 擴充 DLL 將物件或資源匯出 CRuntimeClass 至用戶端應用程式。

如果您要從一或多個一或多個一般 MFC DLL 使用 MFC 擴充 DLL,您必須匯出建立 CDynLinkLibrary 物件的初始化函式。 您必須從使用 MFC 擴充 DLL 的每個一般 MFC DLL 呼叫該函式。 呼叫這個初始化函式的適當位置是在一般 MFC DLL CWinApp 衍生物件的成員函式中 InitInstance ,再使用任何 MFC 延伸模組 DLL 的匯出類別或函式。

DllMain在 MFC DLL 精靈產生的 中,呼叫 AfxInitExtensionModule 會擷取模組的執行時間類別( CRuntimeClass 結構),以及其物件處理站 ( COleObjectFactory objects) 在建立物件時 CDynLinkLibrary 使用。 您應該檢查 的 AfxInitExtensionModule 傳回值;如果從 AfxInitExtensionModule 傳回零值,則從函 DllMain 式傳回零。

如果您的 MFC 延伸模組 DLL 將明確連結至可執行檔(表示要連結至 DLL 的可執行檔呼叫 AfxLoadLibrary ),您應該在 上新增 對 的 DLL_PROCESS_DETACH 呼叫 AfxTermExtensionModule 。 當每個進程與 MFC 延伸模組 DLL 中斷連結時,此函式可讓 MFC 清除 MFC 擴充 DLL(當進程結束或 DLL 因呼叫而卸載時發生 AfxFreeLibrary )。 如果您的 MFC 延伸模組 DLL 會隱含地連結至應用程式,則不需要呼叫 AfxTermExtensionModule

在釋放 DLL 時,明確連結至 MFC 擴充 DLL 的應用程式必須呼叫 AfxTermExtensionModule 。 如果應用程式使用多個執行緒,則它們也應該使用 AfxLoadLibraryAfxFreeLibrary (而不是 Win32 函 LoadLibrary 式和 FreeLibrary )。 使用 AfxLoadLibraryAfxFreeLibrary 可確保載入和卸載 MFC 擴充功能 DLL 時執行的啟動和關機程式碼不會損毀全域 MFC 狀態。

由於 MFCx0.dll 在呼叫時 DllMain 會完全初始化,因此您可以在 內 DllMain 配置記憶體並呼叫 MFC 函式(不同于 16 位版本的 MFC)。

擴充 DLL 可以藉由處理 DLL_THREAD_ATTACH 函式中的 DllMainDLL_THREAD_DETACH 案例來處理多執行緒。 當執行緒附加和中斷連結 DLL 時,會傳遞 DllMain 這些案例。 附加 DLL 時呼叫 TlsAlloc 可讓 DLL 維護連結至 DLL 之每個執行緒的執行緒本機儲存體 (TLS) 索引。

請注意,標頭檔 Afxdllx.h 包含 MFC 副檔名 DLL 中使用的結構的特殊定義,例如 和 CDynLinkLibrary 的定義 AFX_EXTENSION_MODULE 。 您應該在 MFC 擴充 DLL 中包含此標頭檔。

注意

請務必不要在 pch.h 中定義或取消定義任何宏 Visual Studio 2017 和更早版本中的 _AFX_NO_XXX stdafx.h )。 這些宏只為了檢查特定目標平臺是否支援該功能而存在。 您可以撰寫程式來檢查這些宏(例如 #ifndef _AFX_NO_OLE_SUPPORT ),但您的程式絕對不應該定義或取消定義這些宏。

處理多執行緒的範例初始化函式包含在 Windows SDK 的 Dynamic-Link 程式庫中 使用執行緒本機儲存體中。 請注意,此範例包含稱為 LibMain 的進入點函式,但您應該將此函 DllMain 式命名為 ,讓它與 MFC 和 C 執行時間程式庫搭配運作。

另請參閱

在 Visual Studio 中建立 C++ DLL
DllMain 進入點
動態連結程式庫最佳做法