動態連接庫概述
相關函數
動態連接庫編程
dumpbin工具
html
(本章節中例子都是用 VS2005 編譯調試的)ios
說明程序員
所謂動態連接,就是把一些常常會共用的代碼(靜態連接的OBJ程序庫)製做成DLL檔,當可執行文件調用到DLL檔內的函數時,windows操做系統纔會把DLL檔加載存儲器內,DLL檔自己的結構就是可執行文件,當程序需求函數才進行連接.經過動態連接方式,存儲器浪費的情形將可大幅下降.編程
DLL的文檔格式與視窗EXE文檔同樣——也就是說,等同於32位視窗的可移植執行文檔(PE)和16位視窗的New Executable(NE).做爲EXE格式,DLL能夠包括源代碼、數據和資源的多種組合.windows
在使用動態庫的時候,每每提供兩個文件:一個引入庫(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
動態連接庫的優勢
咱們能夠採用本身熟悉的開發語言編寫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來支持多語言,能夠爲每種語言建立一個只支持這種語言的動態連接庫
1.動態連接庫入口函數 DllMain (在加載動態鏈接庫時候會自動被調用,做用如控制檯的main函數,窗體程序的WinMain函數) 函數原型 BOOL WINAPI DllMain(HINSTANCE hinstDLL,DWORD fdwReason,LPVOID lpvReserved);
參數說明 hinstDLL
動態連接庫模塊句柄.當DLL初次被加載時,它的句柄會經過此參數傳遞進來,就好像WinMain函數有一個當前實例句柄參數同樣,所以,在編寫DLL程序時,若是這些函數須要用到當前的DLL模塊句柄,那麼 就能夠爲該DLL提供DllMain函數,而後將經過參數hinstDLL傳遞進來的模塊句柄保存到一個全局變量中,供其餘函數使用 fdwReason
一個標記值,用來調用該DLL入口函數的緣由.該參數的取值是下列值之一 值 說明 DLL_PROCESS_ATTACH 當進程第一次加載DLL並調用DllMain函數 DLL_THREAD_ATTACH 當前進程正建立一個新線程 DLL_THREAD_DETACH 線程結束 DLL_PROCESS_DETACH 線程結束 lpvReserved
保留參數.不需關心此參數,但能夠檢測這個指針,若是DLL被動態加載,則此參數爲NULL,若是是靜態加載,則此參數非NULL值 說明 若是提供了DllMain函數,那麼在此函數中不要進行太複雜的調用.由於在動態連接庫時,可能還有一些核心動態連接庫沒有加載.例如,咱們本身編寫的某個DLL被加載時,user32.dll或GDI32.dll這 兩個核心動態連接庫尚未被加載.前面的內容已經介紹了,程序在運行時,加載程序會依次加載該進程須要的dll,而咱們本身編寫的DLL可能會比較靠前地被加載,若是本身編寫的DllMain函數須要調用 這些核心的DLL中的某個函數的話,這時就會失敗,從而致使程序終止 2.加載動態庫 LoadLibrary 函數原型 HMODULE LoadLibrary(LPCTSTR lpFileName);
參數說明 lpFileName: 一個字符串類型參數,該參數指定了可執行模塊的名稱,既能夠是一個.dll文件,也能夠是一個.exe文件.
返回值 若是調用成函數返回所加載的那個模塊的句柄.該函數的返回類型是HMODULE(和HINSTANCE類型能夠通用) 說明 該函數不只能夠加載DLL(.dll),還能夠加載可執行模塊(.exe),通常來講,當可加載可執行模塊時,主要是爲了訪問該模塊內的一些資源,例如對話框資源,位圖資源或圖標資源等. 3.得到動態庫的函數地址 GetProcAddress 函數原型 FARPROC GetProcAddress(HMODULE hModule,LPCSTR lpProcName);
參數說明 hModule: 指定動態連接庫模塊的句柄,即LoadLibrary函數的返回值 lpProcName: 一個指向常量的字符指針(必須是改編後C++函數的名字),指定DLL導出函數的名字或者函數的序號(函數名和函數序號能夠由dumpbin工具去查看),這裏應該注意,若是參數指定的 是導出函數的序號,那麼序號必須在低字節中,高字節必須是0(能夠用MAKEEINTRESOURCE宏,該宏會吧指定的函數序列號轉換爲相應的函數名字字符串)
返回值 成功返回函數的函數地址,失敗的話返回NULL 4.釋放函數動態鏈接庫連接 FreeLibrary 函數原型 BOOL FreeLibrary( HMODULE hLibModule);
參數說明 hLibModule: 指向鏈接庫的句柄
5.得到動態鏈接庫句柄 GetModuleHandle 函數原型 HMODULE GetModuleHandle ( LPCTSTR lpModuleName);
參數說明 plModuleName:一個指字符串用於表示模塊名字,一個動態鏈接庫(name.dll)或者執行文件的名字(name.exe),若沒有加後綴默認是動態鏈接庫即系統會在幫你加上後綴.dll .
返回值 若函數成功返回對應句柄,若失敗返回空
本程序使用的編譯環境是VS2005,若是是使用VC6.0環境的能夠去網上找孫鑫關於VC的視頻,視頻第19講講的就是動態鏈接庫編寫.
[編寫動態連接庫][加載動態連接庫][C++命名改編][調用約定]
創建DLL項目
生成導出函數,類,成員函數
代碼示例(例子連接)
隱式連接方式加載動態庫(例子連接)
步驟:
流程圖:
顯示加載方式加載DLL(例子連接)
步驟:
//例以下面要調用動態連接庫中的 int max_dll(int a,int b) 函數 //因此須要定義相關類型的指針聲明 typedef int (/*_stdcall*/ *INTMAX)(int a,int b); //之後就能夠用INTMAX來定義相關指針如 INTMAX pMaxInt;
流程圖:
兩種加載方式的比較
動態加載和隱式連接這兩種加載DLL的方式各有優勢.若是採用動態加載的方式,那麼能夠在須要加載時才加載DLL.而隱式連接方式實現起來比較簡單,在編寫客戶端代碼時就能夠把連接工做作好,在程序中能夠隨時調用DLL導出的函數,可是訪問十多個DLL,若是都採用隱式連接的方式連接加載他們的話,那麼在啓動程序時候,這些DLL都須要加載到內存中,並映射到調用進程的地址空間,這樣將加大啓動程序的時間,並且,通常來講,在程序運行過程當中只是在某個條件知足時候,這時纔會訪問某個DLL中的某個函數,其餘狀況下都不須要訪問這些DLL,可是,這時全部的DLL都被已經加載到內存中,資源浪費會比較嚴重,這種狀況下,就能夠採用動態加載DLL技術,也就是說,在須要時,DLL纔會被加載到內存中,並被映射到進程的地址空間中,有一點須要說明的是,實際上,採用隱式連接的方式訪問DLL時,在啓動時也是經過調用LoadLibrary函數加載到該進程須要的動態連接庫的
動態鏈接庫源程序代碼
程序源碼
// .h 頭文件 ----------------------------------------------------- #ifndef DLL_API #define DLL_API _declspec(dllimport) #endif DLL_API int max_dll(int a,int b); DLL_API double max_dll(double a,double b); class DLL_API testClass{ public: testClass(); int getValue(); private: int value; }; // .cpp 源程序 --------------------------------------------------- #define DLL_API _declspec(dllexport) #include "test.h" int max_dll(int a,int b){ return a>b?a:b; } double max_dll(double a,double b){ return a>b?a:b; } testClass::testClass():value(100) {} int testClass::getValue() { return value; }
隱式調用動態連接
程序源碼
#include<iostream> #include<cstdlib> //加載動態鏈接庫頭文件 #include"../dll/test.h" //加載動態鏈接庫的引入庫(LIB) #pragma comment(lib, "../release/dll.lib") using namespace std; void main(){ int a=6,b=10; double a1=11.1,b1=32.22; //調用動態鏈接庫中的類 testClass c; //調用動態連接庫函數 cout<<"the max:"<<max_dll(a,b)<<endl; cout<<"the max:"<<max_dll(a1,b1)<<endl; //調用動態連接庫成員函數 cout<<"the c.value is "<<c.getValue()<<endl; system("pause"); }
運行結果
顯示調用動態連接庫
程序源碼
#include"windows.h" #include<iostream> #include<cstdlib> //加載動態鏈接庫頭文件 #include"../dll/test.h" using namespace std; //聲明函數指針類型 typedef int (/*_stdcall*/ *MAXINT)(int a,int b); typedef double (/*_stdcall*/ *MAXDOUBLE)(double a,double b); void main(){ int a=6,b=10; double a1=11.1,b1=32.22; HINSTANCE hInst; hInst=LoadLibrary("../release/dll.dll"); //動態加載動態連接庫中的函數 int max_dll(int a,int b) //MAXINT max_int=(MAXINT)GetProcAddress(hInst,"?max_dll@@YAHHHH@Z");//用函數名調用 //獲取函數指針 MAXINT max_int=(MAXINT)GetProcAddress(hInst,MAKEINTRESOURCE(4));//用函數序號調用 if(!max_int) { cout<<"獲取max_int函數地址失敗!"<<endl; system("pause"); return; } //動態加載動態連接庫中的函數 double max_dll(double a,double b) //獲取函數指針 MAXDOUBLE max_double=(MAXDOUBLE)GetProcAddress(hInst,"?max_dll@@YANNN@Z");//用函數名調用 //MAXDOUBLE max_double=(MAXDOUBLE)GetProcAddress(hInst,MAKEINTRESOURCE(5));//用函數序號調用 if(!max_double) { cout<<"獲取max_double函數地址失敗!"<<endl; system("pause"); return; } //調用動態連接庫函數 cout<<"the max:"<<max_int(a,b)<<endl; cout<<"the max:"<<max_double(a1,b1)<<endl; //釋放動態鏈接庫連接 FreeLibrary(hInst); system("pause"); }
運行結果
用途
查看dll與exe相關導入導出函數信息
dumpbin程序的文件位置
相關參數
設置VC++使用環境信息
VCVAR32.bat 創建VC++使用環境信息,可是注意當在命令行界面下執行VCVARS32.bat文件後,該文件所設置的環境信息只是在當前命令行窗口生效.若是關閉該窗口,再次啓動一個新的命令行窗口後,仍須要運行 VCVAR32.bat文件
流程圖以下:
在上面使用 dumpbin 程序查看 dll.dll 的導出函數發現函數名有點奇怪,咱們定義的函數名max_dll兩個重載函數名在這裏變成了 ?max_dll@@YAHHH@Z 與 ?max_dll@@YANNN@Z,由於C++支持函數重載,對於重載的多個函數來講,其函數名都是同樣的,爲了加以區分,在編譯鏈接時,C++會按照本身的規則篡改函數名字,這一過程爲"名字改編".有的書中也稱爲"名字粉碎".不一樣的C++編譯器會採用不一樣的規則進行名字改編,這個的話,利用不一樣的C++編譯器生成的程序在調用對方提供的函數時,可能會出現問題
解決名字改變問題
第一種
代碼樣例:
利用 extern"C" 解決名字改編
動態鏈接庫程序源碼
// .h 頭文件 --------------------------------------------------------- #ifndef DLL_API #define DLL_API extern "C" _declspec(dllimport) #endif DLL_API int max_dll(int a,int b); // .cpp 源文件 ------------------------------------------------------- #define DLL_API extern "C"_declspec(dllexport) #include "test.h" int max_dll(int a,int b){ return a>b?a:b; }
主程序源碼
#include<iostream> #include<cstdlib> //加載動態鏈接庫頭文件 #include"../dll/test.h" //加載動態鏈接庫的引入庫(LIB) #pragma comment(lib, "../release/dll.lib") using namespace std; void main(){ int a=6,b=10; //調用動態連接庫函數 cout<<"the max:"<<max_dll(a,b)<<endl; system("pause"); }
利用 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 不是關鍵字,程序員不能使用。參數按照從右至左的方式入棧。
相關連接:
代碼示例(編譯環境VS2005):
使用 extern "C" 時
動態鏈接庫源碼:
// .h 頭文件---------------------------------------------------------- #ifndef DLL_API #define DLL_API extern "C" _declspec(dllimport) #endif DLL_API int __stdcall stdcall_max_dll(int a,int b); DLL_API int __cdecl cdecl_max_dll(int a,int b); DLL_API int __fastcall fastcall_std_max_dll(int a,int b); // c.pp 源文件-------------------------------------------------------- #define DLL_API extern "C" _declspec(dllexport) #include "test.h" int __stdcall stdcall_max_dll(int a,int b){ return a>b?a:b; } int __cdecl cdecl_max_dll(int a,int b){ return a>b?a:b; } int __fastcall fastcall_std_max_dll(int a,int b){ return a>b?a:b; }
用 dumpbin 查看導出函數:
未使用 extern "C" 時
動態鏈接庫源碼:
// .h 頭文件 -------------------------------------------------------------- #ifndef DLL_API #define DLL_API /*extern "C"*/ _declspec(dllimport) #endif DLL_API int __stdcall stdcall_max_dll(int a,int b); DLL_API int __cdecl cdecl_max_dll(int a,int b); DLL_API int __fastcall fastcall_std_max_dll(int a,int b); // .cpp 源文件 ----------------------------------------------------------- #define DLL_API /*extern "C"*/ _declspec(dllexport) #include "test.h" int __stdcall stdcall_max_dll(int a,int b){ return a>b?a:b; } int __cdecl cdecl_max_dll(int a,int b){ return a>b?a:b; } int __fastcall fastcall_std_max_dll(int a,int b){ return a>b?a:b; }
用 dumpbin 查看導出函數: