動態鏈接庫(Dynamic Link Library)學習筆記(附PE文件分析)


作者:EricYou 轉載請注明出處
 

         注:本文所寫的動態鏈接庫指傳統的DLL,並非是.NET中的Assembly.

         我對動態鏈接和動態鏈接庫的概念並不陌,但一直以來就停留在概念的層面上,沒有更深入的了解。今天抽空看了一下有關動態鏈接和動態鏈接庫的文章,有了一些新的認識,當然不能忘了寫在這里。那么現在就開始...

什么是動態鏈接和動態鏈接庫

 

         動態鏈接(Dynamic Linking)是相對於靜態鏈接(Static Linking)而言的。程序設計中,為了能做到代碼和模塊的重用,程序設計者常常將常用的功能函數做成庫,當程序需要實現某種功能時,就直接調用庫文件中的函數,從而實現了代碼的重用。早期的程序設計中,可重用的函數模塊以編譯好的二進制代碼形式放於靜態庫文件中,在MS的操作系統中是Lib為后綴的文件。程序編寫時,如果用戶程序調用到了靜態庫文件中的函數,則在程序編譯時,編譯器會自動將相關函數的二進制代碼從靜態庫文件中復制到用戶目標程序,與目標程序一起編譯成可執行文件。這樣做的確在編碼階段實現了代碼的重用,減輕了程序設計者的負擔,但並未在執行期實現重用。如一個程序a.exe使用了靜態庫中的 f() 函數,那么當a.exe有多個實例運行時,內存中實際上存在了多份f()的拷貝,造成了內存的浪費。
        隨着技術的進步,出現了新的鏈接方式,即動態鏈接,從根本上解決了靜態鏈接方式帶來的問題。動態鏈接的處理方式與靜態鏈接很相似,同樣是將可重用代碼放在一個單獨的庫文件中(在MS的操作系統中是以dll為后綴的文件,Linux下也有動態鏈接庫,被稱為Shared Object的so文件),所不同的是編譯器在編譯調用了動態鏈接庫的程序時並不將庫文件中的函數執行體復制到可執行文件中,而是只在可執行文件中保留一個函數調用的標記。當程序運行時,才由操作系統將動態鏈接庫文件一並加載入內存,並映射到程序的地址空間中,這樣就保證了程序能夠正常調用到庫文件中的函數。同時操作系統保證當程序有多個實例運行時,動態鏈接庫也只有一份拷貝在內存中,也就是說動態鏈接庫是在運行期共享的。
        使用動態鏈接方式帶來了幾大好處:首先是動態鏈接庫和用戶程序可以分開編寫,這里的分開即可以指時間和空間的分開,也可以指開發語言的分開,這樣就降低了程序的耦合度;其次由於動態鏈接獨特的編譯方式和運行方式,使得目標程序本身體積比靜態鏈接時小,同時運行期又是共享動態鏈庫,所以節省了磁盤存儲空間和運行內存空間;最后一個是增加了程序的靈活性,可以實現諸如插件機制等功能。用過winamp的人都知道,它的很多功能都是以插件的形式提供的,這些插件就是一些動態鏈接庫,主程序事先規定好了調用接口,只要是按照規定的調用接口寫的插件,都能被winamp調用。
        WIndow 95、98、NT系列等系統都提供了動態鏈接庫的功能,並且這些操作系統的系統調用大多都是通過動態鏈接庫實現的,最常見的NT系列OS中的KENEL32.dll,USER32.dll,GDI32.dll等動態鏈接庫文件就包含了大量的系統調用。在windows家族中,NT內核的操作系統在動態鏈接庫機制上較之前的95、98系統要更安全。95、98系統在程序調用動態鏈接庫時,將動態鏈接庫加載到2G-3G之間的被稱為進程共享空間的虛擬地址空間,並且所有進程關於這1G的虛擬地址空間的頁表都是相同的,也就是說對於所有的進程,這片共享區的頁表都指向同一組物理頁,這樣一來,加載入內存的的動態鏈接庫對所有正在運行的進程都是可見的。如果一個動態鏈接庫被其中一個進程更改,或其自身崩潰,將影響到所有調用它的進程,如果該動態鏈接庫是系統的動態鏈接庫,那么將導致系統的崩潰。在Windows NT系統中,動態鏈接庫被映射到進程的用戶地址空間中,並用Copy On Write機制保證動態鏈接庫的共享安全,Copy On Write可以理解為寫時拷貝。一般情況下,多個運行的進程還是按原來的模式共享同一個動態鏈接庫,直到有進程需要向動態鏈接庫的某個頁面寫數據時,系統將該頁做一個拷貝,並將新復制頁面的屬性置為可讀可寫,最后修改進程的頁表使之指向新拷貝的物理頁。這樣無論該進程怎么修改此頁的數據,也不會影響到其他調用了此動態鏈接庫的進程了。


