首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 更多程式設計知識

VC++ 動態連結庫 (DLL) 程式設計深度指南:導出、呼叫慣例、DllMain 與 __stdcall

     編輯:更多程式設計知識

本文系統性整理 VC++ DLL 開發核心知識:導出函數聲明兩大方式、靜態/動態呼叫對比、DllMain 入口函數詳解、呼叫慣例 `__stdcall` 與 `__cdecl` 差異、導出變數與類別實作。

1. 導出函數聲明兩種方式

方式一:__declspec(dllexport) 直接標記

// Lib.h
extern "C" __declspec(dllexport) int add(int x, int y);

方式二:模組定義檔 (.def)

; Lib.def
LIBRARY DllTest
EXPORTS
    add @1   ; @1 指定序數,GetProcAddress 可用 MAKEINTRESOURCE(1) 取得

.def 規則:`LIBRARY` 指定 DLL 名稱;`EXPORTS` 列出函數名,可加 `@n` 指定序數;註解以 `;` 開頭獨佔一行。

2. DLL 呼叫方式:動態載入 vs 靜態連結

特性動態載入 (LoadLibrary/GetProcAddress)靜態連結 (隱式連結)
載入時機執行期程式碼控制進程啟動時由載入器自動載入
靈活度高(可延遲載入、條件載入、熱插拔)低(啟動必須存在 DLL)
函數取得`GetProcAddress(hDll, "func")` 或序數直接呼叫函數名,如同本地函數
釋放`FreeLibrary(hDll)` 手動釋放進程結束自動釋放(參考計數歸零)
所需檔案僅 .dll需 .lib (導入庫) + .h + .dll

動態載入範例

typedef int(*LPADD)(int, int);
HINSTANCE hDll = LoadLibrary("DllTest.dll");
if (hDll) {
    LPADD pAdd = (LPADD)GetProcAddress(hDll, "add");
    if (pAdd) printf("%d", pAdd(2,3));
    FreeLibrary(hDll);
}

靜態連結範例

#pragma comment(lib, "DllTest.lib")
extern "C" __declspec(dllimport) int add(int x, int y);
int main() { printf("%d", add(2,3)); }

`.lib` 為導入庫,僅含導出符號重定位資訊與 DLL 檔名,連結時嵌入 EXE,載入器據此尋找並載入 DLL。

3. DllMain 入口函數詳解

BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason, LPVOID lpReserved) {
    switch (ul_reason) {
        case DLL_PROCESS_ATTACH:  // 進程載入 DLL
            // 初始化、分配資源、DisableThreadLibraryCalls(hModule) 優化
            break;
        case DLL_THREAD_ATTACH:   // 執行緒建立
            break;
        case DLL_THREAD_DETACH:   // 執行緒結束
            break;
        case DLL_PROCESS_DETACH:  // 進程卸載 DLL
            // 清理資源
            break;
    }
    return TRUE;  // FALSE 會導致載入失敗(僅 PROCESS_ATTACH 有效)
}
  • `APIENTRY` 定義為 `__stdcall` (WINAPI)。
  • `hModule` 即 `HINSTANCE`,模組在進程虛擬位址空間的基底位址。
  • 若無 `DllMain`,系統提供預設空實作,**但規範要求自行實作**。
  • 執行序列驗證:`LoadLibrary` → `DLL_PROCESS_ATTACH` → 呼叫函數 → `FreeLibrary` → `DLL_PROCESS_DETACH`。

4. 呼叫慣例:__stdcall vs __cdecl

關鍵規則:DLL 導出函數若需被非 C/C++ 語言呼叫,**必須聲明為 `__stdcall`**;呼叫端函數指標類型必須一致:

// DLL 端
extern "C" __declspec(dllexport) int __stdcall add(int x, int y);
// 呼叫端
typedef int (__stdcall *LPADD)(int, int);  // 必須加 __stdcall

不一致會導致執行期錯誤「Run-Time Check Failure #0 - The value of ESP was not properly saved...」。

5. 導出變數

.def 方式

EXPORTS
    g_globalVar DATA   ; 或 CONSTANT (舊式)

呼叫端引用

// 方式一:引用地址 (需強制轉型)
extern int g_globalVar;
printf("%d", *(int*)&g_globalVar);  // 取址後解參考

// 方式二:__declspec(dllimport) 直接引用值 (推薦)
extern __declspec(dllimport) int g_globalVar;
printf("%d", g_globalVar);  // 直接使用
g_globalVar = 100;          // 直接賦值

6. 導出類別

標頭檔宏切換:

#ifdef DLL_EXPORTS
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif

class DLL_API CMyClass {
public:
    void Method();
};

DLL 專案定義 `DLL_EXPORTS`(專案屬性 → C/C++ → Preprocessor),呼叫端不定義,同一標頭檔雙邊通用。

總結

  1. 導出函數:`__declspec(dllexport)` 或 `.def` 二選一。
  2. 呼叫方式:靜態連結簡單高效,動態載入靈活支援插件架構。
  3. `DllMain` 必須正確處理四大事件,避免在其中執行複雜操作(如載入其他 DLL、建立執行緒)。
  4. 跨語言/跨編譯器介面**強制使用 `__stdcall`**,並確保雙邊聲明一致。
  5. 導出變數/類別優先使用 `__declspec(dllimport)` 簡化呼叫端程式碼。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
項目__stdcall (WINAPI, CALLBACK, APIENTRY)__cdecl (C/C++ 預設)
堆疊清理被呼叫者清理呼叫者清理
參數壓入順序右至左右至左
修飾名稱 (C 連結)`_func@n` (n=參數位元組數)`_func`
可變參數不支援支援 (如 printf)
跨語言呼叫Windows API 標準,VB/Delphi 相容C/C++ 內部預設