C++應用程式在Windows下的編譯、連結:第二部分COFF/PE檔案結構
2.1概述
在windows作業系統下,可執行檔案的儲存格式是PE格式;在Linux作業系統下,可執行檔案的儲存格式的WLF格式。它們都是COFF格式檔案的變種,都是從COFF格式的檔案演化而來的。
在windows平臺下,目標檔案(.obj),靜態庫檔案(.lib)使用COFF格式儲存;而可執行檔案(.exe),動態連結庫檔案(.dll)使用PE格式儲存。靜態庫檔案其實就是一堆目標檔案的集合。
在“WinNT.h”標頭檔案中定義了COFF格式檔案,以及PE格式檔案的資料結構。這些定義是一系列的結構體,列舉,以及#define巨集定義。在ImageHlp.dll中定義了編輯和讀取PE檔案內容的Win32API。
在64位Windows作業系統下,PE格式檔案被做了少部分修改。沒有新的欄位定義被加入,並且去除了一些欄位的定義,同時將欄位的寬度從32位擴充到64位。64位windows作業系統下的PE格式檔案被命名為:PE32+。
2.2COFF檔案的結構
2.2.1總體結構圖
COFF檔案的總體結構如下圖所示:
從檔案內容上來看,COFF檔案由二進位制資料組成。這些二進位制資料從檔案的零位置開始,依次儲存,直到檔案末尾。從資料結構的角度來看,這些二進位制資料又分別屬於不同的結構體或者結構體陣列。這些結構體被定義在“WinNT.h”標頭檔案中。
在COFF檔案中,這些結構體或結構體陣列分別表示不同的含義,記錄著COFF檔案中的不同內容。從檔案的頂端開始,依次儲存了檔案頭,可選頭,段表,段資料,重定位表,行號表,符號表,以及字串表的資訊。這些結構體資料之間存在關聯關係。比如:檔案頭資訊中儲存了符號表的開始位置,以及段表中陣列元素的個數;在段表中儲存了各個段的位置,重定位表的位置,行號表的位置;重定位表中的項會關聯到符號表中的某個符號;而符號表中某個符號的名稱可能會儲存在字串表中。
使用dumpbin工具可以將目標檔案的內容匯出,具體的命令格式如下:
Dumpbin /all DemoMath.obj >DemoMath.txt |
在上面的命令中,將目標檔案“DemoMath.obj”的所有內容匯出到文字檔案“DemoMath.txt”中。命令選項“/all”表示匯出所有內容,命令選項“>”表示將匯出的內容儲存到檔案中。
2.2.2檔案頭
檔案頭以一個結構體的形式儲存在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 檔案。 |
2.2.3可選頭
該資料結構為可選資料,在目標檔案中不存在此資料結構。只有當COFF檔案作為可執行檔案存在的時候,該資料結構才有意義。
2.2.4段表
段表是各個段的目錄,用於檢索各個段的資訊。它以結構體陣列的形式儲存在可選頭或者檔案頭的後面。在段表中,每一項的大小是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檔案中被設定。 |
2.2.5重定位表
在編譯階段,將某些原始檔編譯成目標檔案的時候,在目標檔案中,某些被呼叫函式或者資料的位置是無法確定的。這時候,編譯器將這些被呼叫的函式或者資料的地址設定為一個預設的假值。在連結階段,當能夠確定這些被呼叫函式或資料的地址的時候,再用真實的地址來替換這些假值。我們將這個過程叫做重定位。
使用工具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位相對地址。在執行重定位的時候,對於絕對地址型別,將被替換為符號的絕對地址;而對於相對地址型別,將被替換為符號的相對地址,即:符號相對於被修正位置的地址差。 |
2.2.6行號表
行號表描述了二進位制程式碼與原始碼行號之間的關係,除錯階段使用。在“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 |
行號 |
2.2.7符號表
在編譯階段的詞法分析過程中,編譯器掃描整個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…) |
符號為全域性變數/函式,符號定義在當前檔案中,值表示符號在段內偏移。 |
2.2.8字串表
字串表用來儲存長度大於8個位元組的符號名稱。字串表的前4個位元組表示字串的長度,後面的緊跟字串的內容,它以位元組為單位,以’\0’作為字串的結束符。這裡的字串長度不僅僅是字串自身的長度(字串內容+’\0’),還包括前面4個位元組的該資料自身的長度。
2.2.9各資料結構之間的關係
在COFF檔案所包含的資料結構中,各個資料結構之間的關係如下圖所示:
重定位表和符號表之間通過符號表的索引進行關聯;在檔案頭中儲存了可選頭的大小和段表所包含專案的數量,通過計算可以確定段表的起始位置和結束位置。段表起始位置=檔案頭大小+可選頭大小;其他關係通過相對檔案首位置的偏移表示。
2.3Lib檔案的結構
2.3.1總體結構圖
靜態連結庫就是一組目標檔案的集合,當執行靜態連結的時候,被選定的目標檔案的內容就會被合併到相關的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; |
2.4PE檔案的結構
2.4.1總體結構圖
從檔案內容上來看,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”表示匯出所有內容,命令選項“>”表示將匯出的內容儲存到檔案中。
2.4.2 DOS頭
2.4.2.1DOS MZ 頭
所有 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頭在檔案中的位置。
2.4.2.2DOS Stub
DOS Stub實際上是個有效的EXE,在不支援PE檔案格式的作業系統中,它將簡單顯示一個錯誤提示,類似於字串“This program requires Windows”,或者程式設計師可根據自己的意圖實現完整的DOS程式碼。大多數情況下DOS Stub由彙編器/編譯器自動生成。
2.4.3 PE頭
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可選頭的資訊。
2.4.3.1PE頭標識
在一個有效的PE檔案中,PE頭標識欄位的值是0x00004550,用ASCII表示就是“PE00”。 #define IMAGE_NT_SIGNATURE定義了這個值。
2.4.3.2檔案頭
見2.2.2節描述。PE中的檔案頭與COFF中的檔案頭定義一致。
2.4.3.3可選頭
在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結構)。 |
2.4.4匯入表
當一個可執行程式或者動態連結庫呼叫另外一個動態連結庫中的變數或者函式的時候,必須將這些被呼叫函式或者變數匯入。這些被匯入的變數或函式是必須是被呼叫動態連結庫匯出表中的一個子集。也就是說,只有當這些變數或者函式被匯出以後,才能被其他可執行程式或者動態連結庫匯入。
在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值查詢不能命中,那麼就會使用符號名稱進行二分法查詢。
2.4.5匯出表
在一個動態連結庫檔案中,總會有一些變數或者函式被宣告為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 |
相對虛擬地址,指向符號名稱序號對應關係表。 |
2.4.6基址重定位表
在目標檔案中,所有符號的地址都是基於檔案頭或者檔案中某個位置的偏移地址。在將目標檔案連結以後,在輸出的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。
2.4.7各資料結構之間的關係
在PE檔案中,各個資料結構之間的關係如下圖所示:
相關文章
- PE 檔案結構圖
- PE檔案結構複習
- PE檔案結構解析3
- PE檔案結構解析1
- PE檔案結構解析2
- 條件編譯、多檔案程式設計、結構體編譯程式設計結構體
- 再探.NET的PE檔案結構(安全篇)
- PE檔案結構解析 Part3 NT HeadersHeader
- Golang 編譯windows應用程式Golang編譯Windows
- 程式的編譯和連結原理分析編譯
- 從原始檔到可執行檔案:原始檔的預處理、編譯、彙編、連結編譯
- 在AndroidStudio下使用cmake編譯出靜態連結庫的方法Android編譯
- 在Windows下編譯WebRTCWindows編譯Web
- Kaldi在ubuntu 18.04下編譯小結Ubuntu編譯
- 類檔案結構_class類檔案的的結構
- 資料結構實驗:連結串列的應用資料結構
- 編譯連結過程編譯
- windows載入PE檔案的流程Windows
- HarmonyOS:應用程式包結構(1)Stage模型應用程式包結構模型
- PE結構分析(二)
- Linux下的檔案系統結構Linux
- 檔案連結
- 程式設計師的自我修養-編譯連結程式設計師編譯
- Linux---檔案、軟連結於硬連結檔案Linux
- js 透過連結下載檔案JS
- 從編譯連結到cmake編譯
- C++ 獲取PE檔案自校驗值的程式碼C++
- C++資料結構連結串列的基本操作C++資料結構
- 根據網路連線(檔案連結)下載檔案到本地
- Windows下使用Graalvm將Javafx應用編譯成exeWindowsLVMJava編譯
- CMake 進行多專案中dll的編譯和連結編譯
- 編譯deno,deno結構解析編譯
- GCC編譯和連結過程GC編譯
- C語言中編譯和連結C語言編譯
- 資料結構——單連結串列的C++實現資料結構C++
- 關於Vulkan應用程式執行時編譯GLSL Shader檔案的方法編譯
- 編譯、彙編、連結、載入、顯示編譯
- 前端資料結構(3)之連結串列及其應用前端資料結構
- GCC編譯過程(預處理->編譯->彙編->連結)GC編譯