1. Windows和Linux下動態(tài)鏈接的原則不同
Linux系統(tǒng)以.so共享對象設(shè)計共享庫,,并在設(shè)計共享對象的過程,花費(fèi)很多精力實現(xiàn).so對象的代碼段.text多進(jìn)程共享,,提升空間利用率(如PIC機(jī)制,、SO-NAME機(jī)制、符號版本機(jī)制等),;Windows系統(tǒng)設(shè)計.dll(DLL文件還可以是別的擴(kuò)展名,,如OCX控件.ocx,控制面板程序.cpl)則并不像Linux那么小家子氣,,并沒有花費(fèi)太多精力設(shè)計一些精巧的結(jié)構(gòu)或機(jī)制來提升空間利用率,,更側(cè)重程序邏輯上的模塊化,使得各模塊之間能夠自由松散地組合,、重用和升級,。所以Windows系統(tǒng)下可以看到各種各樣的軟件是通過升級DLL的形式來進(jìn)行版本迭代,微軟本身的系統(tǒng)更新也是將這些升級補(bǔ)丁積累到一定程度后發(fā)布一個軟件更新包來實現(xiàn)系統(tǒng)升級,。
Windows COFF/PE文件結(jié)構(gòu)下的.exe可執(zhí)行文件或.dll文件都需要被映射到虛擬內(nèi)存空間中才能得以運(yùn)行,,相比于Linux采用的精打細(xì)算的代碼地址無關(guān)機(jī)制,,Windows下的文件采用的是一種叫做基地址重載的方式,即并沒有采用代碼地址無關(guān),,所有DLL涉及到的絕對地址的引用在實際裝載時都需要重定位,。所以要理解PE文件結(jié)構(gòu)下的文件鏈接和裝載過程就不得不談兩個概念:映像基地址(Image-Base-Address)和相對地址(RVA, Relative-Virtual-Address),。
2.映像基地址
在Windows PE文件結(jié)構(gòu)下,,當(dāng)一個PE文件被裝載時,其對應(yīng)的虛擬空間的起始地址便是基地址,,而任一PE文件在編譯時便存在一個指定(或默認(rèn))的優(yōu)先裝載地址,,如.exe文件一般的基地址為0x0040 0000,而.dll文件的基地址一般默認(rèn)是0x1000 0000,。系統(tǒng)在裝載.exe文件時,,因為可執(zhí)行文件是第一個被加載的文件,顯然沒人和.exe搶默認(rèn)的基地址空間,,從而.exe文件是不需要基地址重定位的,;而DLL文件裝載時,則可能遇到默認(rèn)指定的優(yōu)先基地址空間被別人搶占了,,故而這時就需要重新選擇可用的空閑地址,,這時整個文件將產(chǎn)生整體位移。
3.RVA
相對地址顧名思義就是在PE文件基地址確認(rèn)后,,一個地址相對于基地址的偏移量,,比如一個PE DLL文件默認(rèn)基地址為0x1000 0000,一個符號存儲位置的RVA為0x1000,,DLL文件編譯時有以下賦值操作:
MOV DWORD PTR [0x1000 1000], 0x20
該文件實際裝載時,,被基地址重載到0x4000 0000,則符號對應(yīng)的賦值操作應(yīng)該變成:
MOV DWORD PTR [0x1000 1000 + 0x2000 0000 - 0x1000 0000], 0x20 //此處減法只是顯示重定位的原理,,實際這一步計算在鏈接階段就已經(jīng)完成了,,此處填入的應(yīng)該是計算結(jié)果
4.DLL文件的符號導(dǎo)出聲明
ELF文件.so共享對象,在默認(rèn)情況下,,文件中所有的全局變量和全局函數(shù)都是導(dǎo)出的(除非加static修飾符限制方位范圍),。
DLL文件在默認(rèn)情況下,是不導(dǎo)出任何符號的,,如果要導(dǎo)出需要手動指明,,有兩種方法:
1. “__declspec(dllexport)” 修飾符指明該符號導(dǎo)出,對應(yīng)的,,”__delspec(dllimport)”修飾符是指明該符號從外部導(dǎo)入(如果是C++ 文件,,但是希望導(dǎo)出函數(shù)符號的修飾規(guī)則使用C的簡潔修飾規(guī)則,那么需要再在函數(shù)符號前面添加external “C”,。實際上,,不推薦使用C++編寫DLL,,因為C++只規(guī)定了語言層面的規(guī)則,但是ABI二進(jìn)制層面并沒有定義,,故而不同編譯器甚至同一編譯器的不同版本的具體實現(xiàn)都可能不同,,故而很容易出現(xiàn)版本不兼容或升級困難等問題。如果一定要用C++編寫,,需要涉及到COM(Component Object Model)技術(shù));
2. 編寫.def鏈接腳本,,批量聲明導(dǎo)出符號。如下是某.def鏈接腳本聲明該DLL的導(dǎo)出符號,。
LIBRARY Math
EXPORTS
Add @1
Sub @2
Mul @3
Div @4 NONAME
.def文件是在輸入編譯指令通過/DEF聲明傳遞給link
cl math.c /LD /DEF /math.def
系統(tǒng)級軟件開發(fā)時,,一般推薦使用.def模塊腳本批量定義導(dǎo)出符號。一方面,,C/C++編譯器可能會在編譯后將函數(shù)修飾的面目全非,,如_Add@16之類的,這時在.def文件中可以重命名,,這時__declspec()這種方式無法做到的,;另一方面是除了LIBRARY/EXPORTS關(guān)鍵字,還可以通過NAME/VERSION/SECTIONS/STACKSIZE/HEAPSIZE等關(guān)鍵字來定義輸出文件名/DLL版本/各段的屬性/默認(rèn)堆棧大小/默認(rèn)堆大小,。顯然.def模塊腳本的操作空間更大,,可以封裝更多的細(xì)節(jié)。
EXPORTS
Add = _Add@16
5.PE文件頭下的DataDirectory
這里先介紹以下DataDirectory這一關(guān)鍵的結(jié)構(gòu)數(shù)組,,因為其概念將引出后續(xù)的符號導(dǎo)入表和導(dǎo)出表,。
首先PE文件中除去sectiontable/.data/.code等段信息外,還存在PE HEADER段,,即文件頭,。這里需要深度解析下PE文件的各段組成和DataDirectory的內(nèi)容。
typedef struct{
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
}IMAGE_NT_HEADERS;
typedef struct{
WORD Machine;
WORD NumberOfSection;
WORD TimeDataStamp;
DWORD PointerToSymbolTable;
DWORD NumberOfSymbols;
WORD SizeOfOptionalHeader;
WORD Characteristics;
}IMAGE_FILE_HEADER;
typedef struct{
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData,;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
...
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum,;
...
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory;
}IMAGE_OPTIONAL_HEADER32;
typedef struct{
DWORD VirtualAddress;
DWORD iSize;
}IMAGE_DATA_DIRECTORY;
/* 16個重要數(shù)據(jù)結(jié)構(gòu)列舉如下
1. IMAGE_DIRECTORY_ENTRY_EXPORT
2. IMAGE_DIRECTORY_ENTRY_IMPORT
3. IMAGE_DIRECTORY_ENTRY_RESOURCE
4. IMAGE_DIRECTORY_ENTRY_EXCEPTION
5. IMAGE_DIRECTORY_ENTRY_SECURITY
6. IMAGE_DIRECTORY_ENTRY_BASERELOC
7. IMAGE_DIRECTORY_ENTRY_DEBUG
8. IMAGE_DIRECTORY_ENTRY_COPYRIGHT
9. IMAGE_DIRECTORY_ENTRY_GLOBALPTR
10. IMAGE_DIRECTORY_ENTRY_TLS
11. IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG
12. IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT
13. IMAGE_DIRECTORY_ENTRY_IAT
14. IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT
15. IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR
16. IMAGE_DIRECTORY_ENTRY_ENTRIES
*/
typedef struct{
DWORD OriginalFirstThunk;
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
}IMAGE_IMPORT_DESCRIPTORS;
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORYM;
DataDirectory中項目 | 定義 |
---|
IMAGE_DIRECTORY_ENTRY_EXPORT | 指向?qū)С霰?一個IMAGE_EXPORT_DIRECTORY結(jié)構(gòu)),。 |
IMAGE_DIRECTORY_ENTRY_IMPORT | 指向?qū)氡?一個IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組)。 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 指向資源(一個IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu),。是PE文件結(jié)構(gòu)下最為重要且難懂的地方,。 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION | 指向異常處理表(一個IMAGE_RUNTIME_FUNCTION_ENTRY結(jié)構(gòu)數(shù)組)。CPU特定的并且基于表的異常處理,。用于除x86之外的其它CPU上,。 |
IMAGE_DIRECTORY_ENTRY_SECURITY | 指向一個WIN_CERTIFICATE結(jié)構(gòu)的列表,它定義在WinTrust.H中,。不會被映射到內(nèi)存中,。因此,VirtualAddress域是一個文件偏移,,而不是一個RVA,。 |
IMAGE_DIRECTORY_ENTRY_RESOURCE | 指向資源(一個IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)。是PE文件結(jié)構(gòu)下最為重要且難懂的地方。 |
IMAGE_DIRECTORY_ENTRY_BASERELOC | 指向基址重定位信息,。 |
IMAGE_DIRECTORY_ENTRY_DEBUG | 指向一個IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)數(shù)組,,其中每個結(jié)構(gòu)描述了映像的一些調(diào)試信息。早期的Borland鏈接器設(shè)置這個IMAGE_DATA_DIRECTORY結(jié)構(gòu)的Size域為結(jié)構(gòu)的數(shù)目,,而不是字節(jié)大小,。要得到IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)的數(shù)目,用IMAGE_DEBUG_DIRECTORY 的大小除以這個Size域,。 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE | 指向特定架構(gòu)數(shù)據(jù),,它是一個IMAGE_ARCHITECTURE_HEADER結(jié)構(gòu)數(shù)組。不用于x86或IA-64,,但看來已用于DEC/Compaq Alpha,。 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR | 在某些架構(gòu)體系上VirtualAddress域是一個RVA,被用來作為全局指針(gp),。不用于x86,,而用于IA-64。Size域沒有被使用,。參見2000年11月的Under The Hood 專欄可得到關(guān)于IA-64 gp的更多信息,。 |
IMAGE_DIRECTORY_ENTRY_TLS | 指向線程局部存儲初始化段。 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG | 指向一個IMAGE_LOAD_CONFIG_DIRECTORY結(jié)構(gòu),。IMAGE_LOAD_CONFIG_ DIRECTORY中的信息是特定于Windows NT,、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把這個結(jié)構(gòu)放到你的可執(zhí)行文件中,,你必須用名字__load_config_used 定義一個全局結(jié)構(gòu),,類型是IMAGE_LOAD_CONFIG_ DIRECTORY。 對于非x86的其它體系,,符號名是_load_config_used (只有一個下劃線),。如果你確實要包含一個IMAGE_LOAD_CONFIG_DIRECTORY,那么在 C++ 中要得到正確的名字比較棘手,。鏈接器看到的符號名必須是__load_config_used (兩個下劃線),。C++ 編譯器會在全局符號前加一個下劃線。另外,,它還用類型信息修飾全局符號名,。因此,要使一切正常,,在 C++ 中就必須像下面這樣使用: extern “C” IMAGE_LOAD_CONFIG_DIRECTORY _load_config_used = {…} |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT | 指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組,,對應(yīng)于這個映像綁定的每個DLL。數(shù)組元素中的時間戳允許加載器快速判斷綁定是否是新的,。如果不是,加載器忽略綁定信息并且按正常方式解決導(dǎo)入API。 |
IMAGE_DIRECTORY_ENTRY_IAT | 指向第一個導(dǎo)入地址表(IAT)的開始位置,。對應(yīng)于每個被導(dǎo)入DLL的IAT都連續(xù)地排列在內(nèi)存中,。Size域指出了所有IAT的總的大小。在寫入導(dǎo)入函數(shù)的地址時加載器使用這個地址和Size域指定的大小臨時地標(biāo)記IAT為可讀寫,。 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT | 指向延遲加載信息,,它是一個CImgDelayDescr結(jié)構(gòu)數(shù)組,定義在Visual C++的頭文件DELAYIMP.H中,。延遲加載的DLL直到對它們中的API進(jìn)行第一次調(diào)用發(fā)生時才會被裝入,。Windows中并沒有關(guān)于延遲加載DLL的知識,認(rèn)識到這一點很重要,。延遲加載的特征完全是由鏈接器和運(yùn)行時庫實現(xiàn)的,。 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR | 在最近更新的系統(tǒng)頭文件中這個值已被改名為IMAGE_DIRECTORY_ENTRY_ COMHEADER。它指向可執(zhí)行文件中.NET信息的最高級別信息,,包括元數(shù)據(jù),。這個信息是一個IMAGE_COR20_HEADER結(jié)構(gòu)。 |
6.DLL文件的符號導(dǎo)出表
和ELF文件結(jié)構(gòu)下的.dynsym意義相同,,對于DLL文件,,其在被加載時,顯然需要有個集中存放導(dǎo)出符號的段或數(shù)據(jù)結(jié)構(gòu)來供鏈接器快速收集當(dāng)前DLL的導(dǎo)出信息,。COFF PE文件結(jié)構(gòu)中,,這些導(dǎo)出符號被放在文件的導(dǎo)出表中。導(dǎo)出表提供一個符號名和符號地址的映射關(guān)系,,即可以通過符號名查找該符號對應(yīng)的變量或函數(shù)的具體地址,。
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions;
DWORD AddressOfNames;
DWORD AddressOfNameOrdinals;
} IMAGE_EXPORT_DIRECTORYM;
可以看到對于DLL的導(dǎo)出表中有三個數(shù)組是最為核心的結(jié)構(gòu),分別是導(dǎo)出地址表(EAT,,Export Address Table),、符號名表(Name Table)和名字序號對應(yīng)表(Name-Ordinal Table)。導(dǎo)出地址表EAT對應(yīng)的是DLL各導(dǎo)出函數(shù)的RVA地址,,符號名表存儲的則是DLL各導(dǎo)出函數(shù)名,,序號表則是和符號名表一一對應(yīng),用以指明相應(yīng)的函數(shù)名的RVA地址在EAT數(shù)組中的下標(biāo),。
7.PE文件的符號導(dǎo)入表
一個DLL文件只有一個導(dǎo)出表(.exe可執(zhí)行文件不存在導(dǎo)出符號),,但是一個PE文件可能依賴多個文件(從多個DLL文件中導(dǎo)入符號),所以要為每個依賴文件單獨(dú)弄一份導(dǎo)入符號集合,,再將所有依賴文件的導(dǎo)入符號集合集中在一起便成了PE文件的導(dǎo)入表,。在PE文件中,記錄每個依賴文件的導(dǎo)入符號信息的是一個IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)體,,_IMAGE_IMPORT_DIRECTOTY_ENTRY指向的便是一個該結(jié)構(gòu)體的數(shù)組,。
typedef struct{
DWORD OriginalFirstThunk;
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
}IMAGE_IMPORT_DESCRIPTORS;
IAT中每個元素對應(yīng)一個被導(dǎo)入的符號,,在沒有重定位或符號解析之前,IAT中的元素值表示相對應(yīng)的導(dǎo)入符號的序號或符號名,,當(dāng)重定位和符號解析完成后,,IAT中元素值將被改寫成符號的真正地址,和ELF文件結(jié)構(gòu)下GOT功能相似,。
IAT(INT)的元素為IMAGE_THUNK_DATA32結(jié)構(gòu),,而其指向為IMAGE_IMPORT_BY_NAME結(jié)構(gòu),這兩個結(jié)構(gòu)的定義如下,。
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;
DWORD Function;
DWORD Ordinal;
DWORD AddressOfData;
} u1;
} IMAGE_THUNK_DATA32;
typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32;
typedef struct _IMAGE_IMPORT_BY_NAME {
WORD Hint;
BYTE Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;
對于32位PE來說,,來說如果最高位為1,那么低31位則直接就是導(dǎo)入符號的序號值,;如果沒有,,那么這個IMAGE_THUNK_DATA32啟用的是AddressOfData,其指向一個_IMAGE_IMPORT_BY_NAME結(jié)構(gòu)體,。使用_IMAGE_IMPORT_BY_NAME結(jié)構(gòu)體時,,先根據(jù)Hint值去往相應(yīng)DLL導(dǎo)出表中查找是否對應(yīng)的符號名是所需的符號,如果不是,,則需要按照符號名去符號名數(shù)組中二分查找(符號名數(shù)組在收集導(dǎo)出符號做了預(yù)處理,,按照字典序進(jìn)行排列,所以可以進(jìn)行二分查找),。
Fig.1 COFF PE文件結(jié)構(gòu)下的符號導(dǎo)入導(dǎo)出表對應(yīng)關(guān)系Q: 在導(dǎo)出表中可以看到函數(shù)名序號數(shù)組的存在其實蠻多余的,,明明可以通過函數(shù)符號名表和EAT地址表一一對應(yīng)來解決問題,卻非要通過一個序號表中轉(zhuǎn)下,,這是為什么,?
A: 有很多說法,,但核心還是考慮DLL兼容性。在Windows系統(tǒng)還是16bits的年代,,顯然保留導(dǎo)出函數(shù)的函數(shù)名數(shù)組是一件極為奢侈的事情,,故而出于節(jié)省空間的考慮,將DLL導(dǎo)出函數(shù)符號分配唯一的序號用以代表,,從而在完成重定位的任務(wù)下,也可盡可能地節(jié)省內(nèi)存占用,。如下面.def模塊腳本的內(nèi)容,,為各導(dǎo)出函數(shù)手動綁定序號。
LIBRARY Math
EXPORTS
Add @1
Sub @2
Mul @3
Div @4 NONAME
使用序號雖好,,可一旦發(fā)生函數(shù)變更增減,,則需要再次手動更新一遍函數(shù)序號,但這也會影響到使用老版本DLL的程序的中函數(shù)調(diào)用,,因為這種強(qiáng)綁定關(guān)系導(dǎo)致采用序號機(jī)制的DLL升級較為繁瑣,。后來隨著計算機(jī)內(nèi)存的增加,自然保留導(dǎo)出函數(shù)名成為主流選擇,。而為了兼容性考慮,,序號機(jī)制這一歷史便依舊被傳承下來了。
7.DLL顯式運(yùn)行時加載鏈接demo
#include <windows.h>
#include <stdio.h>
typedef double (*Func) (double, double);
int main(int argc, char **argv)
{
Func function;
double result;
HINSTANCE hinstLib = LoadLibrary("Math.dll");
if (hinstLib == NULL) {
printf("ERROR: unable to load DLL\n");
return -1;
}
function = (Func) GetProcAddress(hinstLib, "Add");
if(function == NULL) {
printf("ERROR: Unable to find target function\n");
FreeLibrary(hinstLib);
return 1;
}
result = function(1.0, 2.0);
printf("Result = %f\n", result);
FreeLibrary(hinstLib);
return 0;
}