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


轉自:http://blog.chinaunix.net/uid-20255102-id-1713076.html

  !建議耐心看完!

  鏈接庫分為靜態鏈接庫和動態鏈接庫,而動態鏈接庫在使用時,又進一步分為裝載時鏈接和運行時鏈接。裝載時鏈接是指該動態鏈接庫是在程序裝入時進行加載鏈接的,而運行時鏈接是指該動態鏈接庫是在程序運行時執行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調用約定在輸出函數名前面加下划線,后面加“@”符號和參數的字節數,形如_functionName@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

          1    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
        [=<internalname1] [@<num1="">]
        [=<internalname2] [@<num2="">]
        ;...
    (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函數的導出名不再是默認的“_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即可。
 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM