1. 程式人生 > >動態連結庫(DLL)

動態連結庫(DLL)

連結庫分為靜態連結庫和動態連結庫,而動態連結庫在使用時,又進一步分為裝載時連結和執行時連結。裝載時連結是指該動態連結庫是在程式裝入時進行載入連結的,而執行時連結是指該動態連結庫是在程式執行時執行LoadLibrary(或LoadLibraryEx,下同)函式動態載入的。因此,由於動態連結庫有這兩種連結方式,所以在編寫使用DLL的程式時,就有了兩種可選方案。     可能有人會問“為什麼需要裝載時連結?直接靜態連結不就行了嗎?”,這是模組化程式設計的需要。試想,如果你開發一個很大的程式,並且經常需要更新。如果你選擇靜態連結,那麼每次更新就必須更新整個exe檔案,而如果你把需要經常更新的模組做成dll,那麼只需要更新這個檔案即可,每次程式執行時載入這個更新的檔案即可。     在進入編寫DLL程式之前,先介紹一些相關知識。     VC支援三種DLL,它們分別是Non-MFC DLL、MFC Regular DLL、MFC Extension DLL。由於本文只講解API程式設計,所以這裡只對第一種DLL進行介紹,後面兩種DLL將在另外的文章中介紹。     動態連結庫的標準字尾是.DLL,當然也可以使用其它任意字尾名。但使用.DLL字尾的好處是:一是,很直觀表明該檔案的性質;二是,只有字尾為.DLL的動態連結庫才能被Windows自動地載入,而其它字尾的動態連結庫只能通過LoadLibrary顯示式載入。     動態連結庫的用途:一是作為動態函式庫使用,另一個常用的方式是作為動態資源庫。當然,沒有絕對的劃分,比如你的動態函式庫時也可能有資源,但動態資源庫一般不會有函式。     另兩個重要的、需要區分的概念是:物件庫(Object Library)和匯入庫(Import Library)。物件庫是指普通的庫檔案,比如C執行時庫libc.lib;而匯入庫是一種比較特殊的物件庫檔案,與一個動態連結庫相對應。它們都有後綴.lib,並且都僅在程式編譯連結時使用,被連結器用來解析函式呼叫。然而,匯入庫不包含程式碼,它只為連結器提供動態連結庫的資訊,以便於連結器對動態連結庫中的物件作恰當地連結。     動態連結庫的查詢規則。如果在使用時沒有指定動態連結庫的路徑,則Windows系統按如下順序搜尋該動態連結庫:使用該動態連結庫的.exe檔案所在目錄、當前目錄、Windows系統目錄、Windows目錄、環境變數%PATH%中的路徑下的目錄。         DLL內的函式劃分為兩種型別:(1)匯出函式,可供應用程式呼叫;(2) 內部函式(普通函式),只能在DLL程式內使用,應用程式無法呼叫它們。同樣的劃分適用於資料物件。     在DLL中,要匯出某個物件(函式或者資料),宣告方式有兩種:一種是利用關鍵字__declspec(dllexport);另一種方式是採用模組定義檔案(.def)。另外,還可以通過連結選項/EXPORT指定匯出。應該優先選用第一種方式,但.def檔案方式在某些情況下是必須的。     下面,我們分別介紹動態連結庫的的製作、釋出、使用及相關技術,重點介紹裝載時連結和執行時連結的使用方法。在介紹執行時連結時,引入了模組定義檔案(.def),詳細介紹了其在DLL製作過程中的作用及使用方法。另外,還介紹了DLL中全域性變數的匯出、DLL中的資料共享和資源DLL的製作及使用。 動態連結庫的製作及裝載時連結
    首先,開啟VC6.0,建立一個名為DLLTest的空工作區。然後,建立一個名為DLL_Lib的Win32 Dynamic-Link Library工程,注意將該工程新增到剛建立的工作區DLLTest中,並且將該工程儲存在工作區的目錄下(不建子目錄)。然後,在該工程中,加入這下面兩個檔案:

/* 
 * dll_lib.
 */
#ifndef DLL_LIB_H
#define DLL_LIB_H

#ifdef __cplusplus
#define EXPORT extern "C" __declspec (dllexport)
#else
#define EXPORT __declspec (

dllexport)
#endif

EXPORT int WINAPI GetMax(int a, int b);

#endif

/*
 * dll_lib.c
 */

#include <windows.h>
#include <stdio.h>
#include "dll_lib.h"

int WINAPI DllMain (HINSTANCE hInstance, DWORD fdwReason, PVOID pvReserved)
{
    switch (fdwReason)    
    {    
    case DLL_PROCESS_ATTACH:
        printf

("> process attach of dll\n");
        break;
        
    case DLL_THREAD_ATTACH:
        printf("> thread attach of dll\n");
        break;
        
    case DLL_THREAD_DETACH:
        printf("> thread detach of dll\n");
        break;
        
    case DLL_PROCESS_DETACH:
        printf("> process detach of dll\n");
        break;
    }

    return TRUE;
}

int GetMax(int a, int b)
{
    return a > b ? a : b;
}

    接著,再建立一個Win32 Console Application工程DLL_Test,同樣將該工程加入先前的DLLTest工作區中,並直接儲存在該工作區目錄下。然後向工程DLL_Test加入下面的檔案:

/* 
 * testMain.c
 */

#include <windows.h>
#include <stdio.h>
#include "dll_lib.h"

int main()
{
    int a = 2;
    int b = 3;
    printf(" max(2, 3) = %d\n", GetMax(2, 3));

    return 0;
}

    此時,工作差不多做完了,但還需進行一下設定。在Project|Settings裡,把兩個工程裡的General標籤裡的Intermediate files和Output files都設定為Debug。這樣確保兩個工程的輸出檔案在一個目錄中,以便後面動態庫連結時的查詢。另外,設定DLL_Test為活動工程(Project|Set Active Project),設定DLL_Test依賴於DLL_Lib(Project|Dependencies)。此時,就可以編譯運行了。執行結果為:

> process attach of dll
 max(2, 3) = 3
> process detach of dll
Press any key to continue

    下面對上面的程式碼和結果進行分析。

    在dll_lib.h中,EXPORT巨集實質上就是一個匯出函式所需要的關鍵字。__declspec (dllexport)是Windows擴充套件關鍵字的組合,表示DLL裡的物件的儲存型別關鍵字。extern "C"用於C++程式使用該函式時的函式宣告的連結屬性。WINAPI是巨集定義,等價於__stdcall。下面列出Windows程式設計中常見的幾種有關呼叫約定的巨集,它們都是與__stdcall和__cdecl有關的(from windef.h):

    #define CALLBACK   __stdcall     // 用於回撥函式
    #define WINAPI     __stdcall     // 用於API函式
    #define WINAPIV    __cdecl
    #define APIENTRY   WINAPI      
    #define APIPRIVATE __stdcall
    #define PASCAL     __stdcall