Windows下動態鏈接庫的編寫

 

         因為本人對linux沒有太多研究,所以這里只介紹windwos環境下動態鏈接庫的編寫。
         在VC中新建一個空的Win32動態鏈接庫工程( Win32 Domanic Library),然后添加一個C++ Sourse File到工程,我這里的文件名取DllTest.cpp。然后在文件中添加如下內容:
 //DllTest.cpp
 
 _declspec(dllexport) int add(int a,int b)
 {
  return a+b;
 }
 
 _declspec(dllexport) int subtract(int a,int b)
 {
  return a-b;
 }
        接下來編譯鏈接,就會在debug目錄下生成一個調試版本的動態鏈接庫,該鏈接庫包含了add和subtract兩個可供外部調用的函數。我們注意到,在源文件中多了一個沒有見過的語句 _declspec(dllexport) ,這個語句的作用就是向編譯器指出我需要在生成的動態鏈接庫中導出的函數,沒有導出的函數是不能被其他程序調用的。要知道一個動態鏈接庫導出了什么函數,可以在命令提示行用命令"dumpbin -exports DllTest.dll"來查看(也可以用VC工具包中的depends使用程序來查看)。以下是用dumpbin命令查看DllTest.dll而生成的信息:

 

Dump of file DllTest.dll
 
File Type: DLL
 
  Section contains the following exports for DllTest.dll
 
           0 characteristics
    4420BEA4 time date stamp Wed Mar 22 11:04:04 2006
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names
 
    ordinal hint RVA      name
 
          1    0 0000100A ?add@@YAHHH@Z
          2    1 00001005 ?subtract@@YAHHH@Z
 
  Summary
 
        7000 .data
        1000 .idata
        3000 .rdata
        2000 .reloc
       2A000 .text
 
        可以看到,我們編寫的動態鏈接庫導出了兩個函數,分別名為 ?add@@YAHHH@Z 和  ?subtract@@YAHHH@Z,為什么名字不是 addsubtract呢?這是因為 C++為了支持函數的重載,會在編譯時將函數的參數類型信息以及返回值類型信息加入到函數名中,這樣代碼中名字一樣的重載函數,在經過編譯后就互相區分開了,調用時函數名也經過同樣的處理,就能找到對應的函數了。編譯器對函數的重命名規則是與調用方式相關的,在這里采用的是 C++的默認調用方式。以此對應的還有 stdcall方式、 cdecl方式、 fastcall方式和 thiscall方式,不同調用方式的重命名規則不一樣。
        需要特別說一下的是 stdcall方式和 cdecl方式:
         stdcall方式(標准調用方式)也即 pascal調用方式,它的重命名規則是函數名自動加前導的下划線,后面緊跟一個@符號,其后緊跟着參數所占字節數,之所以要跟參數字節數,是因為 stdcall采用被調函數平衡堆棧方式,用函數名最后的數字告訴編譯器需要為函數平衡的字節數。例如,如果我們的DllTest.dll采用 stdcall方式編譯的話,導出的函數名將會是  _add@8 和  _subtract@8 ,而函數編譯后的匯編代碼最后一句一定是 ret8。
         cdecl方式即C語言調用方式,它的重命名規則僅僅是在函數名前加下划線(奇怪的是我用vc6編譯的c語言函數,名字沒有任何改變),因為C語言采用的是調用函數平衡堆棧的方式,所以不需要在函數名中加入參數所占的字節數,這樣的堆棧平衡方式也使C語言可以編寫出參數不固定的函數;同時C語言不支持函數重載,因此不需要在函數名中加入參數類型信息和返回值類型信息。
        
        動態鏈接庫已經生成了,接下來就是調用的工作了。調用動態鏈接庫有兩種方式:隱式調用和顯式調用,下面我們分別來看兩種調用方式的具體過程:

 

動態鏈接庫的隱式調用

 

        新建一個空的Win32 Console Application,命名為DllCaller,向工程中添加名為DllCaller.cpp 的C++ Sourse File,在文件中寫入如下代碼:

 

#include <iostream>
using namespace std;

 

//extern int add(int a,int b);
_declspec(dllimport) int add(int a,int b);

 

int main()
{
         cout<<"3+5="<<add(3,5)<<endl;
         return 1;
}

 

        編譯,沒有錯誤,鏈接,有兩個錯誤:找不到外部引用符號。要怎樣才能讓我們的程序找到動態連接庫中的函數呢?這里是關鍵的一步。到剛才的DllTest工程目錄下,從debug文件夾中拷貝生成的DllTest.dll文件和DllTest.lib文件到DllCaller工程目錄。然后依次在vc中選擇菜單:Project -->Settings-->Liink, 在Object/library Modules中加入一項文件名:DllTest.lib,這里的DllTest.lib並不是靜態庫文件,而是DllTest.dll的導入庫文件,它包含了DllTest.dll動態鏈接庫導出的函數信息,只有在工程鏈接設置里添加了該文件,才能夠使調用了該動態鏈接庫的工程正確鏈接。完成以上步驟后,我們再編譯鏈接工程,這次沒有任何錯誤!程序可以順利調用動態連接庫文件,正常運行了(為了能使程序找到並加載需要的動態鏈接庫,動態鏈接庫文件必須與調用程序在同一個目錄下,或在path環境變量指定的目錄下)。
        這里需要說明一點,工程中的源文件在調用動態鏈接庫中的函數時,需要提前聲明,聲名有兩種方式,一種是傳統的 extern方式,一種是 _declspec(dllimport)方式,這兩種方式在代碼中我都給出了。其中,第二種方式能使編譯過程更快,所以推薦使用。

 

動態鏈接庫的顯式調用

 

        比起隱式調用,顯示調用更加靈活,而且在編譯鏈接時不需要lib導入庫文件,也不需要提前聲明函數。我們通過windows提供的API函數來動態加載動態連接庫並調用其中的函數,用完后可以馬上釋放內存中的動態鏈接庫,十分方便。下面就是顯示調用動態鏈接庫的代碼:

 

#include <iostream>
#include <windows.h>
using namespace std;

 

int main()
{
         HINSTANCE hInstance=LoadLibrary("DllTest.dll");
         typedef int (*AddProc)(int,int);
         AddProc Add=(AddProc)GetProcAddress(hInstance,?add@@YAHHH@Z);
         if(!Add)
         {
                  cout<<"動態連接庫庫函數未找到"<<endl;
                  return 0;
         }
         cout<<"3+5="<<Add(3,5)<<endl;
         FreeLibrary(hInstance);
         return 1;
}
        以上代碼並不復雜,首先定義一個實例句柄用來引用由 Windows API 函數 LoadLibrary加載的動態鏈接庫, LoadLibrary函數的參數是一個字符串指針,具體調用時我們需要填入需要加載的動態鏈接庫的位置及文件名,加載成功后返回一個實例句柄。接下來我們定義一個函數指針類型,用該類型聲明一個函數指針,用來存儲 GetProcAddress函數返回的動態庫函數入口地址。 GetProcAddress能從指定的動態庫中查找指定名字的函數,如果查找成功則返回該函數的入口地址,如果失敗則返回NULL。更多 GetProcAddress函數的用法請參看MSDN。有人可能注意到, GetProcAddress函數中指定的函數名並不是 add,而是 ?add@@YAHHH@Z。這里就和前面將的函數調用方式聯系起來了,在 GetProcAddress函數中,我們指定的函數名必須是編譯后經過重命名的函數名,而不是源文件中定義的函數名。這樣實際上給我們的調用帶來了相當大的麻煩,因為我們不可能去了解每一個經過重命名的導出函數名。好在微軟已經給出了解決方法,那就是在編寫動態鏈接庫時同時編寫一個以 def為后綴的編譯命名參考文件,如果動態鏈接庫工程中有該文件,則編譯器會根據該文件指定的函數名來導出動態庫函數,關於 def文件的詳細使用方法請參考 MSDN,這里就不一一贅述。找到需要的動態庫函數后,我們就可以按需要對它進行調用,之后調用 FreeLibrary函數釋放動態庫。因為動態庫是多進程共享的,因此調用 FreeLibrary函數並不意味着動態庫在內存中被釋放,每個動態庫都有一個變量用來記錄它的共享引用計數,而 FreeLibrary的功能只是將這個記數減一,只有當一個動態庫的引用計數為0時,它才會被操作系統釋放。
       

 

隱式調用與顯式調用的對比

 

         前面已經詳細介紹了動態鏈接庫的兩種調用方法,相比之下,隱式調用在編程時比較簡單,指定導入庫文件后,不必考慮函數的重命名,就可以直接調用動態庫函數。但由於隱式調用不能指定動態庫的加載時機,因此在一個程序開始運行時,操作系統會將該程序需要的動態鏈接庫都加載入內存,勢必造成程序初始化的時間過長,影響用戶體驗。而顯式調用采用動態加載的方法,用到什么加載什么,用完即釋放,靈活性較高,可以使程序得到優化。具體運用中到底采用哪種方法,還要依實際情況而定。
 
http://qimo601.iteye.com/blog/1397935


免責聲明!

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



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