深入理解動態庫

深入理解動態庫
    一、動態連接庫的用途
    動態連接庫,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;  file://爲簡單起見,這裏用直接賦值替代"複雜計算"的結果
       AppShow(flag);  file://回調
     }
    }
這裏使用.DEF文件輸出函數。添加下列內容到.DEF文件:
    LIBRARY     TEST32
    DESCRIPTION 'TEST32.DLL'
    EXPORTS
       TestShow           @1
將調用約定設置爲__stdcall,編譯生成Test32.dll,將其拷入系統目錄。
    最後運行上面編寫的VB5項目。OK?!

    實驗一:將調用約定改爲缺省設置,即C調用約定,其它不變,重新編譯生成Test32.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調用約定編譯時,實驗正常通過。而當動態庫用__cdecl調用約定編譯時,實驗一和實驗二的現象能很好地說明問題,其實此時由於調用約定的不統一,用於傳送參數的棧已遭到破壞,現象就是工作不正常。實驗三中雖然仍用__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了。我在實際應用中有時也這樣使用動態庫,帶修飾的函數名雖然有些複雜古怪,但它本身能夠表達更多的可用信息。

 

發佈了61 篇原創文章 · 獲贊 12 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章