Windows API編程之動態鏈接庫(DLL)

From: http://blog.chinaunix.net/u/18517/showart_309975.html

作者:tyc611,2007-05-26

 

鏈接庫分爲靜態鏈接庫和動態鏈接庫,而動態鏈接庫在使用時,又進一步分爲裝載時鏈接和運行時鏈接。裝載時鏈接是指該動態鏈接庫是在程 序裝入時進行加載鏈接的,而運行時鏈接是指該動態鏈接庫是在程序運行時執行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. h
 * /
#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 ame@number ,而__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 _GetMax@8

從上面dumpbin的輸出看,GetMax函數在WINAPI調用約定方式下在DLL裏的名字與源碼中的函數定義時的名字不再相同,其導出名是"_GetMax@8" 。此時,你把testMain.c中的函數指針類型聲明和函數查找語句作如下修改:
    typedef int (WINAPI * PGetMax)(int, int);
    pGetMax = (PGetMax)GetProcAddress(hDll, " _GetMax@8 ");
再次編譯鏈接,然後運行,發現結果又正確了。

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

模塊定義文件(.def)

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

    一個最小的.def文件必須包含下面的模塊定義語句:
    (1)文件中第一個語句必須是LIBRARY語句。該語句標記該.def文件屬於哪個DLL。語法形式爲:LIBRARY <DLL名>。
    (2)EXPORTS語句列表。第一個導出語句的形式爲:entryname[=internalname] [@ordinal],列出DLL中要導出的函數的名字和可選的序號(ordinal value)。要導出的函數名可以是程序源碼中的函數名,也可以定義新的函數別名(但後面必須緊跟[=<原函數名>]);序號必須在範圍1到 N之間且不能重複,其中N是DLL中導出的函數個數。因此,EXPORTS語句語法形式爲:
    EXPORTS
        <funcName1>[=<InternalName1] [@<num1>]
        <funcName2>[=<InternalName2] [@<num2>]
        ;...
    (3)雖然不是必須的,一個.def文件也常常包含DESCRIPTION語句,用來描述該DLL的用途之類,語法形式爲:
    DESCRIPTION "<Description about the purpose of this DLL.>"
    (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函數的導出名不再是默認的“_GetMax@8”。另外,需要注 意的是,兩個導出函數有相同的相對虛擬地址(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即可。這裏放上一個Programming Windows上的一個例子:

文件: ShowBit.rar
大小: 19KB
下載: 下載

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章