2010-12-31 20:56:24| 分類: windows程序設(shè)計(jì) |舉報(bào) |字號(hào) 訂閱
本文意在講解靜態(tài)鏈接庫(kù)與動(dòng)態(tài)鏈接庫(kù)的創(chuàng)建與使用,在此之前先來對(duì)二者的概念、區(qū)別及優(yōu)缺點(diǎn)進(jìn)行簡(jiǎn)要的闡述。其中大多內(nèi)容參考相關(guān)網(wǎng)絡(luò)資料,由于本人能力有限,不能確保完全準(zhǔn)確無(wú)誤,若有偏差之處請(qǐng)不吝指出。文中使用到的代碼均在Visual Studio 2008中編譯通過,如果您使用的IDE與本文不同,可根據(jù)實(shí)際情況進(jìn)行相應(yīng)項(xiàng)目創(chuàng)建與操作。希望本文內(nèi)容對(duì)您有所幫助。
大多數(shù)高級(jí)語(yǔ)言都支持分別編譯(Compiling),程序員可以顯式地把程序劃分為獨(dú)立的模塊或文件,然后由編譯器(Compiler)對(duì)每個(gè)獨(dú)立部分分別進(jìn)行編譯。在編譯之后,由鏈接器(Linker)把這些獨(dú)立編譯單元鏈接(Linking)到一起。鏈接方式分為兩種:
靜態(tài)鏈接庫(kù)(Static Library,簡(jiǎn)稱LIB)與動(dòng)態(tài)鏈接庫(kù)(Dynamic Link Library,簡(jiǎn)稱DLL)都是共享代碼的方式。如果使用靜態(tài)鏈接庫(kù)(也稱靜態(tài)庫(kù)),則無(wú)論你愿不愿意,.LIB文件中的指令都會(huì)被直接包含到最終生成的.EXE文件中。但是若使用.DLL文件,該.DLL文件中的代碼不必被包含在最終的.EXE文件中,.EXE文件執(zhí)行時(shí)可以“動(dòng)態(tài)”地載入和卸載這個(gè)與.EXE文件獨(dú)立的.DLL文件。
鏈接一個(gè)DLL有兩種方式:
使用載入時(shí)動(dòng)態(tài)鏈接,調(diào)用模塊可以像調(diào)用本模塊中的函數(shù)一樣直接使用導(dǎo)出函數(shù)名調(diào)用DLL中的函數(shù)。這需要在鏈接時(shí)將函數(shù)所在DLL的導(dǎo)入庫(kù)鏈接到可執(zhí)行文件中,導(dǎo)入庫(kù)向系統(tǒng)提供了載入DLL時(shí)所需的信息及用于定位DLL函數(shù)的地址符號(hào)。(相當(dāng)于注冊(cè),當(dāng)作API函數(shù)來使用,其實(shí)API函數(shù)就存放在系統(tǒng)DLL當(dāng)中。)
使用運(yùn)行時(shí)動(dòng)態(tài)鏈接,運(yùn)行時(shí)可以通過LoadLibrary或LoadLibraryEx函數(shù)載入DLL。DLL載入后,模塊可以通過調(diào)用GetProcAddress獲取DLL函數(shù)的入口地址,然后就可以通過返回的函數(shù)指針調(diào)用DLL中的函數(shù)了。如此即可避免導(dǎo)入庫(kù)文件了。
DLL 地獄(DLL Hell)是指因?yàn)橄到y(tǒng)文件被覆蓋而讓整個(gè)系統(tǒng)像是掉進(jìn)了地獄。
簡(jiǎn)單地講,DLL地獄是指當(dāng)多個(gè)應(yīng)用程序試圖共享一個(gè)公用組件時(shí),如某個(gè)DLL或某個(gè)組件對(duì)象模型(COM)類,所引發(fā)的一系列問題。
最典型的情況是,某個(gè)應(yīng)用程序?qū)⒁惭b一個(gè)新版本的共享組件,而該組件與機(jī)器上的現(xiàn)有版本不向后兼容。雖然剛安裝的應(yīng)用程序運(yùn)行正常,但原來依賴前一版本共享組件的應(yīng)用程序也許已無(wú)法再工作。在某些情況下,問題的起因更加難以預(yù)料。比如,當(dāng)用戶瀏覽某些web站點(diǎn)時(shí)會(huì)同時(shí)下載某個(gè)Microsoft ActiveX控件。如果下載該控件,它將替換機(jī)器上原有的任何版本的控件。如果機(jī)器上的某個(gè)應(yīng)用程序恰好使用該控件,則很可能也會(huì)停止工作。
在許多情況下,用戶需要很長(zhǎng)時(shí)間才會(huì)發(fā)現(xiàn)應(yīng)用程序已停止工作。結(jié)果往往很難記起是何時(shí)的機(jī)器變化影響到了該應(yīng)用程序。
這些問題的原因是應(yīng)用程序不同組件的版本信息沒有由系統(tǒng)記錄或加強(qiáng)。而且,系統(tǒng)為某個(gè)應(yīng)用程序所做的改變會(huì)影響機(jī)器上的所有應(yīng)用程序—現(xiàn)在建立完全從變化中隔離出來的應(yīng)用程序并不容易。
在此通過一個(gè)實(shí)例來介紹靜態(tài)庫(kù)的創(chuàng)建與使用。在該實(shí)例中,我們將一個(gè)實(shí)現(xiàn)兩整數(shù)相加求和的函數(shù)封裝到靜態(tài)庫(kù)中供其他程序調(diào)用。
首先,使用Visual Studio 2008來創(chuàng)建一個(gè)帶預(yù)編譯頭的靜態(tài)庫(kù)項(xiàng)目Static,該項(xiàng)目包含在名為Library的解決方案中。
創(chuàng)建一個(gè)不帶預(yù)編譯頭的靜態(tài)鏈接庫(kù)項(xiàng)目有以下幾個(gè)步驟:
經(jīng)過上面的步驟,初步創(chuàng)建了一個(gè)帶預(yù)編譯頭的靜態(tài)庫(kù)項(xiàng)目,接下來編輯該項(xiàng)目以達(dá)到我們的創(chuàng)建靜態(tài)庫(kù)的目的。
首先添加一個(gè)用于定義導(dǎo)出函數(shù)的源文件Static.cpp,編碼實(shí)現(xiàn)兩個(gè)整數(shù)相加的Add函數(shù)。源文件代碼如下:
#include “StdAfx.h” // 標(biāo)準(zhǔn)頭文件
int Add(int a, int b)
{
return a + b;
}
接著點(diǎn)擊菜單命令,“工具”-“生成Static”。如果一切順利的話,就會(huì)在解決方案的“Debug”目錄中生成了名為“Static.lib”的靜態(tài)鏈接庫(kù)。
同時(shí),需要給該靜態(tài)鏈接庫(kù)編寫一個(gè)聲明頭文件Static.h,以便在鏈接時(shí)告知編譯該鏈接庫(kù)中的導(dǎo)出函數(shù)聲明。Static.h中的代碼很簡(jiǎn)單,只要聲明一下Add函數(shù)就可以:
#ifndef __STATIC_H__ // 防止該頭文件重復(fù)引用
#define __STATIC_H__
int Add(int a, int b); // 聲明導(dǎo)出函數(shù)
#endif
接著點(diǎn)擊菜單命令,“工具”-“生成Static”。如果一切順利的話,就會(huì)在Library解決方案的Debug目錄中生成了名為MyDLL.lib的靜態(tài)鏈接庫(kù)。
在Library解決方案下,再添加一個(gè)Win32控制臺(tái)應(yīng)用程序空項(xiàng)目UseLIB。程序主文件名為UseLIB.cpp,其中包含用于調(diào)用Add函數(shù)的程序入口函數(shù)main。將剛才創(chuàng)建的Static.lib及其聲明頭文件Static.h一同復(fù)制到UseLIB項(xiàng)目目錄下。并在源文件UseLIB.cpp中使用預(yù)編譯命令鏈接Static.lib(也可以在IDE的項(xiàng)目屬性中設(shè)置鏈接器選項(xiàng),或者只復(fù)制Static.h文件并設(shè)置UseLIB項(xiàng)目的“項(xiàng)目依賴項(xiàng)”為Static項(xiàng)目)。
源文件UseLIB.cpp中的代碼如下:
#pragma comment(lib, “Static.lib”) // 鏈接靜態(tài)庫(kù)Static.lib
#include <stdio.h>
#include “Static.h” // 包含Static.lib的聲明頭文件,聲明導(dǎo)出函數(shù)Add
int main(void)
{
int a = 1, b = 2;
printf(“%d+%d=%d\n”, a, b, Add(a, b)); // 調(diào)用Static.lib中的Add函數(shù)
return 0;
}
接下來點(diǎn)擊菜單命令,“工具”-“生成UseLIB”。如果順利的話,就會(huì)在Library解決方案的Debug目錄中生成了名為UseLIB.exe的可執(zhí)行執(zhí)文件,運(yùn)行UseLIB.exe,將在控制臺(tái)中輸出結(jié)果:1+2=3
由于項(xiàng)目中創(chuàng)建的源文件為.CPP文件,即C++源文件,因此Visual C++按C++規(guī)范,并采用__cdecl調(diào)用約定對(duì)其進(jìn)行編譯。這樣得到的導(dǎo)出函數(shù)就不能被C語(yǔ)言程序所調(diào)用。解決該問題的辦法是在函數(shù)體名稱前添加extern “C”修飾,告訴編譯器,該函數(shù)按照C語(yǔ)言規(guī)范,并采用__cdecl調(diào)用約定進(jìn)行編譯。因此源文件Add.cpp中的代碼可修改如下:
extern “C” int add(int a, int b)
{
return a + b;
}
最后重新編譯該靜態(tài)鏈接庫(kù)項(xiàng)目,導(dǎo)出函數(shù)Add就能夠被C語(yǔ)言程序所調(diào)用了。
另一種不改變代碼的方法是在“Static屬性頁(yè)”左邊的列表中選擇“配置屬性”-“C/C++”-“高級(jí)”,然后在右邊的“調(diào)用約定”選擇“__cdecl (/Gd)”,“編譯為”選擇“編譯為C++代碼 (/TP)”。這種方法在不同的IDE上設(shè)置方法有所不同。
在此同樣通過一個(gè)實(shí)例來介紹動(dòng)態(tài)鏈接庫(kù)的創(chuàng)建與使用。在實(shí)例中,依然使用Add函數(shù)進(jìn)行講解,這樣一方面可以沿用上面靜態(tài)鏈接的有關(guān)內(nèi)容,另一方面也可以了解動(dòng)態(tài)鏈接庫(kù)與靜態(tài)鏈接庫(kù)在創(chuàng)建和使用上的異同。
首先,在之前創(chuàng)建的Library解決方案中添加一個(gè)帶預(yù)編譯頭的動(dòng)態(tài)鏈接庫(kù)項(xiàng)目,項(xiàng)目名稱為Dynamic。使用不同IDE的朋友可以根據(jù)實(shí)際情況進(jìn)行創(chuàng)建。
創(chuàng)建一個(gè)帶預(yù)編譯頭的動(dòng)態(tài)鏈接庫(kù)項(xiàng)目有以下幾個(gè)步驟:
Dynamic項(xiàng)目自動(dòng)生成的dllmain.cpp源文件含有一個(gè)名為DllMain的函數(shù),該函數(shù)是DLL被鏈接時(shí)的入口函數(shù),它由系統(tǒng)自動(dòng)調(diào)用,在這里我們不用去理會(huì)它。
與前面創(chuàng)建靜態(tài)態(tài)鏈接庫(kù)類似的,首先添加一個(gè)用于定義導(dǎo)出函數(shù)的源文件Dynamic.cpp,編碼實(shí)現(xiàn)兩個(gè)整數(shù)相加的Add函數(shù)。源文件代碼如下:
extern “C” __declspec(dllexport) int Add(int a, int b) // 聲明為DLL導(dǎo)出函數(shù)
{
return a + b;
}
與前面靜態(tài)鏈接庫(kù)不同,在Add函數(shù)體名稱前不只添加了extern “C”修飾,還多添加了一個(gè) __declspec(dllexport)修飾。__declspec(dllexport)修飾的作用是告訴編譯器,這個(gè)函數(shù)將作為導(dǎo)出函數(shù),并在輸入庫(kù)中生成該函數(shù)的地址符號(hào)等信息,這樣其他程序就可以使用載入時(shí)動(dòng)態(tài)鏈接方式來調(diào)用該函數(shù)。另外,extern “C”在封裝DLL還有另一個(gè)作用,就是告訴編譯器,在DLL中的導(dǎo)出函數(shù)不要使用函數(shù)名修飾規(guī)則,這樣在采用運(yùn)行時(shí)動(dòng)態(tài)鏈接時(shí)就可以直接使用原函數(shù)名來調(diào)用導(dǎo)出函數(shù)了。關(guān)于函數(shù)調(diào)用方式和導(dǎo)出方式的詳細(xì)說明在后面還將提出,現(xiàn)在先撇開這些煩人的問題。
接著點(diǎn)擊菜單命令,“工具”-“生成Dynamic”。如果一切順利的話,就會(huì)在Library解決方案的Debug目錄中生成了名為Dynamic.dll的動(dòng)態(tài)鏈接庫(kù)和名為Dynamic.lib的導(dǎo)入庫(kù)(與靜態(tài)鏈接庫(kù)不同,只存放DLL的導(dǎo)出表,不包含代碼)。
最后需要給該Dynamic.dll的輸入庫(kù)Dynamic.lib編寫一個(gè)聲明頭文件Dynamic.h,以便在以后鏈接時(shí)告知編譯器該鏈接庫(kù)中的具體的導(dǎo)入內(nèi)容(一般包括代碼和資源)。Dynamic.h中的代碼很簡(jiǎn)單,只要聲明一下Add函數(shù)就可以:
#ifndef __DYNAMIC_H__ // 防止該頭文件重復(fù)引用
#define __DYNAMIC_H__
__declspec(dllexport) int Add(int a, int b); // 聲明導(dǎo)出函數(shù)
#endif
在同Library解決方案中,添加一個(gè)名為UseDLL的Win32控制臺(tái)應(yīng)用程序空項(xiàng)目。程序主文件名為UseDLL.cpp,其中包含用于調(diào)用Add函數(shù)的程序入口函數(shù)main。一下使用兩種動(dòng)態(tài)鏈接方式來鏈接Dynamic.dll。
載入時(shí)動(dòng)態(tài)鏈接是一種輕松使用動(dòng)態(tài)鏈接庫(kù)的方法,它使得使用動(dòng)態(tài)鏈接庫(kù)如同使用靜態(tài)鏈接庫(kù)一樣方便。將導(dǎo)入庫(kù)Dynamic.lib及其聲明頭文件Dynamic.h一同復(fù)制到UseDLL項(xiàng)目目錄下,并把Dynamic.dll復(fù)制到項(xiàng)目的Debug目錄中。并在源文件UseDLL.cpp中使用預(yù)編譯命令鏈接Dynamic.lib(也可以在IDE的項(xiàng)目屬性中設(shè)置鏈接器選項(xiàng),或者只復(fù)制Dynamic.h文件并設(shè)置UseDLL項(xiàng)目的“項(xiàng)目依賴項(xiàng)”為Dynamic項(xiàng)目)。
源文件UseDLL.cpp中的代碼如下:
#pragma comment(lib, “Dynamic.lib”) // 鏈接導(dǎo)入庫(kù)Dynamic.lib
#include <stdio.h>
#include “Dynamic.h” // 包含Dynamic.lib的聲明頭文件,提供導(dǎo)出函數(shù)Add的聲明
int main(void)
{
int a = 1, b = 2;
printf(“%d+%d=%d\n”, a, b, Add(a, b)); // 調(diào)用Dynamic.DLL中的Add函數(shù)
getchar(); // 用于查看輸出結(jié)果
return 0;
}
幾乎跟使用靜態(tài)鏈接庫(kù)一樣。接下來點(diǎn)擊菜單命令,“工具”-“生成UseDLL”。如果一切順利的話,就會(huì)在Library解決方案的Debug目錄中生成了名為UseDLL.exe的可執(zhí)行文件,運(yùn)行UseDLL.exe文件,將在控制臺(tái)中輸出結(jié)果:1+2=3
運(yùn)行時(shí)動(dòng)態(tài)鏈接的代碼相對(duì)麻煩些,需要使用到Windows的三個(gè)API函數(shù),還要進(jìn)行一些判斷以防止不必要的麻煩。我們?cè)?/span>UseDLL項(xiàng)目的基礎(chǔ)上做些修改來實(shí)現(xiàn)運(yùn)行時(shí)動(dòng)態(tài)鏈接。這里只需要把Dynamic.dll復(fù)制到UseDLL項(xiàng)目的Debug目錄中,因?yàn)椴挥迷诰幾g的時(shí)候鏈接導(dǎo)入庫(kù),只要在運(yùn)行根據(jù)需要鏈接Dynamic.dll。下面先給出修改后的源文件Dynamic.cpp的代碼:
#include <windows.h> // 用于聲明window API函數(shù)及宏等
#include <stdio.h>
typedef int (* FuncAdd)(int a, int b); // 定義將要調(diào)用的導(dǎo)出函數(shù)Add的指針類型
int main(void)
{
FuncAdd Add; // 定義Add函數(shù)指針
int a = 1, b = 2;
HMODULE hDLL = LoadLibrary(TEXT("MyDLL.dll")); // 載入DLL,并獲取其句柄
if (hDLL) // MyDLL.dll載入成功
{
Add = (FuncAdd)GetProcAddress(hDLL, "Add"); // 獲取導(dǎo)出函數(shù)Add指針
if (Add) // 正確獲取Add函數(shù)指針
{
printf("%d+%d=%d\n", a, b, Add(a, b)); // 調(diào)用導(dǎo)出函數(shù)Add
}
else // 沒有找到Add函數(shù)
{
printf("Add Not Found!\n");
}
}
else // MyDLL.dll載入失敗
{
printf("LoadLibrary Failed!\n");
}
getchar();
FreeLibrary((TEXT("MyDLL.dll")); // 釋放DLL
return 0;
}
看到了吧,調(diào)用方法比較繁瑣。由于沒有鏈接導(dǎo)入庫(kù),不能使用地址符號(hào)定位導(dǎo)出函數(shù)的入口地址,只能通過GetProcAdress來獲取其在地址空間中的指針,再通過指針調(diào)用。但程序在運(yùn)行之前,GetProcAdress無(wú)法判斷指針的有效性。因此,為了防止Dynamic項(xiàng)目中不存在Add函數(shù)而使程序在運(yùn)行時(shí)出錯(cuò),有必要在調(diào)用Add之前判斷其函數(shù)指針的有效性。
最后,點(diǎn)擊菜單命令,“工具”-“重新生成UseDLL”。如果一切順利的話,就會(huì)在Library解決方案的Debug目錄中生成了名為UseDLL.exe的可執(zhí)行文件,運(yùn)行UseDLL.exe文件,將在控制臺(tái)中輸出結(jié)果:1+2=3
在創(chuàng)建動(dòng)態(tài)鏈接庫(kù)項(xiàng)目時(shí),如果在“應(yīng)用程序設(shè)置”中只勾選“預(yù)編譯頭”,而沒有勾選“空項(xiàng)目”或“導(dǎo)出符號(hào)”,那么在對(duì)該項(xiàng)目進(jìn)行編譯鏈接時(shí)將只會(huì)生成動(dòng)態(tài)鏈接庫(kù)dll文件,不生成導(dǎo)入庫(kù)lib文件。此問題的解決辦法如下:
注意,此時(shí)在項(xiàng)目屬性的配置列表“配置屬性”-“鏈接器”-“輸入”中的“模塊定義文件”項(xiàng)目中將出現(xiàn)剛才創(chuàng)建的模塊定義文件MyDLL.def。如果此前不是添加“新建項(xiàng)”,而是添加“現(xiàn)有項(xiàng)”,那么必須在此項(xiàng)目上填寫該現(xiàn)有模塊定義文件的文件名,否則將不會(huì)生成lib文件。
在 32 位編譯器版本中,可以使用__declspec(dllexport)關(guān)鍵字從DLL導(dǎo)出數(shù)據(jù)、函數(shù)、類或類成員函數(shù)。__declspec(dllexport)將導(dǎo)出指令添加到對(duì)象文件(即obj文件),若要導(dǎo)出函數(shù),__declspec(dllexport)關(guān)鍵字必須出現(xiàn)在調(diào)用約定關(guān)鍵字的左邊(如果指定了關(guān)鍵字)。例如:
__declspec(dllexport) void __cdecl Function1(void);
若要導(dǎo)出類中的所有公共數(shù)據(jù)成員和成員函數(shù),關(guān)鍵字必須出現(xiàn)在類名的左邊,如下所示:
class __declspec(dllexport) CExampleExport : public CObject
{ ... class definition ... };
生成DLL時(shí),通常創(chuàng)建一個(gè)包含正在導(dǎo)出的函數(shù)原型和/或類的頭文件,并將__declspec(dllexport)添加到頭文件中的聲明。
若要提高代碼的可讀性,請(qǐng)為__declspec(dllexport)定義一個(gè)宏并對(duì)正在導(dǎo)出的每個(gè)符號(hào)使用該宏:
#define Export __declspec(dllexport)
模塊定義文件(.def)是包含一個(gè)或多個(gè)描述各種DLL屬性的模塊語(yǔ)句的文本文件。二者的目的都是將公共符號(hào)導(dǎo)入到應(yīng)用程序中或從 DLL 導(dǎo)出函數(shù)。添加__declspec(dllexport)是為了提供不使用.def文件從 .EXE或 .DLL導(dǎo)出函數(shù)的簡(jiǎn)單方法。如果不使用__declspec (dllimport)或__declspec(dllexport)導(dǎo)出DLL函數(shù),則DLL需要.def文件。
并不是任何時(shí)候選擇添加__declspec(dllexport)而放棄.def的方式都是好的。如果DLL是提供給VC++用戶使用的,只需要把編譯DLL時(shí)產(chǎn)生的.lib提供給用戶,它可以很輕松地調(diào)用你的DLL。但是如果DLL是供VB、PB、Delphi用戶使用的,那么會(huì)產(chǎn)生一個(gè)小麻煩。因?yàn)?/span>VC++對(duì)于__declspec(dllexport) 聲明的函數(shù)會(huì)進(jìn)行名稱轉(zhuǎn)換,比如函數(shù):
__declspec(dllexport) int __stdcall IsWinNT()
會(huì)轉(zhuǎn)換為IsWinNT@0,這樣你在VB中必須這樣聲明:
Declare Function IsWinNT Lib "my.dll" Alias "IsWinNT@0" () As Long
@的后面的數(shù)由于參數(shù)類型不同而可能不同。這顯然不太方便。所以如果要想避免這種轉(zhuǎn)換,就要使用.def文件方式。
函數(shù)調(diào)用約定不僅決定了發(fā)生函數(shù)調(diào)用時(shí)函數(shù)參數(shù)的入棧順序,還決定了是由調(diào)用者函數(shù)還是被調(diào)用函數(shù)負(fù)責(zé)清除棧中的參數(shù),還原堆棧。函數(shù)調(diào)用約定有很多方式,除了常見的__cdecl,__fastcall和__stdcall之外,C++的編譯器還支持thiscall方式,不少C/C++編譯器還支持naked call方式。這么多函數(shù)調(diào)用約定常常令許多程序員很迷惑,到底它們是怎么回事,都是在什么情況下使用呢?下面就分別介紹這幾種函數(shù)調(diào)用約定。
編譯器的命令行參數(shù)是/Gd,__cdecl是C Declaration的縮寫,是C和C++程序的缺省調(diào)用方式。所有參數(shù)從右到左依次入棧,這些參數(shù)由調(diào)用者清除,每一個(gè)調(diào)用它的函數(shù)都包含清空堆棧的代碼,稱為手動(dòng)清棧。所以產(chǎn)生的可執(zhí)行文件大小會(huì)比調(diào)用__stdcall函數(shù)的大。被調(diào)用函數(shù)無(wú)需要求調(diào)用者傳遞多少參數(shù),調(diào)用者傳遞過多或者過少的參數(shù),甚至完全不同的參數(shù)都不會(huì)產(chǎn)生編譯階段的錯(cuò)誤。所有非C++成員函數(shù)和那些沒有用__stdcall或__fastcall聲明的函數(shù)都默認(rèn)是__cdecl方式,它使用C函數(shù)調(diào)用方式。
__stdcall是Standard Call的縮寫,是Pascal程序的缺省調(diào)用方式,通常用于Win32 API中,是WIN32 API的標(biāo)準(zhǔn)調(diào)用方式:所有參數(shù)從右到左依次入棧,如果是調(diào)用類成員的話,最后一個(gè)入棧的是this指針。這些堆棧中的參數(shù)由被調(diào)用的函數(shù)在返回后清除,使用的指令是 retn X,X表示參數(shù)占用的字節(jié)數(shù),CPU在ret之后自動(dòng)彈出X個(gè)字節(jié)的堆??臻g。稱為自動(dòng)清棧。函數(shù)在編譯的時(shí)候就必須確定參數(shù)個(gè)數(shù),并且調(diào)用者必須嚴(yán)格的控制參數(shù)的生成,不能多,不能少,否則返回后會(huì)出錯(cuò)。
__fastcall 是編譯器指定的快速調(diào)用方式。由于大多數(shù)的函數(shù)參數(shù)個(gè)數(shù)很少,使用堆棧傳遞比較費(fèi)時(shí)。因此__fastcall通常規(guī)定將前兩個(gè)(或若干個(gè))參數(shù)由寄存器傳遞,其余參數(shù)還是通過堆棧傳遞。不同編譯器編譯的程序規(guī)定的寄存器不同。返回方式和__stdcall相當(dāng)。
__thiscall 是為了解決類成員調(diào)用中this指針傳遞而規(guī)定的。__thiscall要求把this指針放在特定寄存器中,該寄存器由編譯器決定。VC使用ecx,Borland的C++編譯器使用eax。返回方式和__stdcall相當(dāng)。
采用前面幾種函數(shù)調(diào)用約定的函數(shù),編譯器會(huì)在必要的時(shí)候自動(dòng)在函數(shù)開始添加保存ESI,EDI,EBX,EBP寄存器的代碼,在退出函數(shù)時(shí)恢復(fù)這些寄存器的內(nèi)容,使用naked call方式聲明的函數(shù)不會(huì)添加這樣的代碼,這也就是為什么稱其為naked的原因吧。naked call不是類型修飾符,故必須和_declspec共同使用。
__fastcall 和 __thiscall涉及的寄存器由編譯器決定,因此不能用作跨編譯器的接口。所以Windows上的COM對(duì)象接口都定義為__stdcall調(diào)用方式。
帶有可變參數(shù)的函數(shù)必須且只能使用__cdecl方式,例如下面的函數(shù):
int printf(char * fmtStr, ...);
int scanf(char * fmtStr, ...);
__stdcall和__cdecl這兩個(gè)關(guān)鍵字看起來似乎很少和我們打交道,但是看了下面的定義(來自windef.h),你一定會(huì)覺得驚訝:
#define CALLBACK __stdcall
#define WINAPI __stdcall
#define WINAPIV __cdecl
#define APIENTRY WINAPI
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
#define cdecl _cdecl
#ifndef CDECL
#define CDECL _cdecl
#endif
幾乎我們寫的每一個(gè)WINDOWS API函數(shù)都是__stdcall類型的,為什么?
首先,我們談一下兩者之間的區(qū)別:
WINDOWS的函數(shù)調(diào)用時(shí)需要用到棧。當(dāng)函數(shù)調(diào)用完成后,棧需要清除,這里就是問題的關(guān)鍵,如何清除?
如果我們的函數(shù)使用了__cdecl,那么棧的清除工作是由調(diào)用者,用COM的術(shù)語(yǔ)來講就是客戶來完成的。這樣帶來了一個(gè)棘手的問題,不同的編譯器產(chǎn)生棧的方式不盡相同,那么調(diào)用者能否正常的完成清除工作呢?答案是不能。
如果使用__stdcall,上面的問題就解決了,函數(shù)自己解決清除工作。所以,在跨開發(fā)平臺(tái)的調(diào)用中,我們都使用__stdcall(雖然有時(shí)是以WINAPI的樣子出現(xiàn)),如JNI。
那么為什么還需要__cdecl呢?當(dāng)我們遇到這樣的函數(shù)如fprintf()它的參數(shù)是可變的,不定長(zhǎng)的,被調(diào)用者事先無(wú)法知道參數(shù)的長(zhǎng)度(如typedef int (*MYPROC)(LPTSTR, ...);),事后的清除工作也無(wú)法正常的進(jìn)行,因此,這種情況我們只能使用__cdecl。
到這里我們有一個(gè)結(jié)論,如果你的程序中沒有涉及可變參數(shù),最好使用__stdcall關(guān)鍵字。
函數(shù)的名字修飾(Decorated Name)就是編譯器在編譯期間創(chuàng)建的一個(gè)字符串,用來指明函數(shù)的定義或原型。LINK程序或其他工具有時(shí)需要指定函數(shù)的名字修飾來定位函數(shù)的正確位置。多數(shù)情況下程序員并不需要知道函數(shù)的名字修飾,LINK程序或其他工具會(huì)自動(dòng)區(qū)分他們。當(dāng)然,在某些情況下需要指定函數(shù)的名字修飾,例如在C++程序中,為了讓LINK程序或其他工具能夠匹配到正確的函數(shù)名字,就必須為重載函數(shù)和一些特殊的函數(shù)(如構(gòu)造函數(shù)和析構(gòu)函數(shù))指定名字裝飾。另一種需要指定函數(shù)的名字修飾的情況是在匯編程序中調(diào)用C或C++的函數(shù)。如果函數(shù)名字,調(diào)用約定,返回值類型或函數(shù)參數(shù)有任何改變,原來的名字修飾就不再有效,必須指定新的名字修飾。C和C++程序的函數(shù)在內(nèi)部使用不同的名字修飾方式,下面將分別介紹這兩種方式。
對(duì)于__stdcall調(diào)用約定,編譯器和鏈接器會(huì)在輸出函數(shù)名前加上一個(gè)下劃線前綴,函數(shù)名后面加上一個(gè)“@”符號(hào)和其參數(shù)的字節(jié)數(shù),例如_functionname@number。__cdecl調(diào)用約定僅在輸出函數(shù)名前加上一個(gè)下劃線前綴,例如_functionname。__fastcall調(diào)用約定在輸出函數(shù)名前加上一個(gè)“@”符號(hào),后面也是一個(gè)“@”符號(hào)和其參數(shù)的字節(jié)數(shù),例如@functionname@number。
C++的函數(shù)名修飾規(guī)則有些復(fù)雜,但是信息更充分,通過分析修飾名不僅能夠知道函數(shù)的調(diào)用方式,返回值類型,參數(shù)個(gè)數(shù)甚至參數(shù)類型。不管__cdecl,__fastcall還是__stdcall調(diào)用方式,函數(shù)修飾都是以一個(gè)“?”開始,后面緊跟函數(shù)的名字,再后面是參數(shù)表的開始標(biāo)識(shí)和按照參數(shù)類型代號(hào)拼出的參數(shù)表。對(duì)于__stdcall方式,參數(shù)表的開始標(biāo)識(shí)是“@@YG”,對(duì)于__cdecl方式則是“@@YA”,對(duì)于__fastcall方式則是“@@YI”。參數(shù)表的拼寫代號(hào)如下所示:
代號(hào) 類型
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
O long double
PA 指針前綴
AA 引用前綴
V類名@@ 類
指針的方式有些特別,用PA表示指針,用PB表示const類型的指針,如果是引用,則在類型代號(hào)前加上AA。后面的代號(hào)表明指針類型,如果相同類型的指針連續(xù)出現(xiàn),以“0”代替,一個(gè)“0”代表一次重復(fù)。如果相同類型的引用連續(xù)出現(xiàn),則以“1”代替,每個(gè)“1”都代表一次重復(fù)。U表示結(jié)構(gòu)類型,通常后跟結(jié)構(gòu)體的類型名,用“@@”表示結(jié)構(gòu)類型名的結(jié)束。函數(shù)的返回值不作特殊處理,它的描述方式和函數(shù)參數(shù)一樣,緊跟著參數(shù)表的開始標(biāo)志,也就是說,函數(shù)參數(shù)表的第一項(xiàng)實(shí)際上是表示函數(shù)的返回值類型。參數(shù)表后以“@Z”標(biāo)識(shí)整個(gè)名字的結(jié)束,如果該函數(shù)無(wú)參數(shù),則以“Z”標(biāo)識(shí)結(jié)束。下面舉三個(gè)個(gè)例子。
函數(shù)聲明:int Function1(char *var1,unsigned long);
函數(shù)修飾:?Function1@@YGHPADK@Z
函數(shù)聲明:void Function2();
函數(shù)修飾:?Function2@@YGXXZ
函數(shù)原型(Test為自定義類):
void abc(int a, long b, char* c, char* d, bool &e, Test f, short g);
函數(shù)修飾名:?abc@@YAXHJPAD0AA_NVTest@@F@Z
對(duì)于C++的類成員函數(shù)(其調(diào)用方式是thiscall),函數(shù)的名字修飾與非成員的C++函數(shù)稍有不同,首先就是在函數(shù)名字和參數(shù)表之間插入以“@”字符引導(dǎo)的類名;其次是參數(shù)表的開始標(biāo)識(shí)不同,公有(public)成員函數(shù)的標(biāo)識(shí)是“@@QAE”,保護(hù)(protected)成員函數(shù)的標(biāo)識(shí)是“@@IAE”,私有(private)成員函數(shù)的標(biāo)識(shí)是“@@AAE”,如果函數(shù)聲明使用了const關(guān)鍵字,則相應(yīng)的標(biāo)識(shí)應(yīng)分別為“@@QBE”,“@@IBE”和“@@ABE”。如果參數(shù)類型是類實(shí)例的引用,則使用“AAV1”,對(duì)于const類型的引用,則使用“ABV1”。下面就以類CTest為例說明C++成員函數(shù)的名字修飾規(guī)則:
class CTest
{
......
private:
void Function(int);
protected:
void CopyInfo(const CTest &src);
public:
long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
long InsightClass(DWORD dwClass) const;
......
};
對(duì)于成員函數(shù)Function,其函數(shù)修飾名為“?Function@CTest@@AAEXH@Z”,字符串“@@AAE”表示這是一個(gè)私有函數(shù)。成員函數(shù)CopyInfo只有一個(gè)參數(shù),是對(duì)類CTest的const引用參數(shù),其函數(shù)修飾名為“?CopyInfo@CTest@@IAEXABV1@@Z”。DrawText是一個(gè)比較復(fù)雜的函數(shù)聲明,不僅有字符串參數(shù),還有結(jié)構(gòu)體參數(shù)和HDC句柄參數(shù),需要指出的是HDC實(shí)際上是一個(gè)HDC__結(jié)構(gòu)類型的指針,這個(gè)參數(shù)的表示就是“PAUHDC__@@”,其完整的函數(shù)修飾名為“?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z”。InsightClass是一個(gè)共有的const函數(shù),它的成員函數(shù)標(biāo)識(shí)是“@@QBE”,完整的修飾名就是“?InsightClass@CTest@@QBEJK@Z”。
無(wú)論是C函數(shù)名修飾方式還是C++函數(shù)名修飾方式均不改變輸出函數(shù)名中的字符大小寫,這和PASCAL調(diào)用約定不同,PASCAL約定輸出的函數(shù)名無(wú)任何修飾且全部大寫。
extern “C”使得其作用的函數(shù)采用C名字修飾方式進(jìn)行編譯。不要在C程序(源程序文件以.c作為后綴)中使用extern “C”,否則會(huì)出現(xiàn)錯(cuò)誤。因?yàn)?/span>C編譯器不認(rèn)識(shí)extern “C”。
有兩種方式可以檢查你的程序中的函數(shù)的名字修飾:使用編譯輸出列表或使用Dumpbin工具。使用/FAc,/FAs或/FAcs命令行參數(shù)可以讓編譯器輸出函數(shù)或變量名字列表。使用dumpbin.exe /SYMBOLS命令也可以獲得obj文件或lib文件中的函數(shù)或變量名字列表。此外,還可以使用 undname.exe 將修飾名轉(zhuǎn)換為未修飾形式。
聯(lián)系客服