深入動態庫

一、動態連接庫的用途 
   動態連接庫,dynamic-link libraries(DLL),是微軟公司提供的一項軟件技術。 

它實質上是包含了一些函數和數據的可執行模塊,它可以被應用程序(.EXE)或其它DLL
調用。這種技術有以下好處:共享資源、節省內存、支持多語種、可重複利用、便於大
項目的開發等。這樣說是不是有點老套,也是,教科書都有的嘛。咳,就當複習一下功課了.... 

   下面說一下我的理解。 

   沒有總結,就沒有進步。這話好象聽誰說過的。作爲一種載體,用來對過去經驗作 

個總結,動態庫得天獨厚。比方說你在以往的項目開發或編程中積累下了很多的經驗、
技巧、想法(?)和專業資料,而且它們在特定的領域很有價值。但是隨着開發工具的
發展、執行平臺的升級,已往的這些經驗、技巧和資料可能就會被丟棄。其實將它們作
爲對以前勞動成果的一種總結,彙集到特定的動態庫中,不失爲一種兩全其美的方法。
由於動態庫與編程語言無關,如此得到的資源可以得到更廣泛地應用。作爲一種長遠
考慮,資源的重複利用不但沒有使以往的勞動浪費,而且使原來的勞動增值,使工作
更有效。尤其是資源的重複利用問題,如果系統地考慮軟件複用則是解決軟件開發中
重複勞動問題的一種方案,動態庫則是一種途徑和方法。以已有的工作爲基礎,充分
利用過去應用系統開發中積累的知識和經驗,將開發的重點集中於應用的特有構成成
分上,消除重複勞動,避免重新開發可能引入的錯誤,從而提高軟件開發的效率和質量。 

   另外,作爲混合編程的一種特例,動態庫當仁不讓。由於動態庫與具體的編程語言 

無關,只要這種語言支持動態庫技術,則這種語言就能拿來用,目的只有一個“取長補
短”。各類編程語言的存在是由於它們各有所長。我們可以通過動態庫將一個大的任務
分割成一個個子任務,這些子任務可以分別由不同的語言來實現。 

   還有一個最成功的例子:微軟的應用程序接口API。 

   二、動態連接庫的有關約定 

   關於動態庫輸出函數的約定有兩種:調用約定和名字修飾約定。 

   調用約定決定着函數參數傳送時入棧和出棧的順序,以及編譯器用來識別函數名字 

的修飾約定。名字修飾約定隨調用約定和編譯種類(C或C++)的不同而變化。爲了讓不
同的編程語言共享動態庫帶來的方便,函數輸出時必須使用正確的調用約定,並且最好
不帶有任何由編譯器生成的名字修飾。 

下面就以VC5和VB5爲例,結合具體情況來說明如何實現這些要求。 

   (一)調用約定 

    VC++5.0支持的函數調用約定有多種,在這裏僅討論以下三種:__stdcall調用約定 

、C調用約定和__fastcall調用約定。 

   __stdcall調用約定相當於16位動態庫中經常使用的PASCAL調用約定。在32位的VC+ 

+5.0中PASCAL調用約定不再被支持(實際上它已被定義爲__stdcall。除了__pascal外,
__fortran和__syscall也不被支持),取而代之的是__stdcall調用約定。兩者實質上
是一致的,即函數的參數自右向左通過棧傳遞,被調用的函數在返回前清理傳送參數的
內存棧,但不同的是函數名的修飾部分(關於函數名的修飾部分在後面將詳細說明)。 

   C調用約定(即用__cdecl關鍵字說明)和__stdcall調用約定有所不同,雖然參數傳 

送方面是一樣的,但對於傳送參數的內存棧卻是由調用者來維護的(也正因爲如此,實
現可變參數的函數只能使用該調用約定),另外,在函數名修飾約定方面也有所不同。 

   __fastcall調用約定是“人”如其名,它的主要特點就是快,因爲它是通過寄存器 

來傳送參數的(實際上,它用ECX和EDX傳送前兩個雙字或更小的參數,剩下的參數仍舊
自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的內存棧),在函數名修飾約定
方面,它和前兩者均不同。 

   關鍵字 __stdcall、__cdecl和__fastcall可以直接加在要輸出的函數前,也可以在 

編譯環境的Setting...\C/C++ \Code Generation項選擇。當加在輸出函數前的關鍵字與編
譯環境中的選擇不同時,直接加在輸出函數前的關鍵字有效。它們對應的命令行參數分別
爲/Gz、/Gd和/Gr。缺省狀態爲/Gd,即__cdecl。 

   順便說明一下,要完全模仿PASCAL調用約定首先必須使用__stdcall調用約定,至於 

函數名修飾約定,可以通過其它方法模仿。還有一個值得一提的是WINAPI宏,Windows.h支
持該宏,它可以將輸出函數翻譯成適當的調用約定,在WIN32中,它被定義爲__stdcall。 

   建議:使用WINAPI宏,這樣你就可以創建自己的APIs了。 

  (二)函數名修飾約定 

   函數名修飾約定隨編譯種類和調用約定的不同而不同,下面分別說明。 

   對於C編譯,__stdcall調用約定在輸出函數名前加上一個下劃線前綴,後面加上一 

個“@”符號和其參數的字節數,格式爲_functionname@number。__cdecl調用約定僅在
輸出函數名前加上一個下劃線前綴,格式爲_functionname。__fastcall調用約定在輸出
函數名前加上一個“@”符號,後面也是一個“@”符號和其參數的字節數,格式爲
@functionname@number。它們均不改變輸出函數名中的自符大小寫,這和PASCAL調用約定
不同,PASCAL約定輸出的函數名無任何修飾且全部大寫。說到這裏,我給出一種完全模仿
PASCAL調用約定的方法,在.DEF文件的EXPORTS段通過別名來實現。例如: 

          int  __stdcall MyFunc (int a, double b); 

          void __stdcall InitCode (void); 

在 .DEF 文件中: 

         EXPORTS 

             MYFUNC=_MyFunc@12 

             INITCODE=_InitCode@0 

   C++編譯輸出的函數名修飾較爲複雜,VC++5.0的隨機文檔中也沒有給出說明。經過 

一些實驗和摸索, 

我發現了C++編譯時函數名修飾約定規則,現在說明如下。 

   __stdcall調用約定: 

         1、以“?”標識函數名的開始,後跟函數名; 

         2、函數名後面以“@@YG”標識參數表的開始,後跟參數表; 

         3、參數表以代號表示: 

            X--void , 

            D--char, 

            E--unsigned char, 

            F--short, 

            H--int, 

            I--unsigned int, 

            J--long, 

            K--unsigned long, 

            M--float, 

            N--double, 

            _N--bool, 

            .... 

            PA--表示指針,後面的代號表明指針類型,如果相同類型的指針連續出現 

,以“0”代替, 

                一個“0”代表一次重複; 

         4、參數表的第一項爲該函數的返回值類型,其後依次爲參數的數據類型; 

         5、參數表後以“@Z”標識整個名字的結束,如果該函數無參數,則以“Z” 

標識結束。 

其格式爲“?functionname@@YG*****@Z”或“?functionname@@YG*XZ”,例如 

         int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” 

         void Test2()                       -----“?Test2@@YGXXZ” 

   __cdecl調用約定: 

   規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變爲“ 

@@YA”。 

   __fastcall調用約定 

  規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變爲“@ 

@YI”。 

   (三)得到沒有修飾的函數名 

    VC++輸出函數時使用__declspec(dllexport),而不再用_export修飾字。 

    __declspec(dllexport)在C調用約定、C編譯情況下可以去掉輸出函數名的下劃線 

前綴。extern "C" 

使得在C++中使用C編譯方式成爲可能,在一個C++文件中,用extern "C"來指明該函數使 

用C編譯方式。例 

如,在一個C++文件中,有如下函數: 

  extern "C" {void  __declspec(dllexport) __cdecl Test(int var);} 

其輸出函數名爲:Test 

爲了方便,你可以使用下列預處理語句: 

          #if defined(__cplusplus) 

          extern "C" 

          { 

          #endif 

                //函數原型說明 

         #if defined(__cplusplus) 

         } 

        #endif 

如此以來,經過上面的特殊處理,不管在C中,還是在C++中都可以得到一個無任何修飾 

的函數名了。 

   下面再介紹另一條途徑:不用__declspec(dllexport)修飾字輸出函數,而用.DEF文 

件來輸出函數。 

將要輸出的函數修飾名羅列在EXPORTS之下,這個名字必須與定義函數的名字完全一致, 

如此就得到一 

個沒有任何修飾的函數名了。 

   至此,我們已有至少三種方法可以獲得“沒有任何修飾的函數名”了。 

   我在開始時就提到過“函數輸出時....最好不帶有任何由編譯器生成的名字修飾” 

,這一點在多語 

種混合編程時尤其重要。 

   (四)實驗 

   下面做一個實驗來加深一下上面介紹內容的印象。 

   實驗設想:有這樣一個軟件系統,用VB5設計它的界面,用VC5寫一個動態庫,用於 

執行一些繁瑣的 

計算,在計算過程中有一些中間結果要作簡單的顯示,我們用VB5來完成顯示任務,於是 

在VB5中定義了 

一個顯示函數,由動態庫來回調它,並且將計算結果作爲回調時的參數.... 

   首先用VB5編寫界面並定義顯示函數。新建一個工程,添加一個模塊文件,在該模塊 

文件中定義我們 

的顯示函數(即回調函數): 

        Public Sub ShowResult(result  As Long) 

         form1.Print result   '簡單模擬一下顯示而已 

        End Sub 

另外,給出動態庫輸出函數的描述: 

Declare Sub TestShow Lib "test32.dll" (ByVal Show As Long, Param As Any) 

之後,在窗體上放一個命令按鈕並添加如下代碼: 

        Private Sub Command1_Click() 

          Dim i As Long 

          TestShow AddressOf ShowResult, i 

        End Sub 

   現在用VC5寫我們的動態庫。 

   新建一個項目。選擇New Projects | Win32 Dynamic-Link Library,並輸入項目名 

Test32;然後添 

加下面內容到.CPP文件: 

     #include <windows.h> 

     BOOL WINAPI DllEntryPoint( HINSTANCE hinstDll,DWORD fdwRreason, 

                         LPVOID plvReserved) 

    { 

      return 1; // Indicate that the DLL was initialized successfully. 

    } 

   void  TestShow(int AppShow(int*),int *flag) 

   { 

    for(int i=0;i<10;i++) 

    { 

      *flag=11011+i;  //爲簡單起見,這裏用直接賦值替代“複雜計算”的結果 

      AppShow(flag);  //回調 

    } 

   } 

這裏使用.DEF文件輸出函數。添加下列內容到.DEF文件: 

   LIBRARY     TEST32 

   DESCRIPTION 'TEST32.DLL' 

   EXPORTS 

      TestShow           @1 

將調用約定設置爲__stdcall,編譯生成Test32.dll,將其拷入系統目錄。 

   最後運行上面編寫的VB5項目。OK?! 

   實驗一:將調用約定改爲缺省設置,即C調用約定,其它不變,重新編譯生成Test3 

2.dll並將其拷 

入系統目錄,試運行VB5項目看看...... 

   實驗二:將調用約定改爲缺省設置,即C調用約定,在上面的TestShow函數前加上_ 

_stdcall關鍵字 

或WINAPI宏,其它不變,重新編譯生成Test32.dll並將其拷入系統目錄,試運行VB5項目 

看看...... 

   實驗三:將調用約定改爲缺省設置,即C調用約定,在上面的TestShow函數前加上_ 

_stdcall關鍵字 

或WINAPI宏,並且在其第一個參數AppShow前加上__stdcall關鍵字,其它不變,即 

   void __stdcall TestShow(int __stdcall AppShow(int*),int *flag) 

重新編譯生成Test32.dll並將其拷入系統目錄,試運行VB5項目看看...... 

   提示:VB5的函數調用遵循API調用約定(__stdcall,即原來的PASCAL)。 

   關於回調函數的概念和約定請參閱相關書籍。 

   三、參數傳遞 

   有關WIN32動態庫的輸出函數的參數傳遞上面也說了一些,這裏主要再進一步詳細說 

明。在32位動態 

庫中,所有的參數都被擴展爲32位(如字符型參數、短整型參數),自右向左反向入棧 

。函數的返回值 

也被擴展爲32位,放在EAX寄存器中,8字節的返回值放在EDX:EAX寄存器對中,返回值 

爲更大結構時使用 

EAX作爲指向隱形返回結構的指針返回。當函數用到一些相關寄存器(如ESI, EDI, EBX 

和 EBP)時,編譯 

器會自動生成一個函數頭和一個函數尾,用於保存和恢復這些用到的寄存器。下面舉例 

描述參數傳遞的情 

況。我們已經知道,__stdcall和__cdecl調用約定的參數傳遞是相同的,__fastcall調 

用約定和它們有所 

不同。 

                void   MyFunc( char c, short s, int i, double f ); 

                 . 

                 . 

                 . 

                void    MyFunc( char c, short s, int i, double f ) 

                { 

                  . 

                  . 

                  . 

                } 

                . 

                . 

                . 

               MyFunc ('a', 22, 8192, 2.1418); 