另外,關於__stdcall:如果通過VC++編寫的DLL欲被其他語言編寫的程式呼叫,應將函式的呼叫約定宣告為__stdcall方式,WINAPI、CALLBACK都採用這種方式,而C/C++預設的呼叫方式卻為__cdecl。__stdcall方式與__cdecl對函式名最終生成符號的方式不同。若採用C編譯方式(在C++中需將函式宣告為extern "C"),__stdcall呼叫約定在輸出函式名前面加下劃線,後面加“@”符號和引數的位元組數,形如_functionN[email protected] ,而__cdecl呼叫約定僅在輸出函式名前面加下劃線,形如_functionName。(小技巧:如何檢視這些符號?寫一個程式,只提供函式的宣告而不給定義,就可以看到連結器給出的符號了)

    因此,在前面例子中,該DLL聲明瞭一個匯出函式GetMax,其連線屬性採用CALLBACK(即__stdcall)。另外,請注意,例子中的巨集EXPORT會根據是在C程式還是在C++程式中被呼叫選擇相應的連線方式。在定義匯出函式時,不需要EXPORT巨集,只需要在函式宣告時使用即可。

    DllMain函式在DLL載入和解除安裝時被呼叫。它的第一個引數是DLL控制代碼,第三個引數保留。第二個引數用來區分該DLLMain函式是在什麼情況下被呼叫的,如程式所示。如果初始化成功,則DllMain應該返回一個非零值。如果返回零值將導致程式停止執行(你可以修改上面例子中的DllMain的返回值為0,將看到相應的出錯結果)。如果在你的DLL程式中沒有編寫DllMain函式,那麼在執行該DLL時,系統將引入一個不做任何操作的預設DllMain函式版本。

    在前面的例子中,給出了DLL的製作及使用。注意,我們在使用DLL時,直接關聯了兩個工程。如果你想把自己製作的DLL提供給別人使用,而又不想提供原始碼,那應該怎麼做呢?     由文章最開始的分析知,要達到這個目的,只需要提供給DLL使用者三個檔案即可:.h檔案,.lib檔案和.dll檔案。當然,對於dll_lib庫,我們只需要提供dll_lib.h, dll_lib.lib, dll_lib.dll三個檔案即可。     使用者應該怎麼使用些檔案呢?我們利用前面的工程進行介紹。首先將前面兩個工程的依賴關係去掉,並設定DLL_Test工程為當前活動工程。先編譯下下試試,你會發現,編譯器在連結時會發生錯誤,提示不能完成GetMax函式的連結。然後,找到Project|Settings|Link|Object/Library Modules,往裡加入庫檔案debug\dll_lib.lib。再次連結,OK!執行,結果跟最先的結果一模一樣。小結:(1)庫使用者在呼叫DLL的匯出函式的檔案中包含庫標頭檔案;(2)將與.dll對應的.lib庫檔案加入工程的連結庫中;(3)在.exe檔案所在目錄中放入一份.dll檔案的拷貝。當然,如果是已經發布的.exe程式使用的.dll需要更新,此時只需要將.dll替換原來的.dll即可。 執行時連結     前面介紹了DLL的製作及相關技術和它的裝載時連結,下面介紹執行時連結的方法。還是接著利用前面的例子,需要做一點小小的修改:把DLL_Lib工程裡的GetMax函式的WINAPI呼叫約定暫時先去掉(後面將說明為什麼這樣做),然後編譯該工程。然後,將testMain函式作如下修改:

/* 
 * testMain.c
 */

#include <windows.h>
#include <stdio.h>

typedef int (* PGetMax)(int, int);

int main()
{
    int a = 2;
    int b = 3;
    
    HINSTANCE hDll;  // DLL控制代碼 
    PGetMax pGetMax; // 函式指標
    
    hDll = LoadLibrary(".\\Debug\\DLL_lib.dll");
    if (hDll == NULL) {
        printf("Can't find library file \"dll_lib.dll\"\n");
        exit(1);
    }
    
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
    if (pGetMax == NULL) {
        printf("Can't find function \"GetMax\"\n");
        exit(1);
    }

    printf(" max(2, 3) = %d\n", pGetMax(2, 3));
    
    FreeLibrary(hDll);

    return 0;
}

此時,不再需要動態的.h檔案和.lib檔案,只需要提供.dll檔案即可。在具體使用時,先用LoadLibrary載入Dll檔案,然後用GetProcAddress尋找函式的地址,此時必須提供該函式的在Dll中的名字(不一定與函式名相同)。

    然後編譯連結、執行,結果與前面的執行結果相同。

    下面將解釋,為什麼前面要去掉WINAPI呼叫約定(即採用預設的__cdecl方式)。我們可以先看看DLL_Lib.dll裡面的連結符號。在cmd中執行命令:
    dumpbin /exports DLL_Lib.dll
得到如下結果:

Dump of file f:\code\DLLTest\Debug\Dll_lib.dll

File Type: DLL

  Section contains the following exports for DLL_Lib.dll

           0 characteristics
    4652C3B1 time date stamp Tue May 22 18:19:29 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

             0 0000100A GetMax

  Summary

        4000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       28000 .text

可以看到GetMax函式在編譯後在Dll中的名字仍為GetMax,所以在前面的程式中使用的是:
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");

    然後,我們把WINAPI添加回去,重新編譯DLL_Lib工程。執行剛才的DLL_Test程式,執行出錯,結果如下:
> process attach of dll
Can't find function "GetMax"
> process detach of dll
Press any key to continue

