动态链接概述
目錄
動態鏈接庫概述
相關函數
動態鏈接庫編程
dumpbin工具
(本章節中例子都是用 VS2005 編譯調試的)
動態鏈接概述
?
說明
所謂動態鏈接,就是把一些經常會共用的代碼(靜態鏈接的OBJ程序庫)制作成DLL檔,當可執行文件調用到DLL檔內的函數時,windows操作系統才會把DLL檔加載存儲器內,DLL檔本身的結構就是可執行文件,當程序需求函數才進行鏈接.通過動態鏈接方式,存儲器浪費的情形將可大幅降低.
DLL的文檔格式與視窗EXE文檔一樣——也就是說,等同于32位視窗的可移植執行文檔(PE)和16位視窗的New Executable(NE).作為EXE格式,DLL可以包括源代碼、數據和資源的多種組合.
在使用動態庫的時候,往往提供兩個文件:一個引入庫(LIB)和一個動態鏈接庫(DLL).引入庫(LIB)包含被動態連接庫(DLL)所導出的函數和變量的符號名,動態連接庫(DLL)包含實際的函數和數據.在編譯鏈接可執行文件時,只需要鏈接引入庫(LIB),動態連接庫(DLL)中的函數代碼和數據并不復制到可執行文件中,在運行的時候,再去加載DLL,訪問動態鏈接庫(DLL)中導出的函數.
動態鏈接庫(DLL)通常都不能直接運行,也不能接收消息.它們是一些獨立的文件,其中包含能被可執行程序或其它動態連接庫(DLL)調用來完成某項工作的函數.只有在其它模塊調用動態鏈接庫(DLL)中的函數時,它才發揮作用.但是動態連接庫(DLL)被多進程調用時候,動態鏈接庫(DLL)中進程訪問到動態鏈接庫(DLL)的成員時,系統會為它開辟一個新的數據成員頁面給訪問進程提供單獨的動態連接庫(DLL)數據區.
特征(來自維基百科此處為鏈接)
- 內存管理
在Win32中,DLL文檔按照片段(sections)進行組織.每個片段有它自己的屬性,如可寫或是只讀、可執行(代碼)或者不可執行(數據)等等.
DLL代碼段通常被使用這個DLL的進程所共享;也就是說它們在物理內存中占據一個地方,并且不會出現在頁面文檔中.如果代碼段所占據的物理內存被收回,它的內容就會被放棄,后面如果需要的話就直接從DLL文檔重新加載.
與代碼段不同,DLL的數據段通常是私有的;也就是說,每個使用DLL的進程都有自己的DLL數據副本.作為選擇,數據段可以設置為共享,允許通過這個共享內存區域進行進程間通信.但是,因為用戶權限不能應用到這個共享DLL內存,這將產生一個安全漏洞;也就是一個進程能夠破壞共享數據,這將導致其它的共享進程異常.例如,一個使用訪客賬號的進程將可能通過這種方式破壞其它運行在特權賬號的進程.這是在DLL中避免使用共享片段的一個重要原因.
當DLL被如UPX這樣一個可執行的packer壓縮時,它的所有代碼段都標記為可以讀寫并且是非共享的.可以讀寫的代碼段,類似于私有數據段,是每個進程私有的并且被頁面文檔備份.這樣,壓縮DLL將同時增加內存和磁盤空間消耗,所以共享DLL應當避免使用壓縮DLL. - 符號解析和綁定
DLL輸出的每個函數都由一個數字序號唯一標識,也可以由可選的名字標識.同樣,DLL引入的函數也可以由序號或者名字標識.對于內部函數來說,只輸出序號的情形很常見.對于大多數視窗API函數來說名字是不同視窗版本之間保留不變的;序號有可能會發生變化.這樣,我們不能根據序號引用視窗API函數.
按照序號引用函數并不一定比按照名字引用函數性能更好:DLL輸出表是按照名字排列的,所以對半查找可以用來在在這個表中根據名字查找這個函數.另外一方面,只有線性查找才可以用于根據序號查找函數.
將一個可執行文件綁定到一個特定版本的DLL也是可能的,這也就是說,可以在編譯時解析輸入函數(imported functions)的地址.對于綁定的輸入函數,連結工具保存了輸入函數綁定的DLL的時間戳和校驗和.在運行時Windows檢查是否正在使用同樣版本的庫,如果是的話,Windows將繞過處理輸入函數;否則如果庫與綁定的庫不同,Windows將按照正常的方式處理輸入函數.
綁定的可執行文件如果運行在與它們編譯所用的環境一樣,函數調用將會較快,如果是在一個不同的環境它們就等同于正常的調用,所以綁定輸入函數沒有任何的缺點.例如,所有的標準Windows應用程序都綁定到它們各自的Windows發布版本的系統DLL.將一個應用程序輸入函數綁定到它的目的環境的好機會是在應用程序安裝的過程. - 運行時顯式鏈接
對每個DLL來說,Windows存儲了一個全局計數器,每多一個進程使用便多額外一個.LoadLibrary與FreeLibrary指令影響每一個進程內含的計數器;動態鏈接則不影響.因此借由調用FreeLibrary多次,從存儲器反加載一DLL是很重要的.一個進程可以從它自己的VAS注銷此計數器.
DLL文檔能夠在運行時使用LoadLibrary(或者LoadLibraryEx)API函數進行顯式調用,這個的過程微軟簡單地稱為運行時動態調用.API函數GetProcAddress根據查找輸出名稱符號、FreeLibrary卸載DLL.這些函數類似于POSIX標準API中的dlopen、dlsym、和dlclose.
注意微軟簡單稱為運行時動態鏈接的運行時隱式鏈接,如果不能找到鏈接的DLL文檔,Windows將提示一個錯誤消息并且調用應用程序失敗.應用程序開發人員不能通過編譯鏈接來處理這種缺少DLL文檔的隱式鏈接問題.另外一方面,對于顯式鏈接,開發人員有機會提供一個完善的出錯處理機制.
運行時顯式鏈接的過程在所有語言中都是相同的,因為它依賴于Windows API而不是語言結構.
與靜態鏈接庫的區別
靜態庫本身就包含了實際執行代碼、符號表等等,而對于導入庫而言,其實際的執行代碼位于動態庫中,動態鏈接庫只包含了地址符號表等,確保程序找到對應函數的一些基本地址信息.
Windows 下 3 個重要的 DLL
Windows API中的所有函數都包含在DLL中。其中有3個最重要的DLL
- Kernel32.dll,它包含用于管理內存、進程和線程的各個函數
- User32.dll,它包含用于執行用戶界面任務(如窗口的創建和消息的傳送)的各個函數
- GDI32.dll,它包含用于畫圖和顯示文本的各個函數
動態鏈接庫的優點
- 可以采用多種編程語言來編寫
我們可以采用自己熟悉的開發語言編寫DLL,然后由其他語言編寫的可執行程序來調用這些DLL.例如,可以利用VB來編寫程序界面,然后利用VC++或Delphi編寫的完成程序作業邏輯的DLL
- 增強產品的功能
在發布產品時,可以發布產品功能實現的動態鏈接庫規范,讓其他公司或個人遵照這種規范開發自己的DLL,以取代產品原有的DLL.讓產品調用新的DLL,從而實現功能的增強,在實際工作中,我們看到許多產品都提供了界面插件功能,允許用戶動態地更換程序的界面,這就可以通過更換界面DLL來實現
- 提供二次開發的平臺
在銷售產品時,可以采用DLL的形式提供一個二次開發的平臺,讓用戶可以利用該DLL調用其中實現的功能,編寫符合自己業務需要的產品,從而實現二次開發
- 簡化項目管理
在一個大型項目開發中,通常都是由多個項目小組同時開發,如果采用串行開發,則效率非常低的,我們可以將項目細分,將不同功能交由各個項目小組以多個DLL方式實現,這樣各個項目小組就可以同時進行開發了
- 可以節省磁盤空間和內存
如果多個應用程序需要訪問同樣的功能,那么可以將該功能以DLL的形式提供,這樣在機器上只需要存在一份該DLL文件就可以了,從而節省了磁盤空間.另外如果多個應用程序使用同一個DLL,該DLL的頁面只需要放入內存一次,所有的應用程序就都可以共享它的頁面了.這樣,內存的使用將更加有效
如下圖所示就是一個動態鏈接庫被兩個進程調用時的內存示意圖,當進程被加載時,系統為它分配4GB的地址空間,接著分析該可執行模塊,找到該程序要調用那些DLL模塊,然后系統搜索這些DLL,找到后就加載它們,并為它們分配虛擬的內存空間,最后將DLL的頁面映射到進程的地址空間.,從此可以導致多個程序中共享DLL的同一份代碼,這樣就可以節省空間 - 有助于資源的共享
DLL可以包含對話框模板,字符串,圖標和位圖等多種資源,多個應用程序可以使用DLL來共享這些資源.在實際工作中,可以寫一個純資源的動態鏈接庫文件,供其他應用程序訪問
- 有助于實現應用程序的本地化
如果產品需要提供多語言版本,那么就可以使用DLL來支持多語言,可以為每種語言創建一個只支持這種語言的動態鏈接庫
相關函數
顯示相關函數?
編寫動態連接庫
本程序使用的編譯環境是VS2005,如果是使用VC6.0環境的可以去網上找孫鑫關于VC的視頻,視頻第19講講的就是動態連接庫編寫.
[編寫動態鏈接庫][加載動態鏈接庫][C++命名改編][調用約定]
?
編寫動態鏈接庫
建立DLL項目
- Win32 -> Win32項目 -> DLL(D)
- MFC -> MFC DLL
生成導出函數,類,成員函數
- 生成導出函數: 在函數前添加_declspec(dllexport)[導出標示符]生成導出函數,
- 生成導出類: 在class后類名前添加_declspec(dllexport)[[導出標示符]這樣就可以導出整個類,但是訪問類的函數時候,仍然受限于函數自身的范圍權限.也就是說,如果該函數訪問權限是private或protect的話,那么外部程序仍然無法訪問這個函數
- 生成導出成員函數: 在成員函數的返回類型后在函數的函數名前面加_declspec(dllexport)
代碼示例(例子鏈接)
?
加載動態連接庫
隱式鏈接方式加載動態庫(例子鏈接)
步驟:
- 加載dll.lib文件
- VC6.0: 點擊 Project/Settings后對話框下Link的Object/library modules下添加dll的lib文件
- VS/VC6.0: ? ?在文件中利用#pragma comment(lib,"鏈接庫地址")
- 加載dll.dll文件?
把dll文件放在下面路徑的一種中- 程序目錄
- 當前目錄
- 系統目錄
- path環境變量中列出的路徑中
- 利用extern / _declspec(dllimport)[導出標示符]聲明動態鏈接庫的函數
- _declspec(dllimport) 可以調用dll中非導出函數[沒有_declspec(dllexport)的函數]
- extern ?只能調用dll中導出函數[有_declspec(dllexport)的函數]
流程圖:
顯示加載方式加載DLL(例子鏈接)
步驟:
- 將最新的dll文件復制到以下路徑中
- 程序目錄
- 當前目錄
- 系統目錄
- path環境變量中列出的路徑中
- 調用LoadLibrary,加載動態庫
- 聲明需要的動態鏈接的函數指針類型(此處可以要可以不要,這樣只是方便以后定義相關函數指針) //例如下面要調用動態鏈接庫中的 int max_dll(int a,int b) 函數 //所以需要定義相關類型的指針聲明 typedef int (/*_stdcall*/ *INTMAX)(int a,int b);//以后就可以用INTMAX來定義相關指針如 INTMAX pMaxInt;
- 獲得函數地址GetProcAddress
- 調用FreeLibrary釋放動態鏈接庫
流程圖:
兩種加載方式的比較
動態加載和隱式鏈接這兩種加載DLL的方式各有優點.如果采用動態加載的方式,那么可以在需要加載時才加載DLL.而隱式鏈接方式實現起來比較簡單,在編寫客戶端代碼時就可以把鏈接工作做好,在程序中可以隨時調用DLL導出的函數,但是訪問十多個DLL,如果都采用隱式鏈接的方式鏈接加載他們的話,那么在啟動程序時候,這些DLL都需要加載到內存中,并映射到調用進程的地址空間,這樣將加大啟動程序的時間,而且,一般來說,在程序運行過程中只是在某個條件滿足時候,這時才會訪問某個DLL中的某個函數,其他情況下都不需要訪問這些DLL,但是,這時所有的DLL都被已經加載到內存中,資源浪費會比較嚴重,這種情況下,就可以采用動態加載DLL技術,也就是說,在需要時,DLL才會被加載到內存中,并被映射到進程的地址空間中,有一點需要說明的是,實際上,采用隱式鏈接的方式訪問DLL時,在啟動時也是通過調用LoadLibrary函數加載到該進程需要的動態鏈接庫的
?
代碼示例
動態連接庫源程序代碼
程序源碼
View Code隱式調用動態鏈接
程序源碼
View Code運行結果
顯示調用動態鏈接庫
程序源碼
View Code運行結果
dumpbin 工具
[概述][使用步驟][C++命名改編][調用約定]
?
概述
用途
查看dll與exe相關導入導出函數信息
dumpbin程序的文件位置
- VC6.0: VC98 \ bin
- VS2005: Microsoft Visual Studio 8\VC\bin
相關參數
- -exports 文件名.dll ? (查看導出函數和導出類)
- -imports 文件名.exe (查看導入函數)
設置VC++使用環境信息
VCVAR32.bat 建立VC++使用環境信息,但是注意當在命令行界面下執行VCVARS32.bat文件后,該文件所設置的環境信息只是在當前命令行窗口生效.如果關閉該窗口,再次啟動一個新的命令行窗口后,仍需要運行VCVAR32.bat文件
?
使用步驟
- 以上面的DLL源程序為例子,做個使用說明,先打開控制臺(如下圖)
- 然后,把建立VC++使用環境信息,把VCVAR32.bat(批處理的位置和dumpbin一樣)拖到控制臺界面中(如下圖).
- 然后回車,便建立好了VC++使用環境信息(如下圖).這樣就可以是用 dumpbin 這個命名了.
- 然后切換到工程目錄中的 debug 或 release 中去.(如下圖)
- 接著用 dumpbin 查看工程生成的 dll.dll 動態鏈接庫的導出函數(如下圖)(注意:這個函數必須有_declspec(dllexport)修飾,否則在這里是看不到).
流程圖如下:
?
C++名字改編(C++名字粉碎)
在上面使用 dumpbin 程序查看 dll.dll 的導出函數發現函數名有點奇怪,我們定義的函數名max_dll兩個重載函數名在這里變成了 ?max_dll@@YAHHH@Z?與 ?max_dll@@YANNN@Z,因為C++支持函數重載,對于重載的多個函數來說,其函數名都是一樣的,為了加以區分,在編譯連接時,C++會按照自己的規則篡改函數名字,這一過程為"名字改編".有的書中也稱為"名字粉碎".不同的C++編譯器會采用不同的規則進行名字改編,這個的話,利用不同的C++編譯器生成的程序在調用對方提供的函數時,可能會出現問題
解決名字改變問題
第一種
- 聲明:
- 在定義導出函數時,需要加上限定符: extern"C" (雙引號中的C一定要大寫)
- 注意:
- 使動態鏈接庫文件在鏈接時,導出函數的函數名稱不發生改變.
- 利用 extern "C" 可以解決 C++ 和 C 語言之間相互調用時函數命名的問題.但是這種方法有一個缺陷,就是不能用于導出一個類成員函數,只能導出全局函數這種情況
- 如果導出函數的調用約定發生了改變,那么即使使用了限定符: extern "C" ,該函數的名字仍然會發生改編.
- 記得導出和導入都要加入 extern "C" 否則在調用動態鏈接庫時候會發生找不到函數這個現象.
代碼樣例:
利用 extern"C" 解決名字改編
動態連接庫程序源碼
View Code主程序源碼
View Code利用 dumpbin 查看命名
?
調用約定
四種調用方式:
__cdecl
__cdecl調用約定又稱為 C 調用約定,是 C/C++ 語言缺省的調用約定。參數按照從右至左的方式入棧,函數本身不清理棧,此工作由調用者負責,返回值在EAX中。由于由調用者清理棧,所以允許可變參數函數存在,如int sprintf(char* buffer,const char* format,...);。
__stdcall
__stdcall 很多時候被稱為 pascal 調用約定。pascal 語言是早期很常見的一種教學用計算機程序設計語言,其語法嚴謹。參數按照從右至左的方式入棧,函數自身清理堆棧,返回值在EAX中。
__fastcall
顧名思義,__fastcall 的特點就是快,因為它通過 CPU 寄存器來傳遞參數。他用 ECX 和 EDX 傳送前兩個雙字(DWORD)或更小的參數,剩下的參數按照從右至左的方式入棧,函數自身清理堆棧,返回值在 EAX 中。
__thiscall
這是 C++ 語言特有的一種調用方式,用于類成員函數的調用約定。如果參數確定,this 指針存放于 ECX 寄存器,函數自身清理堆棧;如果參數不確定,this指針在所有參數入棧后再入棧,調用者清理棧。__thiscall 不是關鍵字,程序員不能使用。參數按照從右至左的方式入棧。
相關鏈接:
- C/C++函數調用約定
- 剖析VC++函數調用約定
代碼示例(編譯環境VS2005):
使用 extern "C" 時
動態連接庫源碼:
View Code用 dumpbin 查看導出函數:
未使用 extern "C" 時
動態連接庫源碼:
View Code用 dumpbin 查看導出函數:
?
總結
- 上一篇: C语言的头文件和库文件(函数库)
- 下一篇: Linux下动态链接库调用