其執行時參數傳遞情況將是這樣的: 

      __stdcall和__cdecl調用約定 

        位置                     棧 

      ESP+0x14          2.1418 

      ESP+0x10 

      ESP+0x0c          8192 

      ESP+0x08          22 

      ESP+0x04          a 

      ESP                    返回值 

      __fastcall調用約定 

        位置                     棧 

      ESP+0x0c          2.1418 

      ESP+0x08 

      ESP+0x04          8192 

      ESP                    返回值 

      ECX                  a 

      EDX                  22 

   四、棧 

   前面曾提到不同的調用約定在傳送參數時對棧的不同處理。這裏再重點說一下不同 

的調用約定是如 

何來維護棧的正常工作的,同時也更深刻地理解保持相同調用約定的重要性。我們已經 

知道,上面所提 

到的三種調用約定傳送參數時都是自右至左壓棧,這裏的壓棧的動作是由調用者來完成 

的。當調用開始, 

被調用者得到控制權,它可以對寄存器操作,而當調用結束,被調用者失去控制權,調 

用者重新得到控 

制權,此時它期望它所用到的某些寄存器恢復其調用前的狀態,尤其是棧指針,這就牽 

涉到棧的維護問 

題。前面提到__stdcall和__fastcall調用約定均是被調用的函數在返回前清理傳送參數 

的內存棧,而 

__cdecl調用約定是由調用者來維護用於傳送參數的棧。下面舉例來說明。 

                void   MyFunc1(int c ); 

                 . 

                 . 

                 . 

                void    MyFunc2(  ) 

                { 

                  int i=1; 

                  .... 

                  MyFunc1( i ); 

                  .... 

                } 

我們看一下MyFunc2的實現過程: 

       1、__stdcall和__fastcall調用約定 

             .... 

             mov eax,dword ptr [i] 

             push eax 

             call  @ILT+445(?MyFunc1@@YGXH@Z)(0x014a11bd) 

             //調用結束棧指針已恢復,由被調用者在返回前恢復 

             .... 

       2、__ cdecl調用約定 

             .... 

             mov eax,dword ptr [i] 

             push eax 

             call @ILT+30(?MyFunc1@@YAXH@Z)(0x014a101e) 

              //調用結束棧指針未恢復 

             add  esp,4 

              //調用者自己恢復棧指針 

             .... 

   現在再回過頭來看一下前面設計的實驗,由於VB5支持的是標準API調用約定,類同 

於__stdcall調 

用約定,所以當動態庫用__stdcall調用約定編譯時,實驗正常通過。而當動態庫用__c 

decl調用約定 

編譯時,實驗一和實驗二的現象能很好地說明問題,其實此時由於調用約定的不統一, 

用於傳送參數的 

棧已遭到破壞,現象就是工作不正常。實驗三中雖然仍用__cdecl編譯,但在函數名前的 

__stdcall纔是 

真正起作用的調用約定,故它也順利通過。 

   五、總結與補充 

   上面結合實驗描述了動態庫技術的幾個關鍵點:調用約定(或稱調用協議)、名字 

修飾約定、堆棧 

與參數傳遞等。目的就是爲了更深刻地理解該項技術,更好地在實際應用中使用該項技 

術。 

   另外需要補充的是關於輸出函數名的問題。前面一再強調,函數輸出時“最好不帶 

有任何由編譯(充實--學海無涯) 

生成的名字修飾”,這一點是受限於編程語言中對函數命名的規則。VB雖然也有此規則 

,但它仍然可以 

通過別名使用帶修飾的輸出函數。VB使用動態庫的語法: 

Declare Sub name Lib "libname" Alias "aliasname" (arglist) 

Declare Function name Lib "libname" Alias "aliasname" (arglist) As type 

其中Alias(別名)可以作爲一條使用帶修飾字函數的途徑。例如 

   int Test1(char *var1,unsigned long)-----“?Test1@@YGHPADK@Z” 

這是在C++環境__stdcall調用約定下得到的一個輸出函數,在VB中可以如此描述: 

   Declare Function Test Lib "test32.dll" Alias "?Test1@@YGHPADK@Z" 

                             (var1 as Byte,Byval var2 as long) As Long 

這樣一來,在VB應用程序中就可以使用Test來實際調用動態庫Test32.dll中的Test1了。 

我在實際應用中 

有時也這樣使用動態庫,帶修飾的函數名雖然有些複雜古怪,但它本身能夠表達更多的 

可用信息。 
 

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