在windows操做系統下,可執行文件的存儲格式是PE格式;在Linux操做系統下,可執行文件的存儲格式的WLF格式。它們都是COFF格式文件的變種,都是從COFF格式的文件演化而來的。程序員
在windows平臺下,目標文件(.obj),靜態庫文件(.lib)使用COFF格式存儲;而可執行文件(.exe),動態連接庫文件(.dll)使用PE格式存儲。靜態庫文件其實就是一堆目標文件的集合。windows
在「WinNT.h」頭文件中定義了COFF格式文件,以及PE格式文件的數據結構。這些定義是一系列的結構體,枚舉,以及#define宏定義。在ImageHlp.dll中定義了編輯和讀取PE文件內容的Win32API。數組
在64位Windows操做系統下,PE格式文件被作了少部分修改。沒有新的字段定義被加入,而且去除了一些字段的定義,同時將字段的寬度從32位擴充到64位。64位windows操做系統下的PE格式文件被命名爲:PE32+。cookie
COFF文件的整體結構以下圖所示:數據結構
從文件內容上來看,COFF文件由二進制數據組成。這些二進制數據從文件的零位置開始,依次存儲,直到文件末尾。從數據結構的角度來看,這些二進制數據又分別屬於不一樣的結構體或者結構體數組。這些結構體被定義在「WinNT.h」頭文件中。架構
在COFF文件中,這些結構體或結構體數組分別表示不一樣的含義,記錄着COFF文件中的不一樣內容。從文件的頂端開始,依次存儲了文件頭,可選頭,段表,段數據,重定位表,行號表,符號表,以及字符串表的信息。這些結構體數據之間存在關聯關係。好比:文件頭信息中存儲了符號表的開始位置,以及段表中數組元素的個數;在段表中存儲了各個段的位置,重定位表的位置,行號表的位置;重定位表中的項會關聯到符號表中的某個符號;而符號表中某個符號的名稱可能會存儲在字符串表中。app
使用dumpbin工具能夠將目標文件的內容導出,具體的命令格式以下:函數
Dumpbin /all DemoMath.obj >DemoMath.txt工具 |
在上面的命令中,將目標文件「DemoMath.obj」的全部內容導出到文本文件「DemoMath.txt」中。命令選項「/all」表示導出全部內容,命令選項「>」表示將導出的內容存儲到文件中。佈局
文件頭以一個結構體的形式存儲在COFF文件的開始位置,佔20個字節的大小。每個COFF格式的二進制文件都必須包含一個文件頭,它用來保存COFF文件的基本信息,如:文件標識,各個表的位置等。
使用dumpbin工具導出「DemoMath.obj」目標文件的內容後,文件頭部分的信息內容以下:
Dump of file demomath.obj File Type: COFF OBJECT //表示該文件格式爲COFF格式 FILE HEADER VALUES //如下依次是文件頭中各項的值 14C machine (x86) //魔數 20 number of sections //段的數量 519AFB7E time date stamp Tue May 21 12:43:42 2013 //創建時間 288A file pointer to symbol table //符號表的位置 83 number of symbols //符號的數量 0 size of optional header //可選頭的大小 0 characteristics //文件屬性標記。零表示有重定位信息,有符號表,有行號,不可執行,具體解釋可見「Characteristics字段的取值狀況表」的描述。 |
在「WinNT.h」頭文件中,文件頭被定義爲IMAGE_FILE_HEADER類型,具體的定義形式以下:
typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER 20 //文件頭的大小 |
在文件頭中,各個字段的詳細解釋以下表所示:
字段名稱 |
類型 |
描述 |
Machine |
Word |
魔法數字,在i386平臺中,該值爲0x014c。這是一個平臺的標識。 |
NumberOfSections |
Word |
段的數量。段表的大小由它肯定。 段表的大小 = Sizeof(IMAGE_SECTION_HEADER) * NumberOfSections |
TimeDateStamp |
Dword |
該字段是一個時間戳,用來記錄COFF文件建立的時間。當COFF文件做爲一個可執行文件的時候,該值被用來看成加密用的比對標識。 |
PointerToSymbolTable |
Dword |
符號表在文件中的偏移量,該偏移量從文件的零位置爲基準。使用該值能夠肯定符號表的第一個字節的位置。 |
NumberOfSymbols |
Dword |
符號表中符號的個數。 |
SizeOfOptionalHeader |
Word |
可選頭的大小。一般爲零。經過此值可定位段表。 |
Characteristics |
Word |
文件的屬性標記,它標記了文件的類型,以及文件中所保存的數據的信息。該標記的詳細說明見下表。 |
Characteristics字段的取值狀況以下表所示:
名稱 |
值 |
說明 |
F_RELFLG |
0x0001 |
無重定位信息標記。值爲1表示無重定位信息。在目標文件中,該值爲1,可執行文件中,該值爲零。 |
F_EXEC |
0x0002 |
可執行標記。值爲2表示該文件中全部符號都已經被解析完畢,能夠被執行。在目標文件中,該值爲零。 |
F_LNNO |
0x0004 |
無行號標記。值爲4表示該文件中沒有行號表 |
F_LSYMS |
0x0008 |
無符號標記。值爲8表示該文件中沒有符號表 |
F_AR32WR |
0x0100 |
該標記指出文件是 32 位的 Little-Endian COFF 文件。 |
該數據結構爲可選數據,在目標文件中不存在此數據結構。只有當COFF文件做爲可執行文件存在的時候,該數據結構纔有意義。
段表是各個段的目錄,用於檢索各個段的信息。它以結構體數組的形式存儲在可選頭或者文件頭的後面。在段表中,每一項的大小是36個字節,數組元素的個數記錄在文件頭的「NumberOfSections」字段中。
段的劃分是基於各組數據的共同屬性,而不是邏輯概念。每段是一塊擁有共同屬性的數據,好比代碼/數據、讀/寫等。若是COFF文件中的數據/代碼擁有相同屬性,它們就能被納入同一段中。
在段表中記錄了各個段在段數據區域中的位置(相對文件首位置的絕對偏移),以及各段重定位信息在重定位表中的位置。
在COFF格式的目標文件中,每個函數造成一個.text段,所以會有多個名爲.text的段。在使用工具dumpbin導出「DemoMath.obj」目標文件的內容後,除了列出.text段的同時,也將與該段相對應的重定位段一塊兒列出。具體內容以下:
SECTION HEADER #9 .text name //段表信息的內容 0 physical address 0 virtual address 2A size of raw data 16B0 file pointer to raw data (000016B0 to 000016D9) 16DA file pointer to relocation table 0 file pointer to line numbers 1 number of relocations 0 number of line numbers 60501020 flags Code COMDAT; sym= "int __cdecl GetOperTimes(void)" (?GetOperTimes@@YAHXZ) 16 byte align Execute Read RAW DATA #9 //段的二進制數據 00000000: 55 8B EC 81 EC C0 00 00 00 53 56 57 8D BD 40 FF U.ì.ìà...SVW.?@? 00000010: FF FF B9 30 00 00 00 B8 CC CC CC CC F3 AB A1 00 ??10...?ììììó??. 00000020: 00 00 00 5F 5E 5B 8B E5 5D C3 ..._^[.?]?
RELOCATIONS #9 //段的重定位信息 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 0000001F DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) |
在「WinNT.h」頭文件中,文件頭被定義爲IMAGE_SECTION_HEADER類型,具體的定義形式以下:
#define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER { BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; union { DWORD PhysicalAddress; DWORD VirtualSize; } Misc; DWORD VirtualAddress; DWORD SizeOfRawData; DWORD PointerToRawData; DWORD PointerToRelocations; DWORD PointerToLinenumbers; WORD NumberOfRelocations; WORD NumberOfLinenumbers; DWORD Characteristics; } IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER; #define IMAGE_SIZEOF_SECTION_HEADER 40 |
在段表中,各個字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
Name |
BYTE |
節的ASCII名稱。節名不保證必定是以NULL結尾的。若是你指定了長於8個字符的節名,連接器會把它截短爲8個字符。在OBJ文件中存在一個機制容許更長的節名。節名一般以一個句點開始,但這並非必須的。節名中有一個「$」時連接器會對之進行特殊處理。前面帶有「$」的相同名字的節將會被合併。合併的順序是按照「$」後面字符的字母順序進行合併的 |
PhysicalAddress |
DWORD |
|
VirtualSize |
DWORD |
指出實際被使用的節的大小。這個域的值能夠大於或小於SizeOfRawData域的值。若是VirtualSize的值大,SizeOfRawData就是可執行文件中已初始化數據的大小,剩下的字節用0填充。在OBJ文件中這個域被設爲0。 |
VirtualAddress |
DWORD |
在可執行文件中,是節被加載到內存中後的RVA。在OBJ文件中應該被設爲0 |
SizeOfRawData |
DWORD |
在可執行文件或OBJ文件中該節所佔用的字節大小。對於可執行文件,這個值必須是PE頭中給出的文件對齊值的倍數。若是是0,則說明這個節中的數據是未初始的。 |
PointerToRawData |
DWORD |
節在磁盤文件中的偏移。對於可執行文件,這個值必須是PE頭部給出的文件對齊值的倍數。 |
PointerToRelocations |
DWORD |
節的重定位數據的文件偏移。只用於OBJ文件,在可執行文件中被設爲0。對於OBJ文件,若是這個域的值不爲0的話,它就指向一個IMAGE_RELOCATION結構數組。 |
PointerToLinenumbers |
DWORD |
節的COFF樣式行號的文件偏移。若是非0,則指向一個IMAGE_LINENUMBER結構數組。只在COFF行號被生成時使用。 |
NumberOfRelocations |
WORD |
PointerToRelocations 指向的重定位的數目。在可執行文件中應該是0。 |
NumberOfLinenumbers |
WORD |
NumberOfRelocations 域指向的行號的數目。只在COFF行號被生成時使用。 |
Characteristics |
WORD |
被或到一塊兒的一些標記,用來表示節的屬性。這些標記中不少均可以經過連接器選項/SECTION來設置。 |
Characteristics字段的取值狀況以下表所示:
值 |
描述 |
IMAGE_SCN_CNT_CODE |
節中包含代碼。 |
IMAGE_SCN_MEM_EXECUTE |
節是可執行的。 |
IMAGE_SCN_CNT_INITIALIZED_DATA |
節中包含已初始化數據。 |
IMAGE_SCN_CNT_UNINITIALIZED_DATA |
節中包含未初始化數據。 |
IMAGE_SCN_MEM_DISCARDABLE |
節可被丟棄。用於保存連接器使用的一些信息,包括.debug$節。 |
IMAGE_SCN_MEM_NOT_PAGED |
節不可被頁交換,所以它老是存在於物理內存中。常常用於內核模式的驅動程序。 |
IMAGE_SCN_MEM_SHARED |
包含節的數據的物理內存頁在全部用到這個可執行體的進程之間共享。所以,每一個進程看到這個節中的數據值都是徹底同樣的。這對一個進程的全部實例之間共享全局變量頗有用。要使一個節共享,可以使用/section:name,S 連接器選項。 |
IMAGE_SCN_MEM_READ |
節是可讀的。幾乎老是被設置。 |
IMAGE_SCN_MEM_WRITE |
節是可寫的。 |
IMAGE_SCN_LNK_INFO |
節中包含連接器使用的信息。只在OBJ文件中存在。 |
IMAGE_SCN_LNK_REMOVE |
節中的數據不會成爲映像的一部分。只出如今OBJ文件中。 |
IMAGE_SCN_LNK_COMDAT |
節中的內容是公共數據(comdat)。公共數據是指可被定義在多個OBJ文件中的數據。連接器將選擇一個包含到可執行文件中。Comdat 對於支持C++模板函數和在函數級別上的連接是相當重要的。Comdat節只出如今OBJ文件中。 |
IMAGE_SCN_ALIGN_XBYTES |
在最終的可執行文件中這個節中數據的對齊大小。它可有許多取值(_4BYTES,_8BYTES,_16BYTES等)。若是沒有被指定,缺省是16字節。這些標記只在OBJ文件中被設置。 |
在編譯階段,將某些源文件編譯成目標文件的時候,在目標文件中,某些被調用函數或者數據的位置是沒法肯定的。這時候,編譯器將這些被調用的函數或者數據的地址設定爲一個默認的假值。在連接階段,當可以肯定這些被調用函數或數據的地址的時候,再用真實的地址來替換這些假值。咱們將這個過程叫作重定位。
使用工具dumpbin將目標文件main.obj的內容輸出爲彙編格式的文件後,能夠觀察到這些假值的設定狀況,以及須要重定位的位置。命令格式以下:
Dumpbin /disasm main.obj >mainasm.txt |
輸入的彙編文件的一部份內容以下:
//objMath.SubData(nGlobalData,3);如下是執行該函數調用的彙編代碼 00000080: 8B F4 mov esi,esp 00000082: 83 EC 08 sub esp,8 00000085: DD 05 00 00 00 00 fld qword ptr [__real@4008000000000000] 0000008B: DD 1C 24 fstp qword ptr [esp] 0000008E: DB 05 00 00 00 00 fild dword ptr [?nGlobalData@@3HA] 00000094: 83 EC 08 sub esp,8 00000097: DD 1C 24 fstp qword ptr [esp] 0000009A: 8D 4D EC lea ecx,[ebp-14h] 0000009D: FF 15 00 00 00 00 call dword ptr [__imp_?SubData@DemoMath@@QAEXNN@Z] 000000A3: 3B F4 cmp esi,esp 000000A5: E8 00 00 00 00 call __RTC_CheckEsp |
在上面的代碼中,地址0x0000008E處引用了全局變量nGlobalData,指令格式爲:DB 05 00 00 00 00。DB 05爲fild彙編指令的二進制碼,然後邊四個字節的零(紅色表示)是nGlobalData的地址,這個地址是個臨時的假值。
在當前目標文件中,若是被調用的函數或數據位於另一個目標文件中,那麼在連接的時候須要對被調用的函數或數據執行重定位;若是被調用的函數或數據是全局函數或者全局變量,那麼在連接的時候,須要對該全局函數或全局變量執行重定位。在示例代碼中,全局變量:nGlobalData, nOperTimes,全局函數:GetOperTimes()在連接的時候須要執行重定位。
重定位表只存在於目標文件中,它存儲了各個段的重定位信息。在每一個段的段表中,記錄了該段重定位信息在重定位表中的位置(相對於文件首位置的偏移)。
使用工具dumpbin將目標文件的內容導出後,若是某個代碼段存在重定位信息(該代碼段引用過了全局符號或者外部符號),那麼在該代碼段的後面就會列出該代碼段的重定位信息。該重定位信息是重定位表中的一個片斷。示例以下:
SECTION HEADER #16 //代碼段的信息摘要。Subdata函數所在的代碼段 .text name 0 physical address 0 virtual address 5C size of raw data 2088 file pointer to raw data (00002088 to 000020E3) 20E4 file pointer to relocation table 0 file pointer to line numbers 4 number of relocations 0 number of line numbers 60501020 flags Code COMDAT; sym= "public: void __thiscall DemoMath::SubData(double,double)" 16 byte align Execute Read
RAW DATA #16 //代碼段的二進制數據內容,紅色字體表示須要重定位的位置。被//VirtualAddress字段指定。 00000000: 55 8B EC 81 EC CC 00 00 00 53 56 57 51 8D BD 34 U.ì.ìì...SVWQ.?4 00000010: FF FF FF B9 33 00 00 00 B8 CC CC CC CC F3 AB 59 ???13...?ììììó?Y 00000020: 89 4D F8 A1 00 00 00 00 83 C0 01 A3 00 00 00 00 .M??.....à.£.... 00000030: DD 45 08 DC 65 10 83 EC 08 DD 1C 24 8B 45 F8 8B YE.üe..ì.Y.$.E?. 00000040: 08 E8 00 00 00 00 5F 5E 5B 81 C4 CC 00 00 00 3B .è...._^[.?ì...; 00000050: EC E8 00 00 00 00 8B E5 5D C2 10 00 ìè.....?]?.. RELOCATIONS #16 //代碼段的重定位信息。 Symbol Symbol Offset Type Applied To Index Name -------- ---------------- ----------------- -------- ------ 00000024 DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 0000002C DIR32 00000000 F ?nOperTimes@@3HA (int nOperTimes) 00000042 REL32 00000000 59 ?OutPutInfo@DemoOutPut@@QAEXN@Z 00000052 REL32 00000000 3F __RTC_CheckEsp |
這是類DemoMath的成員函數:SubData()所在的代碼段的重定位信息,在該重定位信息中,須要重定位的符號是:全局變量nOperTimes和外部函數OutPutInfo()。在上面代碼中,紅色字體的部分被重定位表中的字段:VirtualAddress指向,標記了須要重定位的位置。
在「WinNT.h」頭文件中,文件頭被定義爲IMAGE_RELOCATION類型,具體的定義形式以下:
typedef struct _IMAGE_RELOCATION { union { DWORD VirtualAddress; DWORD RelocCount; // Set to the real count when IMAGE_SCN_LNK_NRELOC_OVFL is set }; DWORD SymbolTableIndex; WORD Type; } IMAGE_RELOCATION; typedef IMAGE_RELOCATION UNALIGNED *PIMAGE_RELOCATION; |
在重定位表中,各個字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
VirtualAddress |
DWORD |
該字段指向代碼段中的一個地址。該地址所包含的數據是須要重定位的符號的地址。這部分數據將要被重定位修正。該字段指向了這個數據的第一個字節。上面的示例中,紅色字體標記的部分被該字段指向。 |
RelocCount |
DWORD |
|
SymbolTableIndex |
DWORD |
須要重定位的符號在符號表中的索引,該值爲符號表數組的索引。經過該值能夠檢索符號在符號表中的信息。 |
Type |
WORD |
通常分兩種類型。DIR32表示32位絕對地址;REL32表示32位相對地址。在執行重定位的時候,對於絕對地址類型,將被替換爲符號的絕對地址;而對於相對地址類型,將被替換爲符號的相對地址,即:符號相對於被修正位置的地址差。 |
行號表描述了二進制代碼與源代碼行號之間的關係,調試階段使用。在「WinNT.h」頭文件中,文件頭被定義爲IMAGE_RELOCATION類型,具體的定義形式以下:
typedef struct _IMAGE_LINENUMBER { union { DWORD SymbolTableIndex; // Symbol table index of function name if Linenumber is 0. DWORD VirtualAddress; // Virtual address of line number. } Type; WORD Linenumber; // Line number. } IMAGE_LINENUMBER; typedef IMAGE_LINENUMBER UNALIGNED *PIMAGE_LINENUMBER; |
在行號表中,各個字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
SymbolTableIndex |
DWORD |
符號在符號表中的索引 |
VirtualAddress |
DWORD |
符號的地址值 |
Linenumber |
WORD |
行號 |
在編譯階段的詞法分析過程當中,編譯器掃描整個C++源代碼,將源代碼中的函數名稱,變量名稱收集起來,而後寫入符號表中。在符號表中主要包含以下內容:函數名稱,變量名稱,段的名稱,以及一些常量信息,這些名稱被統稱爲符號。
符號表中的信息被用於靜態連接階段,用來進行被引用的函數或變量的地址重定位。每個目標文件中都會包含一個符號表。在該符號表中的符號,要麼是在該目標文件中定義的函數名稱或變量名稱;要麼是被該目標文件引用的,定義於其餘目標文件中的函數名稱或變量名稱。在靜態連接階段,多個目標文件進行連接的時候,存在於這些目標文件中的符號表會被合併到一塊兒,造成一個全局符號表。在C++源代碼中出現的全部符號都應該能在全局符號表中被查找到。
將符號表中的符號進行分類,具體的分類狀況以下:
在執行連接的時候,只關注前兩種類型的符號。
若是符號的名稱小於8個字節,那麼將該符號的名稱直接存儲在符號表中;若是符號的名稱大於8個字節,那麼將符號的名稱存儲在字符串表中,原來符號表中存儲符號名稱的地方存儲了一個地址偏移量,該地址偏移量指向了字符串表中符號名稱的位置。
根據符號存儲類型以及符號在段中位置的不一樣,符號的值有不一樣的解釋。
使用工具dumpbin將DemoMath.obj的內容導出之後,其符號表中的一部分的內容描述以下:
000 00847809 ABS notype Static | @comp.id //絕對值常量 001 00000001 ABS notype Static | @feat.00 //絕對值常量 002 00000000 SECT1 notype Static | .drectve //段名稱 //段名稱符號下面緊跟段的信息。每行佔用一個符號索引的位置,因此符號索引不是連續的。 Section length 201, #relocs 0, #linenums 0, checksum 0 Relocation CRC 00000000 005 00000000 SECT4 notype External | ?nOperTimes@@3HA (int nOperTimes) //變量 006 00000000 SECT1A notype () External | ?DivData@DemoMath@@QAEXNN@Z //函數 007 00000000 UNDEF notype () External | ?OutPutInfo@DemoOutPut@@QAEXPBD@Z //外部函數
|
在上面的示例中,從左到右各字段的含義依次是:符號結構體所在數組的索引,符號大小,符號在段中位置,符號類型,符號的存儲類型,符號名稱。在該符號表的內容中,列出了全局變量名:nOperTimes,類成員函數名:DivData,被引用的外部函數名:OutPutInfo。段的名稱也被做爲一個符號寫入到符號表中,上面示例中的「.drectve」即爲一個段的名稱。
在「WinNT.h」頭文件中,文件頭被定義爲IMAGE_SYMBOL類型,具體的定義形式以下:
typedef struct _IMAGE_SYMBOL { union { BYTE ShortName[8]; Struct { DWORD Short; // if 0, use LongName DWORD Long; // offset into string table } Name; DWORD LongName[2]; // PBYTE [2] } N; DWORD Value; SHORT SectionNumber; WORD Type; BYTE StorageClass; BYTE NumberOfAuxSymbols; } IMAGE_SYMBOL; typedef IMAGE_SYMBOL UNALIGNED *PIMAGE_SYMBOL; |
在符號表中,各個字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
ShortName |
BYTE |
小於8個字節的符號名稱存儲於此。 |
Short |
DWORD |
0表示符號名稱位於字符串表中。 |
Long |
DWORD |
符號名稱在字符串表中的偏移量。 |
LongName |
DWORD |
|
Value |
DWORD |
符號的值。對於變量或函數來講,符號值就是它們的地址。根據符號存儲類型的不一樣,符號值有不一樣的解釋。 |
SectionNumber |
SHORT |
符號所在的段落。ABS表示符號是個絕對值,是個常量;UNDEF表示符號是未定義的,即該符號的定義在其餘段中;SECT1表示該符號位於編號爲1的段中。 |
Type |
WORD |
符號的類型。Notype表示變量;notype()表示函數。 |
StorageClass |
BYTE |
符號的存儲類型。Static表示局部變量,文件內部可見;external表示全局變量,全局範圍內可見。 |
NumberOfAuxSymbols |
BYTE |
附加記錄的數量。 |
符號的值的具體含義須要根據符號所在的段落(SectionNumber)以及符號的存儲類型(StorageClass)來肯定,這三者之間的具體關係以下表所示:
StorageClass |
SectionNumber |
Value |
Static |
SECTn(n爲1,2,3…) |
若是值不爲零,表示符號在段內偏移。 |
SECTn(n爲1,2,3…) |
若是值爲零,表示這個符號爲段名。 |
|
ABS |
常量的值。 |
|
External |
UNDEF |
符號爲全局變量/函數,符號定義在外部文件中,值待定。 |
SECTn(n爲1,2,3…) |
符號爲全局變量/函數,符號定義在當前文件中,值表示符號在段內偏移。 |
字符串表用來保存長度大於8個字節的符號名稱。字符串表的前4個字節表示字符串的長度,後面的緊跟字符串的內容,它以字節爲單位,以’\0’做爲字符串的結束符。這裏的字符串長度不只僅是字符串自身的長度(字符串內容+’\0’),還包括前面4個字節的該數據自身的長度。
在COFF文件所包含的數據結構中,各個數據結構之間的關係以下圖所示:
重定位表和符號表之間經過符號表的索引進行關聯;在文件頭中保存了可選頭的大小和段表所包含項目的數量,經過計算能夠肯定段表的起始位置和結束位置。段表起始位置=文件頭大小+可選頭大小;其餘關係經過相對文件首位置的偏移表示。
靜態連接庫就是一組目標文件的集合,當執行靜態連接的時候,被選定的目標文件的內容就會被合併到相關的Pe文件中去。靜態連接庫的整體結構以下圖所示:
靜態連接庫以簽名開始,簽名的數據內容爲「(!<arch>\n」,長8個字節。緊跟在簽名後面的是三個特別成員,分別是第一連接器節,第二連接器節,以及長名稱節。在這三個特別成員以後,直到文件結束,存儲的都是目標文件節的內容。
第一連接器節,第二連接器節,長名稱節,以及目標文件節的數據結構都是由頭數據+節數據這樣的數據結構組成的。
第一連接器節。在靜態連接庫中必須存在該節,它包含了靜態連接庫中全部的符號名以及這些符號在靜態連接庫文件中的偏移;
第二連接器節。在靜態連接庫中該節可選,它包含了與第一連接器節相同的內容,可是它的內容是有序的,經過它查找符號要比在第一連接器節中查找的快;
長名稱節。在靜態連接庫中該節可選,它是一個字符串表,用於存儲名稱大於16個字節的目標文件的名稱。在目標文件節中,若是目標文件的名稱小於16個字節,那麼這個名稱會被存儲在頭文件的名稱域;若是這個名稱大於16個字節,那麼這個名稱就會被存儲到這裏。而在頭文件的名稱域存儲的則是該字符串在長名稱節的偏移。
目標文件節。該節是靜態連接庫的主要內容,節的數量不定,它存儲了若干各目標文件的內容,每一節都是頭信息+目標文件的結構。目標文件的結構與2.2節描述的一致。
在WinNT.h頭文件中,頭信息被定義爲IMAGE_ARCHIVE_MEMBER_HEADER類型,具體的定義內容以下:
typedef struct _IMAGE_ARCHIVE_MEMBER_HEADER { BYTE Name[16]; // File member name - `/' terminated. BYTE Date[12]; // File member date - decimal. BYTE UserID[6]; // File member user id - decimal. BYTE GroupID[6]; // File member group id - decimal. BYTE Mode[8]; // File member mode - octal. BYTE Size[10]; // File member size - decimal. BYTE EndHeader[2]; // String to end header. } IMAGE_ARCHIVE_MEMBER_HEADER, *PIMAGE_ARCHIVE_MEMBER_HEADER; |
從文件內容上來看,PE文件由二進制數據組成。這些二進制數據從文件的零位置開始,依次存儲,直到文件末尾。從數據結構的角度來看,這些二進制數據又分別屬於不一樣的結構體或者結構體數組。這些結構體被定義在「WinNT.h」頭文件中。
在PE文件中,這些結構體或結構體數組分別表示不一樣的含義,記錄着PE文件中的不一樣內容。從文件的頂端開始,依次存儲了DOS頭,PE頭,段表,各段詳細數據等信息。在進行信息字段定位的時候,PE文件採用兩種方式:1利用指針。好比:在Dos頭中存儲一個指向PE頭的指針;2利用數據結構的大小。在PE的頭部信息中,一些數據結構的大小是固定的。在數據存儲的時候,各個數據結構緊湊存放,中間沒有空隙。在這種狀況下,以一個數據結構的字段爲基點,經過計算數據結構佔用空間的大小,就能夠定位另一個數據結構的位置。
使用dumpbin工具能夠將PE文件的內容導出,具體的命令格式以下:
Dumpbin /all DemoDlld.dll >DemoDll.txt |
在上面的命令中,將PE文件「DemoDlld.dll」的全部內容導出到文本文件「DemoDll.txt」中。命令選項「/all」表示導出全部內容,命令選項「>」表示將導出的內容存儲到文件中。
全部 PE文件都必須以DOS MZ header開始,它是一個IMAGE_DOS_HEADER的結構。有了它,一旦程序在DOS下執行,DOS就能識別出這是有效的執行體,而後運行緊隨MZ Header以後的DOS Stub。
在「WinNT.h」頭文件中,DOS MZ 頭被定義爲IMAGE_DOS_HEADER類型,具體的定義形式以下:
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // 魔術數字 WORD e_cblp; // 文件最後頁的字節數 WORD e_cp; // 文件頁數 WORD e_crlc; // 重定義元素個數 WORD e_cparhdr; // 頭部尺寸,以段落爲單位 WORD e_minalloc; // 所需的最小附加段 WORD e_maxalloc; // 所需的最大附加段 WORD e_ss; // 初始的SS值 WORD e_sp; // 初始的SP值 WORD e_csum; // 校驗和 WORD e_ip; // 初始的IP值 WORD e_cs; // 初始的CS值 WORD e_lfarlc; // 重分配表文件地址 WORD e_ovno; // 覆蓋號 WORD e_res[4]; // 保留字 WORD e_oemid; // OEM 標識符 WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // 保留字 LONG e_lfanew; // PE頭的地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; |
在DOS頭中,第一個域「e_magic」被稱爲魔術數字,它用於表示一個MS-DOS兼容的文件類型。全部MS-DOS兼容的可執行文件都將這個值設爲0x5A4D,表示ASCII字符MZ。MS-DOS頭部之因此有的時候被稱爲MZ頭部,就是這個緣故。
對於MS-DOS操做系統來講,許多其餘的域都是有用的。可是對於 Windows NT來講,只有最後一個域e_lfnew是有用的,該域是一個指針,佔用4個字節,用於指明PE頭在文件中的位置。
DOS Stub其實是個有效的EXE,在不支持PE文件格式的操做系統中,它將簡單顯示一個錯誤提示,相似於字符串「This program requires Windows」,或者程序員可根據本身的意圖實現完整的DOS代碼。大多數狀況下DOS Stub由彙編器/編譯器自動生成。
PE頭緊跟在DOS MS頭以及實模式程序殘餘以後,在WinNt.h頭文件中,PE頭被定義爲IMAGE_NT_HEADER類型,具體的定義內容以下所示:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //PE頭標識 IMAGE_FILE_HEADER FileHeader; //PE文件物理分佈信息 IMAGE_OPTIONAL_HEADER32 OptionalHeader; //PE文件邏輯分佈信息 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; |
在該結構體數據中,除了包含PE頭標識外,又嵌套了兩個結構體數據,分別是PE文件頭的信息,以及PE可選頭的信息。
在一個有效的PE文件中,PE頭標識字段的值是0x00004550,用ASCII表示就是「PE00」。 #define IMAGE_NT_SIGNATURE定義了這個值。
見2.2.2節描述。PE中的文件頭與COFF中的文件頭定義一致。
在PE文件頭以後,是PE可選頭。該頭224字節大小,它包含了許多重要的信息,例如:初始堆棧大小,程序的入口地址,首選加載基地址,操做系統版本,段對齊等。該頭並不是可選,而是必需要有的頭。
在可選頭中,包含了三類主要信息,分別是:標準域信息,WinNT附加信息,以及數據目錄信息。
所謂標準域就是指和UNIX可執行文件的COFF格式所公共的部分,雖然標準域中保留了COFF文件中定義的名稱,可是WindowsNT仍然將它用做了不一樣的目的。
在操做系統的加載器加載PE文件的時候,WinNT附件域的信息爲加載器提供了支持。
在可執行文件中有許多數據結構須要被快速定位,數據目錄提供了這種支持。數據目錄是一個指針列表,在該列表中保存了一系列的指針值,這些指針指向了其餘的數據表。如:導入表,導出表,資源表,重定位表等。數據目錄以指針的形式提供了一種信息查找的方式。
使用工具dumpbin能夠將PE文件的內容導出,在該內容中包含了描述可選頭的摘要信息,具體的信息內容以下:
OPTIONAL HEADER VALUES //如下爲標準域的信息 10B magic # (PE32) 9.00 linker version 5800 size of code 4800 size of initialized data 0 size of uninitialized data 11159 entry point (10011159) @ILT+340(__DllMainCRTStartup@12) 1000 base of code 1000 base of data //如下爲WinNT附加域的信息 10000000 image base (10000000 to 1001CFFF) 1000 section alignment 200 file alignment 5.00 operating system version 0.00 image version 5.00 subsystem version 0 Win32 version 1D000 size of image 400 size of headers 0 checksum 2 subsystem (Windows GUI) 140 DLL characteristics Dynamic base NX compatible 100000 size of stack reserve 1000 size of stack commit 100000 size of heap reserve 1000 size of heap commit 0 loader flags 10 number of directories //如下爲數據目錄的信息 18AD0 [ 2BA] RVA [size] of Export Directory 1A000 [ 50] RVA [size] of Import Directory 1B000 [ C09] RVA [size] of Resource Directory 0 [ 0] RVA [size] of Exception Directory 0 [ 0] RVA [size] of Certificates Directory 1C000 [ 38C] RVA [size] of Base Relocation Directory 17520 [ 1C] RVA [size] of Debug Directory 0 [ 0] RVA [size] of Architecture Directory 0 [ 0] RVA [size] of Global Pointer Directory 0 [ 0] RVA [size] of Thread Storage Directory 0 [ 0] RVA [size] of Load Configuration Directory 0 [ 0] RVA [size] of Bound Import Directory 1A224 [ 1D4] RVA [size] of Import Address Table Directory 0 [ 0] RVA [size] of Delay Import Directory 0 [ 0] RVA [size] of COM Descriptor Directory 0 [ 0] RVA [size] of Reserved Directory |
在WinNT.h頭文件中,可選頭被定義爲IMAGE_OPTIONAL_HEADER類型,具體的定義內容描述以下:
//數據目錄中數據元素的個數 #define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 //可選頭的定義 typedef struct _IMAGE_OPTIONAL_HEADER { //標準域 WORD Magic; //魔法數字 BYTE MajorLinkerVersion;//連接器的最大版本號 BYTE MinorLinkerVersion;//連接器的最小版本號 DWORD SizeOfCode;//可執行代碼長度 DWORD SizeOfInitializedData;//初始化數據的長度(data段) DWORD SizeOfUninitializedData;//未初始化數據的長度(bss段) DWORD AddressOfEntryPoint;//代碼的入口地址,程序今後處開始執行 DWORD BaseOfCode;//可執行代碼的起始位置 DWORD BaseOfData;//初始化數據的起始位置 //NT附件域 DWORD ImageBase;//載入程序首選的相對虛擬地址 DWORD SectionAlignment;//段加載到內存之後的對齊方式 DWORD FileAlignment;//段在文件中的對齊方式 WORD MajorOperatingSystemVersion;//操做系統最大版本號 WORD MinorOperatingSystemVersion;//操做系統最小版本號 WORD MajorImageVersion;//程序最大版本號 WORD MinorImageVersion;//程序最小版本號 WORD MajorSubsystemVersion;//子程序最大版本號 WORD MinorSubsystemVersion;//子程序最小版本號 DWORD Win32VersionValue;//這個值一直爲零 DWORD SizeOfImage;//程序加載到內存之後,佔用內存的大小。 DWORD SizeOfHeaders;//文件頭部總大小 DWORD CheckSum;//校驗和 WORD Subsystem;//一個標明可執行文件所指望的子系統的枚舉值 WORD DllCharacteristics;//dll狀態 DWORD SizeOfStackReserve;//保留棧大小 DWORD SizeOfStackCommit;//啓動後實際申請棧數 DWORD SizeOfHeapReserve;//保留堆的大小 DWORD SizeOfHeapCommit;//啓動後實際申請堆數 DWORD LoaderFlags; DWORD NumberOfRvaAndSizes; //數據目錄的定義 IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32; |
在可選頭中,各個字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
Magic |
WORD |
一個簽名,肯定這是什麼類型的頭。兩個最經常使用的值是IMAGE_NT_OPTIONAL_HDR32_MAGIC 0x10b 和IMAGE_NT_OPTIONAL_HDR64_MAGIC 0x20b. |
MajorLinkerVersion |
BYTE |
建立可執行文件的連接器的主版本號。對於Microsoft的連接器生成的PE文件,這個版本號的Visual Studio的版本號相一致 |
MinorLinkerVersion |
BYTE |
建立可執行文件的連接器的次版本號 |
SizeOfCode |
DWORD |
全部具備IMAGE_SCN_CNT_CODE屬性的節的總的大小 |
SizeOfInitializedData |
DWORD |
全部包含已初始數據的節的總的大小。 |
SizeOfUninitializedData |
DWORD |
全部包含未初始化數據的節的總的大小。這個域老是0,由於連接器能夠把未初始化數據附加到常規數據節的末尾。 |
AddressOfEntryPoint |
DWORD |
文件中將被執行的第一個代碼字節的RVA。對於DLL,這個進入點將在進程初始化和關閉時,以及線程被建立和銷燬時調用。在大多數可執行文件中,這個地址並不直接指向main,WinMain或DllMain函數,而是指向運行時庫代碼,由運行時庫調用前述函數。在DLL中,這個域能夠被設爲0,這樣的話上面所說的通知就不能被接收到。連接器選項/NOENTRY能夠設置這個域爲0。 |
BaseOfCode |
DWORD |
加載到內存後代碼的第一個字節的RVA。 |
BaseOfData |
DWORD |
理論上,它表示加載到內存後數據的第一個字節的RVA。然而,這個域的值對於不一樣版本的Microsoft連接器是不一致的。在64位的可執行文件中這個域不出現。 |
ImageBase |
DWORD |
文件在內存中的首選加載地址。加載器儘量地把PE文件加載到這個地址(就是說,若是當前這塊內存沒有被佔用,它是對齊的而且是一個合法的地址,等等)。若是可執行文件被加載到這個地址,加載器就能夠跳過進行基址重定位(在這篇文章的第二部分描述)這一步。對於EXE,缺省的ImageBase是0x400000。對於DLL,缺省是0x10000000。在連接時能夠經過/BASE 選項來指定ImageBase,或者之後用REBASE工具從新設置。 |
SectionAlignment |
DWORD |
加載到內存後節的對齊大小。這個值必須大於等於FileAlignment(下一個域)。缺省的對齊值是目標CPU的頁大上。對於運行在Windows 9x或Windows Me下的用戶模式可執行文件,最小對齊大小是一頁(4KB)。這個域能夠經過連接器選項/ALIGN來設置。 |
FileAlignment |
DWORD |
在PE文件中節的對齊大小。對於x86下的可執行文件,這個值一般是0x200或0x1000。不一樣版本的Microsoft連接器缺省值不一樣。這個值必須是2的冪,而且若是SectionAlignment小於CPU的頁大小,這個域必須和SectionAlignment相匹配。連接器選項/OPT:WIN98可設置x86可執行文件的文件對齊爲0x1000,/OPT:NOWIN98設置文件對齊爲0x200。 |
MajorOperatingSystemVersion |
WORD |
所要求的操做系統的主版本號。隨着那麼多版本Windows的出現,這個域的值就變得很不確切。 |
MinorOperatingSystemVersion |
WORD |
所要求的操做系統的次版本號。 |
MajorImageVersion |
WORD |
這個文件的主版本號。不被系統使用並可設爲0。能夠經過連接器選項/VERSION來設置。 |
MinorImageVersion |
WORD |
這個文件的次版本號。 |
MajorSubsystemVersion |
WORD |
可執行文件所要求的操做子系統的主版本號。它曾經被用來表示須要較新的Windows 95或Windows NT用戶界面,而不是老版本的Windows NT界面。今天隨着各類不一樣版本Windows的出現,這個域已不被系統使用,而且一般被設爲4。可經過連接器選項/SUBSYSTEM設置這個域的值。 |
MinorSubsystemVersion |
WORD |
執行文件所要求的操做子系統的次版本號。 |
Win32VersionValue |
DWORD |
不被使用的域,一般設爲0。 |
SizeOfImage |
DWORD |
映像的大小。它表示了加載文件到內存中時系統必須保留的內存的數量。這個域的值必須是SectionAlignmnet的倍數。 |
SizeOfHeaders |
DWORD |
MS-DOS頭,PE頭和節表的總的大小。PE文件中全部這些項目出如今任何代碼或數據節以前。這個域的值被調整爲文件對齊大小的整數倍。 |
CheckSum |
DWORD |
映像的校驗和。IMAGEHLP.DLL中的CheckSumMappedFile函數能夠計算出這個值。校驗和用於內核模式的驅動和一些系統DLL。對於其它的,這個域能夠爲0。當使用連接器選項/RELEASE時校驗和被放入文件中。 |
Subsystem |
WORD |
指示可執行文件指望的子系統(用戶界面類型)的枚舉值。這個域只用於EXE。一些重要的值包括: IMAGE_SUBSYSTEM_NATIVE // 映像不須要子系統 IMAGE_SUBSYSTEM_WINDOWS_GUI // 使用Windows GUI IMAGE_SUBSYSTEM_WINDOWS_CUI // 做爲控制檯程序運行。 // 運行時,操做系統建立一個控制檯 // 窗口並提供stdin,stdout和stderr // 文件句柄。 |
DllCharacteristics |
WORD |
標記DLL的特性。對應於IMAGE_DLLCHARACTERISTICS_xxx定義。當前的值是: IMAGE_DLLCHARACTERISTICS_NO_BIND // 不要綁定這個映像 IMAGE_DLLCHARACTERISTICS_WDM_DRIVER // WDM模式的驅動程序 IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE // 當終端服務加載一個不是 // Terminal- Services-aware 的應用程 // 序時,它也加載一個包含兼容代碼 // 的DLL。 |
SizeOfStackReserve |
DWORD |
在EXE文件中,爲線程保留的堆棧大小。缺省是1MB,但並非全部的內存一開始都被提交。 |
SizeOfStackCommit |
DWORD |
在EXE文件中,爲堆棧初始提交的內存數量。缺省狀況下,這個域是4KB。 |
SizeOfHeapReserve |
DWORD |
在EXE文件中,爲默認進程堆初始保留的內存大小。缺省是1MB。然而在當前版本的Windows中,堆不通過用戶干涉就能超出這裏指定的大小。 |
SizeOfHeapCommit |
DWORD |
在EXE文件中,提交到堆的內存大小。缺省狀況下,這裏的值是4KB。 |
LoaderFlags |
DWORD |
不使用。 |
NumberOfRvaAndSizes |
DWORD |
在IMAGE_NT_HEADERS結構的末尾是一個IMAGE_DATA_DIRECTORY結構數組。此域包含了這個數組的元素個數。自從最先的Windows NT發佈以來這個域的值一直是16。 |
數據目錄一共有16項,每一項都存儲一個指向其餘數據表的指針,數據目錄爲數據的快速定位提供了支持。在WinNT.h頭文件中,數據目錄被定義爲IMAGE_DATA_DIRECTORY,具體的定義內容描述以下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; //被指向元素表的起始RVA地址。 DWORD Size; //被指向元素表的長度 } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 |
在數據目錄的16項元素中,每一項都預約義了它的含義,IMAGE_DIRECTORY_ENTRY_XXX定義了數據目錄數組的索引。該索引的具體含義描述以下表:
序號 |
索引 |
描述 |
0 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
指向導出表(一個IMAGE_EXPORT_DIRECTORY結構)。 |
1 |
IMAGE_DIRECTORY_ENTRY_IMPORT |
指向導入表(一個IMAGE_IMPORT_DESCRIPTOR結構數組)。 |
2 |
IMAGE_DIRECTORY_ENTRY_RESOURCE |
指向資源(一個IMAGE_RESOURCE_DIRECTORY結構。 |
3 |
IMAGE_DIRECTORY_ENTRY_EXCEPTION |
指向異常處理表(一個IMAGE_RUNTIME_FUNCTION_ENTRY結構數組)。CPU特定的而且基於表的異常處理。用於除x86以外的其它CPU上。 |
4 |
IMAGE_DIRECTORY_ENTRY_SECURITY |
指向一個WIN_CERTIFICATE結構的列表,它定義在WinTrust.H中。不會被映射到內存中。所以,VirtualAddress域是一個文件偏移,而不是一個RVA。 |
5 |
IMAGE_DIRECTORY_ENTRY_BASERELOC |
指向基址重定位信息。 |
6 |
IMAGE_DIRECTORY_ENTRY_DEBUG |
指向一個IMAGE_DEBUG_DIRECTORY結構數組,其中每一個結構描述了映像的一些調試信息。早期的Borland連接器設置這個IMAGE_DATA_DIRECTORY結構的Size域爲結構的數目,而不是字節大小。要獲得IMAGE_DEBUG_DIRECTORY結構的數目,用IMAGE_DEBUG_DIRECTORY 的大小除以這個Size域。 |
7 |
IMAGE_DIRECTORY_ENTRY_ARCHITECTURE |
指向特定架構數據,它是一個IMAGE_ARCHITECTURE_HEADER結構數組。不用於x86或IA-64,但看來已用於DEC/Compaq Alpha。 |
8 |
IMAGE_DIRECTORY_ENTRY_GLOBALPTR |
在某些架構體系上VirtualAddress域是一個RVA,被用來做爲全局指針(gp)。不用於x86,而用於IA-64。Size域沒有被使用。參見2000年11月的Under The Hood 專欄可獲得關於IA-64 gp的更多信息。 |
9 |
IMAGE_DIRECTORY_ENTRY_TLS |
指向線程局部存儲初始化節。 |
10 |
IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG |
指向一個IMAGE_LOAD_CONFIG_DIRECTORY結構。IMAGE_LOAD_CONFIG_DIRECTORY中的信息是特定於Windows NT、Windows 2000和 Windows XP的(例如 GlobalFlag 值)。要把這個結構放到你的可執行文件中,你必須用名字__load_config_used 定義一個全局結構,類型是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 = {...} |
11 |
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT |
指向一個 IMAGE_BOUND_IMPORT_DESCRIPTOR結構數組,對應於這個映像綁定的每一個DLL。數組元素中的時間戳容許加載器快速判斷綁定是不是新的。若是不是,加載器忽略綁定信息而且按正常方式解決導入API。 |
12 |
IMAGE_DIRECTORY_ENTRY_IAT |
指向第一個導入地址表(IAT)的開始位置。對應於每一個被導入DLL的IAT都連續地排列在內存中。Size域指出了全部IAT的總的大小。在寫入導入函數的地址時加載器使用這個地址和Size域指定的大小臨時地標記IAT爲可讀寫。 |
13 |
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT |
指向延遲加載信息,它是一個CImgDelayDescr結構數組,定義在Visual C++的頭文件DELAYIMP.H中。延遲加載的DLL直到對它們中的API進行第一次調用發生時纔會被裝入。Windows中並無關於延遲加載DLL的知識,認識到這一點很重要。延遲加載的特徵徹底是由連接器和運行時庫實現的。 |
14 |
IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR |
在最近更新的系統頭文件中這個值已被更名爲IMAGE_DIRECTORY_ENTRY_COMHEADER。它指向可執行文件中.NET信息的最高級別信息,包括元數據。這個信息是一個IMAGE_COR20_HEADER結構。 |
15 |
IMAGE_DIRECTORY_ENTRY_EXPORT |
指向導出表(一個IMAGE_EXPORT_DIRECTORY結構)。 |
當一個可執行程序或者動態連接庫調用另一個動態連接庫中的變量或者函數的時候,必須將這些被調用函數或者變量導入。這些被導入的變量或函數是必須是被調用動態連接庫導出表中的一個子集。也就是說,只有當這些變量或者函數被導出之後,才能被其餘可執行程序或者動態連接庫導入。
在PE文件中,咱們將這些變量和函數統稱爲符號,這些被導入的符號的信息被存儲在導入表中。在PE文件中,導入表位於.idata段中,該段能夠單獨存在。
使用工具dumpbin將可執行成DemoExe.exe中的內容導出,.idata段的部份內容以下所示:
//.idata段的信息摘要 SECTION HEADER #5 .idata name 1130 virtual size 1A000 virtual address (0041A000 to 0041B12F) 1200 size of raw data 7A00 file pointer to raw data (00007A00 to 00008BFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers C0000040 flags Initialized Data Read Write //idata段的二進制數據內容 RAW DATA #5 0041A000: 64 A0 01 00 00 00 00 00 00 00 00 00 B4 A5 01 00 d?..........′¥.. 0041A010: B0 A2 01 00 58 A1 01 00 00 00 00 00 00 00 00 00 °¢..X?.......... 0041A020: 30 AB 01 00 A4 A3 01 00 F4 A1 01 00 00 00 00 00 0?..¤£..??...... ……………. //導入表的信息 Section contains the following imports:
DemoDLLd.dll 41A2B0 Import Address Table //導入地址表地址 41A064 Import Name Table //導入名稱表地址 0 time date stamp 0 Index of first forwarder reference //導出地址表數組下標 符號名稱 6 ?GetOperTimes@@YAHXZ 4 ?Area@DemoMath@@QAEXN@Z 5 ?DivData@DemoMath@@QAEXNN@Z 8 ?SubData@DemoMath@@QAEXNN@Z 3 ?AddData@DemoMath@@QAEXNN@Z 0 ??0DemoMath@@QAE@XZ 1 ??1DemoMath@@QAE@XZ |
對於每個可執行程序或者動態連接庫,只要它調用了其餘動態連接庫中的符號,那麼就會有一個或多個IMAGE_IMPORT_DESCRIPTOR類型的數組與之對應。該數組元素的個數與該可執行程序或者動態連接庫所依賴的動態連接庫的數量有關。
在IMAGE_IMPORT_DESCRIPTOR類型的數據結構中,存儲的是與導入表相關的信息。該數據結構與其餘數據結構發生關聯,與該數據結構發生關聯的數據實體包括:數據目錄,字符串表,導入地址表,導入名稱表。它們之間的關係以下圖所示:
在可選頭中包含了數據目錄,在數據目錄的第二項中,存儲了指向導入表的位置的指針,以及該數據結構的大小。在數據目錄的第十二項中,存儲了指向第一個IAT的位置的指針,以及全部IAT數組的大小。
導入表是一個數組,該數據元素的類型是IMAGE_IMPORT_DESCRIPTOR。在該數組中,每個數組元素都會對應一個被導入的DLL。在該數組的末尾,存儲了一個全部的域爲零的IMAGE_IMPORT_DESCRIPTOR類型的數組元素,表示該導入表結束。所以,在一個可執行程序或者動態連接庫的導入表中,至少會包含兩個數據元素。一個對應被導入符號的信息,一個全部域爲零。
在每一個導入表的數組的元素中,擁有兩個地址字段,它們分別指向了導入地址表(IAT)和導入名稱表(INT)。在導入地址表和導入名稱表中,存儲了被導入符號的名稱或地址,它們是一個擁有多個數據元素的數組。由於導入表的數組元素多是多個(當前可執行程序或動態連接庫依賴多個其餘的動態連接庫),因此在一個動態連接庫的導入信息中也會存在多個IAT數組以及INT數組。當PE文件被加載到內存之後,這些IAT數組會被存儲在一塊連續地內存區域中,它們首尾相連,中間沒有空隙。在數據目錄的第十二項中,存儲了第一個IAT的位置,以及全部IAT數組的大小。導入表的結構佈局以下圖所示:
導入地址表和導入名稱表的數據結構是一致的,它們均爲IMAGE_THUNK_DATA類型,並以一個全部域均爲零的數組元素結尾。導入地址表用於動態連接,而導入名稱表用於符號地址綁定。在PE文件中,導入地址表和導入名稱表中的數據內容是同樣的,它們都存儲着被導入符號的名稱或者序號;在程序加載階段,導入名稱表的數據內容不會發生變化,而導入地址表中的被導入符號的序號或者名稱將會被替換爲該符號的真實虛擬內存地址。導入名稱表的數據內容永遠不會發生變化。
在WinNT.h頭文件中,導入表被定義爲IMAGE_IMPORT_DESCRIPTOR類型,具體的定義內容以下所示:
typedef struct _IMAGE_IMPORT_DESCRIPTOR { union { DWORD Characteristics; // 0 for terminating null import descriptor DWORD OriginalFirstThunk; // RVA to original unbound IAT (PIMAGE_THUNK_DATA) }; DWORD TimeDateStamp; // 0 if not bound, // -1 if bound, and real date\time stamp // in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND) // O.W. date/time stamp of DLL bound to (Old BIND)
DWORD ForwarderChain; // -1 if no forwarders DWORD Name; DWORD FirstThunk; // RVA to IAT (if bound this IAT has actual addresses) } IMAGE_IMPORT_DESCRIPTOR; typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR; |
在該定義中,各個字段的詳細解釋以下表所示:
字段名稱 |
類型 |
描述 |
Characteristics |
DWORD |
|
OriginalFirstThunk |
DWORD |
指向導入名稱表的相對虛擬地址。 |
TimeDateStamp |
DWORD |
若是可執行文件不與被導入DLL綁定時,該值爲0;若是以舊的樣式綁定時,改值爲時間戳;若是以新的樣式綁定時,該值爲-1。 |
ForwarderChain |
DWORD |
第一個被轉送符號的索引,若是沒有轉送,則設定爲-1。 |
Name |
DWORD |
該字段存儲了一個相對虛擬地址。該地址指向字符串表中某個字符串,這個字符串是導入DLL的名稱。 |
FirstThunk |
DWORD |
指向導入地址表的相對虛擬地址。 |
在WinNT.h頭文件中,IAT以及INT被定義爲IMAGE_THUNK_DATA類型,具體的定義內容以下所示:
typedef struct _IMAGE_THUNK_DATA32 { union { DWORD ForwarderString; // PBYTE DWORD Function; // PDWORD DWORD Ordinal; DWORD AddressOfData; // PIMAGE_IMPORT_BY_NAME } u1; } IMAGE_THUNK_DATA32; typedef IMAGE_THUNK_DATA32 * PIMAGE_THUNK_DATA32; |
從上面的定義能夠看出,IAT數據結構是一個聯合。在該聯合中,每個字段表明着在不一樣條件或者時間上,該數據結構可能存儲不一樣意義的數據。在該定義中,各個字段的詳細解釋以下表所示:
字段名稱 |
類型 |
描述 |
ForwarderString |
DWORD |
指向一個轉向字符串的相對虛擬地址。轉向導入的時候使用。 |
Function |
DWORD |
被導入符號的虛擬地址。動態連接完成之後寫入該值。 |
Ordinal |
DWORD |
被導入符號在導出表中的序號。編譯連接完畢後,該值可能會被寫入到PE文件中。該值與AddressofData字段互斥。 |
AddressOfData |
DWORD |
指向一個IMAGE_IMPORT_BY_NAME類型的數據結構。編譯連接完畢後,該值可能會被寫入到PE文件中。該值與Ordinal字段互斥。 |
在WinNT.h頭文件中,IMAGE_IMPORT_BY_NAME的定義內容以下:
typedef struct _IMAGE_IMPORT_BY_NAME { WORD Hint; BYTE Name[1]; } IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME; |
在上面的定義中,各字段的詳細解釋以下表:
字段名稱 |
類型 |
描述 |
Hint |
WORD |
導入符號最有可能的序號值。在動態連接的時候,當用符號名導入時,首先用Hint值在導出符號表中查找該符號,若是可以查找到,則命中;若是不能查找到,則使用符號名進行二分法查找。 |
Name |
BYTE |
符號名稱的字符串 |
在IAT數組中,每個數組元素都會對應一個被導入符號的地址,數組元素在不一樣的狀況下有不一樣的含義。具體的含義形式有四種,它們分別是:
在不一樣的時間點上,或者在不一樣的狀況下,被導出符號的地址用不一樣的形式表示。所以在數據結構定義的時候使用了聯合。
在源程序被編譯、連接完畢之後生成的PE文件中,以PE文件被加載到內存中,還沒有執行動態連接的時候,在IAT中符號的地址以序號或者符號名稱的形式表示;當執行完畢動態連接之後,使用符號的序號或者符號名稱,經過對相關DLL導出表的查找,這些符號的序號或者符號的名稱被替換成了符號的虛擬內存地址。
當IAT中的符號地址以序號或者符號名稱表示的時候,能夠經過DWORD值的最高位來區分是使用序號表示仍是使用符號名稱表示。若是最高位被置1,那麼該符號地址使用序號的形式表示,DWORD值的低31位表示序號;若是最高爲被置0,那麼該符號地址使用符號名稱的形式表示,DWORD值表示一個地址,指向了一個IMAGE_IMPORT_BY_NAME類型的數據結構。
在IMAGE_IMPORT_BY_NAME類型的數據結構中,包含了一個符號的名稱字符串,以及一個WORD類型的提示值,該提示值指示了符號在導出表中最可能的序號。在符號查找的時候,若是使用Hint值查找不能命中,那麼就會使用符號名稱進行二分法查找。
在一個動態連接庫文件中,總會有一些變量或者函數被聲明爲public,這些變量和函數被稱爲該動態連接庫的接口,它們能夠被其它可執行程序或者動態連接庫調用。在該動態連接庫中,並非全部被聲明爲public類型的接口都能被其餘可執行程序或動態連接庫調用,只有當這些接口被導出之後才能被其餘可執行程序或者動態連接庫調用。
在PE文件中,咱們將變量和函數統稱爲符號。這些被導出的符號的信息被存儲在導出表。通常狀況,導出表不會單獨存在,在輸出PE文件的時候,導出表可能會被合併到.rata段中,該段是隻讀數據段。
使用工具dumpbin將動態連接庫DemoDlld.dll的內容導出,.rdata段的部份內容以下所示:
//只讀段的摘要信息 SECTION HEADER #3 .rdata name 1D8A virtual size 17000 virtual address (10017000 to 10018D89) 1E00 size of raw data 5C00 file pointer to raw data (00005C00 to 000079FF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 40000040 flags Initialized Data Read Only //只讀段的二進制數據內容 RAW DATA #3 10017000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10017010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 10017020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ ….. //導出表的信息 Section contains the following exports for DemoDLLd.dll
00000000 characteristics 51A86C37 time date stamp Fri May 31 17:24:07 2013 0.00 version 1 ordinal base 9 number of functions //符號地址表元素的數量 9 number of names //符號名稱表元素的數量
ordinal hint RVA name //序號,數組下標,相對虛擬地址,符號名稱 1 0 00011145 ??0DemoMath@@QAE@XZ 2 1 00011109 ??1DemoMath@@QAE@XZ 3 2 0001101E ??4DemoMath@@QAEAAV0@ABV0@@Z 4 3 000111C7 ?AddData@DemoMath@@QAEXNN@Z 5 4 0001116D ?Area@DemoMath@@QAEXN@Z 6 5 00011221 ?DivData@DemoMath@@QAEXNN@Z 7 6 000110B4 ?GetOperTimes@@YAHXZ 8 7 00011005 ?MulData@DemoMath@@QAEXNN@Z 9 8 0001114A ?SubData@DemoMath@@QAEXNN@Z |
對於每個動態連接庫,都會有一個IMAGE_EXEPORT_DIRECTORY類型的數據結構與之對應。該數據結構保存了與導出表有關的信息,並與其餘數據結構發生關聯。與該數據結構有關聯的數據實體包括:數據目錄,字符串表,符號地址表,符號名稱表,名稱序號對應關係表。這些實體之間的關係以下圖所示:
可選頭中包含了數據目錄,數據目錄的第一項指向了導出表。導出表是一個結構體類型,在該結構體的字段中保存了與導出表相關的信息,如:DLL名稱字段指向了一個字符串表,在該字符串表中保存了DLL的名稱;符號地址表地址字段指向了一個地址數組,該數組中保存了符號的地址;符號名稱表地址字段指向了符號名稱數組,該數組中保存了符號名稱字符串的地址;名稱序號對應關係表地址指向了名稱序號對應關係的數組,該數組中存儲了符號的名稱與序號之間的對應關係。
符號地址表是一個DWORD類型的數組,在該數組中存儲了被導出符號的相對虛擬地址,數組中每個非零的數組元素都指向了一個符號的相對虛擬地址;符號名稱表是一個DWORD類型的數組,數組中保存的是相對虛擬地址,該相對虛擬地址指向了字符串表中的相關位置,這些位置中保存了符號的名稱;名稱序號表是一個WORD類型的數組,該數組中保存了符號的序號。符號名稱表中的元素與名稱序號表中的元素經過數組下標對應。好比:在符號名稱表中,數組下標爲1的元素,它的序號存在在名稱序號表中,數組下標也爲1。
序號的計算公式爲:序號 = 符號地址表數組下標 + Base字段值。在Windows16位時代,因爲受到硬件大小的限制,在執行動態連接的時候,使用序號查找符號的地址,即:用序號的值減去Base的值,得到符號地址表數組的下標,進而得到符號的相對虛擬內存地址。這種方式節省了內存空間以及符號查找的時間,可是易讀性差。隨着時間的發展,當硬件的物理內存不在是問題的時候,開始使用符號名稱查找符號的地址,具體的查找過程是:經過符號名稱在名稱序號對應關係表中查找到符號的序號,而後再用符號的序號查找符號的地址。雖然引入了符號名稱表,可是這個表不是必須的,依然能夠經過序號查找符號的地址。在一個DLL中,每個導出符號都有一個惟一對應的序號,而導出符號名是可選的。
在動態連接的時候,能夠經過兩種方式進行符號地址的查找,一種是直接利用符號的序號直接查找,另一種是利用符號的名稱間接查找。在進行符號地址查找的時候,符號地址表,符號名稱表,名稱序號對應表之間的關係以下圖所示:
在WinNT.h頭文件中,導出表被定義爲IMAGE_EXPORT_DIRECTORY類型,具體的定義內容以下:
typedef struct _IMAGE_EXPORT_DIRECTORY { DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; DWORD Name; DWORD Base; DWORD NumberOfFunctions; DWORD NumberOfNames; DWORD AddressOfFunctions; // RVA from base of image DWORD AddressOfNames; // RVA from base of image DWORD AddressOfNameOrdinals; // RVA from base of image } IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY; |
在該定義中,各字段的詳細解釋以下表所示:
字段名稱 |
類型 |
描述 |
Characteristics |
DWORD |
關於導出表的一些標記,目前沒有被定義。 |
TimeDateStamp |
DWORD |
導出表被建立的時間。 |
MajorVersion |
WORD |
導出表的主版本號,目前沒有使用,設爲零。 |
MinorVersion |
WORD |
導出表的次版本號,目前沒有使用,設爲零。 |
Name |
DWORD |
一個相對虛擬地址,指向字符串表的一個位置,該位置存儲了該Dll的名稱。 |
Base |
DWORD |
一般設定爲1,但也不是必須的。 序號 = Base + 符號地址數組下標。 |
NumberOfFunctions |
DWORD |
符號地址表中數據元素的個數。 |
NumberOfNames |
DWORD |
符號名稱表以及符號名稱序號對應關係表中數據元素的個數。因爲符號名稱表和符號名稱序號對應關係表是一一對應的,因此它們的數組元素個數相同;該值可能會小於NumberOfFunctions,若是小於這個值,表示有一部分符號只經過序號對應,沒有保存它們的名稱。 |
AddressOfFunctions |
DWORD |
相對虛擬地址,指向符號地址表。 |
AddressOfNames |
DWORD |
相對虛擬地址,指向符號名稱表。 |
AddressOfNameOrdinals |
DWORD |
相對虛擬地址,指向符號名稱序號對應關係表。 |
在目標文件中,全部符號的地址都是基於文件頭或者文件中某個位置的偏移地址。在將目標文件連接之後,在輸出的PE文件中,這些偏移地址會被轉化成相對虛擬內存地址,而且再加上一個默認內存加載位置的地址值,造成符號的基於默認內存加載位置的虛擬內存地址。基於默認內存加載位置的虛擬內存地址的計算公式爲:
符號虛擬內存地址 = 默認內存加載位置 + 相對虛擬內存地址 |
可執行程序默認加載到內存的0x0400000位置,而動態連接庫默認加載到內存的0x10000000位置。
當PE文件被加載到內存之後,若是該文件不能被加載到默認的內存位置,那麼在指令代碼中,全部使用絕對地址表示的符號的地址都須要被重定位。在Windows中,這一地址重定位的過程被叫作重定基地址。具體的操做過程是:在每個須要進行地址重定位的符號處,將該符號當前地址的數值上再加上一個固定的數值,這個新得到的地址值就是該符號正確的虛擬內存地址。
這個固定值的計算公式是:
固定值 = DLL當前內存加載的位置 – DLL默認內存加載位置(0x10000000) |
地址重定位工做由操做系統的加載器來完成,在基地址重定位表中,記錄了每個須要進行地址重定位的符號的地址。在地址重定位的時候,加載器讀取該表中的數值,而後查找到須要進行地址重定位的符號的位置,最後修正該符號的虛擬內存地址。
在PE文件被加載到內存之後,這些文件內容是以頁爲單位存儲在內存中,每一個內存頁的大小是4KB。在基址重定位表中,數據表中的數據被分割成一個個數據塊,每個數據塊會對應一個虛擬內存頁,表示在該虛擬內存頁中的符號的地址重定位信息。
使用工具dumpbin將DemoDlld.dll中的內容導出,涉及到基址重定位表的內容以下所示:
//基址重定位表的摘要信息 SECTION HEADER #7 .reloc name 524 virtual size 1C000 virtual address (1001C000 to 1001C523) 600 size of raw data 9A00 file pointer to raw data (00009A00 to 00009FFF) 0 file pointer to relocation table 0 file pointer to line numbers 0 number of relocations 0 number of line numbers 42000040 flags Initialized Data Discardable Read Only //基址重定位表的二進制數據內容 RAW DATA #7 1001C000: 00 10 01 00 68 00 00 00 4F 35 76 35 9F 35 A4 37 1001C010: AC 37 24 38 2C 38 A4 38 AC 38 30 39 41 39 49 39 1001C020: C4 39 CC 39 D8 39 C6 3A D7 3A DD 3A EE 3A FD 3A ……………… //如下是對二進制內容的翻譯。 BASE RELOCATIONS #7 //基址重定位表的數據塊 相對虛擬地址爲:11000,塊大小爲68,該數據塊對應一個4KB大小的內存頁 11000 RVA, 68 SizeOfBlock 低12位偏移 類型 符號的地址 符號名稱 54F HIGHLOW 10019140 ?nOperTimes@@3HA (int nOperTimes) 576 HIGHLOW 100156AE 59F HIGHLOW 10019004 ___security_cookie ……. //下一個數據塊,相對虛擬地址爲:12000,塊大小爲F4,該數據塊對應一個4KB大小的內存頁 12000 RVA, F4 SizeOfBlock C HIGHLOW 10012010 18 HIGHLOW 1001201C 146 HIGHLOW 10015718 16F HIGHLOW 10019004 ___security_cookie ……… |
基址重定位表的結構以下圖所示:
可選頭中包含了數據目錄,數據目錄的第五項數據中包含了指向了基址重定位表的指針,以及基址重定位表的大小。基址重定位表之內存頁的大小爲依據進行分塊,在每個塊中,都以IMAGE_BASE_RELOCATION類型的數據結構開頭,後面跟隨着每一個符號的基地址重定位信息。這些符號的重定位信息是一系列的WORD值。這些WORD值的高4位指出了重定位的類型,而低12位是一個地址偏移。將該地址偏移數值與數據塊的虛擬內存地址數值(即:IMAGE_BASE_RELOCATION. VirtualAddress)相加,能夠獲得該符號須要進行重定位的位置。
在基址重定位表的數據塊中,所包含的重定位信息的個數的計算公式爲:
重定位信息個數 = (塊大小 – sizeof(IMAGE_BASE_RELOCATION))/2 由於塊大小以字節爲單位表示,而重定位信息以字爲單位表示,轉化成字須要除2 |
重定位的類型描述以下表:
類型 |
描述 |
IMAGE_REL_BASED_ABSOLUTE (0) |
這種不需操做;用於將塊按32位邊界對齊。位置應該爲0。 |
IMAGE_REL_BASED_HIGH (1) |
重定位的高16位必須被用於被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的高位WORD。 |
IMAGE_REL_BASED_LOW (2) |
重定位的低16位必須被用於被偏移量所指向的那個16位的WORD單元,此WORD是一個32位的DWORD的低位WORD。 |
IMAGE_REL_BASED_HIGHLOW (3) |
重定位的所有32位必須應用於上面所說的所有32位。PE文件採用該類型。 |
IMAGE_REL_BASED_HIGHADJ (4) |
這種修正要求一個全32位值。高16位定位於偏移量處,低16位定位在下一個數組元素(此數組元素包括在大小的域中)的偏移量處。它們兩個須要被連成一個有符號的變量。加上32位的增量。而後加上0x8000 並將有符號變量的高16位存儲在偏移量處的16位域中。」 |
IMAGE_REL_BASED_MIPS_JMPADDR (5) |
|
IMAGE_REL_BASED_SECTION (6) |
|
IMAGE_REL_BASED_REL32 (7) |
|
在WinNT.h頭文件中,基址重定位表被定義爲IMAGE_BASE_RELOCATION類型,具體的定義內容以下:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION; |
在該定義中,各字段的詳細解釋以下:
字段名稱 |
類型 |
描述 |
VirtualAddress |
DWORD |
數據塊的虛擬內存地址 |
SizeOfBlock |
DWORD |
數據塊的大小 |
在如下內容中,將以一個示例的形式來講明如何查找符號重定位的位置。使用工具dumpbin將DemoDlld.dll的基址重定位表的內容導出,其中一個數據塊的內容以下:
BASE RELOCATIONS #7 //基址重定位表的數據塊 相對虛擬地址爲:11000,塊大小爲68,該數據塊對應一個4KB大小的內存頁 11000 RVA, 68 SizeOfBlock 低12位偏移 類型 符號的地址 符號名稱 54F HIGHLOW 10019140 ?nOperTimes@@3HA (int nOperTimes) 576 HIGHLOW 100156AE 59F HIGHLOW 10019004 ___security_cookie …….
|
在上面的內容中,紅色信息表示引用了符號nOperTimes處的地址須要被重定位,該引用形式必然是使用了絕對地址。
重定位地址的計算公式爲:默認加載位置 + 數據塊相對虛擬內存地址 + 偏移 = 0x10000000 + 0x11000 + 0x54F = 0x1001154F。處於虛擬內存地址0x1001154F處的地址值須要被重定位。
將DemoDlld.dll的內容導出爲彙編格式,與地址0x1001154F相關的內容以下:
?GetOperTimes@@YAHXZ: 10011530: 55 push ebp 10011531: 8B EC mov ebp,esp 10011533: 81 EC C0 00 00 00 sub esp,0C0h 10011539: 53 push ebx 1001153A: 56 push esi 1001153B: 57 push edi 1001153C: 8D BD 40 FF FF FF lea edi,[ebp-0C0h] 10011542: B9 30 00 00 00 mov ecx,30h 10011547: B8 CC CC CC CC mov eax,0CCCCCCCCh 1001154C: F3 AB rep stos dword ptr es:[edi] 1001154E: A1 40 91 01 10 mov eax,dword ptr [?nOperTimes@@3HA] 10011553: 5F pop edi 10011554: 5E pop esi 10011555: 5B pop ebx 10011556: 8B E5 mov esp,ebp 10011558: 5D pop ebp 10011559: C3 ret |
上面紅色字體標記出了關鍵代碼行,綠色的字體是須要被重定位的地址值,該地址的當前值爲0x10019140。該值的第一個字節正好對應地址0x1001154F,這就是須要被重定位的位置。該值是符號nOperTimes在PE文件中被分配的虛擬內存地址,因爲在此處使用了絕對地址的形式,因此當PE文件被加載到內存之後,該符號的地址須要被重定位。
假設DemoDlld.dll被加載到了內存位置爲:0x20000000,那麼該地址值將被修正爲:0x20000000 – 0x10000000 + 0x10019140 = 0x20019140。
在PE文件中,各個數據結構之間的關係以下圖所示: