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
作 物件的 InitInstance
和 ExitInstance
成員函式,以執行 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 衍生類別的 CWinApp
和 ExitInstance
成員函式中。 由於 MFC 提供 DllMain
針對 DLL_PROCESS_ATTACH
和DLL_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、MFCD版本D.dll,以及 MFCN版本D.dll(其中版本是版本號碼)會自動連結。 您必須針對您在一般 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
。 如果應用程式使用多個線程,則它們也應該使用 AfxLoadLibrary
和 AfxFreeLibrary
(而不是 Win32 函 LoadLibrary
式和 FreeLibrary
)。 使用 AfxLoadLibrary
和 AfxFreeLibrary
可確保載入和卸除 MFC 擴充功能 DLL 時執行的啟動和關機程式代碼不會損毀全域 MFC 狀態。
由於MFCx0.dll在呼叫時 DllMain
會完全初始化,因此您可以在 內 DllMain
配置記憶體並呼叫 MFC 函式(不同於 16 位版本的 MFC)。
擴充 DLL 可以藉由處理 DLL_THREAD_ATTACH
函式中的 DllMain
和 DLL_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 運行時間連結庫搭配運作。