顯然,執行失敗原因是因為沒有找到GetMax函式。再次執行命令:dumpbin /exports DLL_Lib.dll,結果如下(部分結果):

           1 ordinal base
           1 number of functions
           1 number of names

    ordinal hint RVA      name

          1    0 0000100A [email protected]

從上面dumpbin的輸出看,GetMax函式在WINAPI呼叫約定方式下在DLL裡的名字與原始碼中的函式定義時的名字不再相同,其匯出名是"[email protected]"。此時,你把testMain.c中的函式指標型別宣告和函式查詢語句作如下修改:
    typedef int (WINAPI* PGetMax)(int, int);
    pGetMax = (PGetMax)GetProcAddress(hDll, "[email protected]");
再次編譯連結,然後執行,發現結果又正確了。

    現在找到了問題所在。很顯然,這種修改方式並不適用,而預設生成的名字又不是我們所想要的。那麼該怎麼解決這個問題呢?這就需要用到.def檔案來解決。

模組定義檔案(.def)

    模組定義檔案(.def檔案)是一個描述DLL的各種屬性的檔案,可以包含一個或多個模組定義語句。如果你不使用關鍵字__declspec(dllexport)關鍵字匯出DLL中的函式,那麼DLL就需要一個.def檔案。

    一個最小的.def檔案必須包含下面的模組定義語句:
    (1)檔案中第一個語句必須是LIBRARY語句。該語句標記該.def檔案屬於哪個DLL。語法形式為:LIBRARY 。
    (2)EXPORTS語句列表。第一個匯出語句的形式為:entryname[=internalname] [@ordinal],列出DLL中要匯出的函式的名字和可選的序號(ordinal value)。要匯出的函式名可以是程式原始碼中的函式名,也可以定義新的函式別名(但後面必須緊跟[=<原函式名>]);序號必須在範圍1到N之間且不能重複,其中N是DLL中匯出的函式個數。因此,EXPORTS語句語法形式為:
    EXPORTS
        [=<internalname1] [@]
        [=<internalname2] [@]
        ;...
    (3)雖然不是必須的,一個.def檔案也常常包含DESCRIPTION語句,用來描述該DLL的用途之類,語法形式為:
    DESCRIPTION ""
    (4)在任意位置,可以包含註釋語句,以分號(;)開始。

    例如,在本文中後面將用到的.def檔案為:

; DLL_Lib.def

LIBRARY DLL_Lib     ; the dll name
DESCRIPTION "Learn how to use the dll."

EXPORTS
    GetMax @1
    Max=GetMax @2   ; alias name of GetMax

; Ok, over

    現在,讓我們回到DLL_Lib工程,修改GetMax函式的宣告,把EXPORT去掉,重新編譯該工程。然後,執行dumpbin命令,我們發現此時沒有匯出函式。再將上面的DLL_Lib.def檔案新增進DLL_Lib工程,再次編譯,並執行dumpbin命令,得到如下結果(引用部分結果):

          1 ordinal base
          2 number of functions
          2 number of names

   ordinal hint RVA      name

         1    0 0000100A GetMax
         2    1 0000100A Max

    正如我們所預期的,有兩個匯出函式GetMax和Max。注意,此時原始碼中的GetMax函式的匯出名不再是預設的“[email protected]”。另外,需要注意的是,兩個匯出函式有相同的相對虛擬地址(RVA),也說明了兩個匯出名實質是同一個函式的不同名字而已,都是原始碼中GetMax函式的匯出名。

    現在,回到DLL_Test工程,修改testMain.c檔案內容如下:

/* 
 * testMain.c
 */

#include <windows.h>
#include <stdio.h>

typedef int (WINAPI* PGetMax)(int, int);

int main()
{
    int a = 2;
    int b = 3;
    
    HINSTANCE hDll; // DLL控制代碼 
    PGetMax pGetMax; // 函式指標
    
    hDll = LoadLibrary(".\\Debug\\DLL_lib.dll");
    if (hDll == NULL) {
        printf("Can't find library file \"dll_lib.dll\"\n");
        exit(1);
    }
    
    pGetMax = (PGetMax)GetProcAddress(hDll, "GetMax");
    if (pGetMax == NULL) {
        printf("Can't find function \"GetMax\"\n");
        exit(1);
    }
    printf(" GetMax(2, 3) = %d\n", pGetMax(2, 3));

    pGetMax = (PGetMax)GetProcAddress(hDll, "Max");
    if (pGetMax == NULL) {
        printf("Can't find function \"GetMax\"\n");
        exit(1);
    }
    printf(" Max(2, 3) = %d\n", pGetMax(2, 3));
    
    FreeLibrary(hDll);
    return 0;
}

    編譯連結、執行,結果如下:

> process attach of dll
 GetMax(2, 3) = 3
 Max(2, 3) = 3
> process detach of dll
Press any key to continue

    執行結果正如前面分析的那樣,GetMax和Max都得到了相同的結果。

    到這裡,我們解決了DLL匯出函式名在各種呼叫約定下的預設名可能不同於原始碼中函式名的問題。此時,你就可以製作跟Windows的自帶API函式庫相同的庫了:使用__stdcall呼叫約定以滿足Windows下的任何語言都可以呼叫DLL庫,同時使用函式名作為匯出名,以方便使用者使用DLL裡的函式。

匯出全域性變數     前面我們介紹了DLL中的函式的匯出方法,這裡也介紹一下DLL中全域性變數的匯出。     首先需要明確的是,當多個應用程式同時使用同一個DLL時,系統中只有一個DLL例項(這裡主要指程式碼段,一般不包含資料段)。也就是說,如果沒有特殊處理,DLL中的資料都是每個使用DLL的應用都保留一份副本的(但是,可以根據需要實現DLL資料的共享,後面進行介紹)。因此,使用DLL的各應用程式之間不會發生干擾。     要匯出DLL中的全域性變數,方法與匯出函式基本一樣。只是,在定義.def檔案時,在EXPORTS定義語句之後用DATA識別符號表明這是變數。例如:g_oneNumber DATA 或者 g_oneNumber @3 DATA。     在使用DLL中匯出的全域性變數時,對於前面DLL的兩種連結方式,有不同的方法。其中,對於執行時連結的DLL,其使用方法與函式一樣(流程:LoadLibrary, GetProcAddress),只是在使用時要知道這是一個變數的地址,而不再是一個函式的地址即可(其實,用dumpbin工具檢視DLL的匯出列表,會發現匯出的資料也被當作函式計數)。 對於裝載時連結,要匯入DLL中的變數,有點與函式不一樣的地方,那就是必須顯示地用關鍵字__declspec(dllimport)匯入DLL中的變數。例如,在使用前面的g_oneNumber前,應先匯入:__declspec(dllimport) extern int g_oneNumber。然後,其它與函式的使用方法無異。 共享DLL中的資料     有時,可能需要在使用DLL的多個應用之間共享DLL的資料,而預設情況下,DLL的資料是每個應用擁有一份副本的。要實現這個需求,就需要做些特殊處理。     首先,定義一個數據段,裡面有需要共享的變數,並要初始化這些變數。然後設定該資料段為共享即可,比較簡單。例如,要在DLL中共享int型變數g_oneNumber,那麼應按如下方式定義該變數:
#pragma data_seg ("shared")        
int g_oneNumber = 0;
#pragma data_seg ()
#pragma comment(linker,"/SECTION:shared,RWS")     對上面的程式碼做些解釋:#pragma data_seg ("shared")建立了一個數據段,命名為Shared;#pragma data_seg()標記該資料段的結束;它們之間定義的是該資料段中的變數。注意:這裡對變數的初始化是必須的,否則,編譯器會把未初始化的變數放在普通的未初始化資料段,而不是在共享的資料段。     #pragma comment(linker, "SECTION:shared,RWS")告訴連結器shared資料段具有RWS屬性。這裡的RWS是指Read、Write和Shared三個屬性。也可以在IDE中設定工程屬性:在Settings|Link|Project Options中,新增連結引數:/SECTION:shared,RWS。 資源DLL的製作及使用     有了前面的基礎,資源DLL的製作及使用相對簡單多了。如果是純資源DLL的話(沒有匯出函式),那麼只需要定義一個有DLLMain函式的檔案即可,然後加入資源,編譯成DLL庫即可。在使用時,只需要動態載入這個資源庫,然後載入庫裡的資源即可。例如,資源庫裡有點陣圖資源,那麼只需要LoadBitmap即可。