PE文件格式

1 PE文件結構


2 文件頭

PE 文件頭是一個 IMAGE_NT_HEADERS 類型的結構,它在WINNT.H文件中定義。

typedef struct _IMAGE_NT_HEADERS {  
	DWORD Signature;  
	IMAGE_FILE_HEADER FileHeader; 
	IMAGE_OPTIONAL_HEADER OptionalHeader;
} IMAGE_NT_HEADERS,  *PIMAGE_NT_HEADERS;
Signature域是 ASCII文本 “PE\0\0”。

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;
WORD    Machine 
文件所適用於的 CPU 類型。已經定義了以下 CPU ID: 

0x14d  Intel i860
0x14c  Intel I386 (486 和 586 也用此 ID)
0x162  MIPS R3000
0x166  MIPS R4000
0x183  DEC Alpha AXP

WORD    NumberOfSections 
文件中節的數目。 

DWORD   TimeDateStamp 
鏈接器(對於 OBJ 文件來說是編譯器)生成此文件的時間。它保存的是自 1969 年十二月 31 日下午 4:00 開始的總秒數。 

DWORD   PointerToSymbolTable 
COFF 符號表的文件偏移。這個域只用於 OBJ 文件和帶 COFF 調試信息的 PE 文件。PE 文件支持多種調試信息格式,因此調試器應該參考數據目錄中
IMAGE_DIRECTORY_ENTRY_DEBUG 這一項的信息(在後面定義)。 

DWORD   NumberOfSymbols 
COFF 符號表中的符號數,可以參考 PointerToSymbolTable 域的信息。 

WORD    SizeOfOptionalHeader 
這個結構後面的可選文件頭的大小。在 OBJ 文件中,這個域爲 0。在可執行文件中,它是這個結構後面的 IMAGE_OPTIONAL_HEADER 結構的大小。 

WORD    Characteristics 
關於文件信息的標誌。下面是一些比較重要的值,其它的值在 WINNT.H 文件中定義。 
0x0001  此文件中不包含重定位信息 
0x0002  此文件是可執行映像(不是 OBJ 或 LIB) 
0x2000  此文件是動態鏈接庫,不是可執行程序 

PE 文件頭的第三個組成部分是一個IMAGE_O PTIONAL_HEADER 類型的結構。對於PE 文件來說,這一部分並不是可選的。COFF 結構允許在標準的 IMAGE_FILE_HEADER 之外定義一些附加信息。這個結構中的信息是 PE 設計者認爲除 IMAGE_FILE_HEADER 中的基本信息之外非常重要的信息。 並不是 IMAGE_OPTIONAL_HEADER 結構中的所有域都很 重要。比較重要的是 ImageBase 和Subsystem 這兩個域。你可以跳過其中一些域的描述。

typedef struct _IMAGE_OPTIONAL_HEADER {  
	WORD Magic;  
	BYTE MajorLinkerVersion;  
	BYTE MinorLinkerVersion;  
	DWORD SizeOfCode;  
	DWORD SizeOfInitializedData;  
	DWORD SizeOfUninitializedData;  
	DWORD AddressOfEntryPoint;  
	DWORD BaseOfCode;  
	DWORD BaseOfData;  
	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;  
	DWORD SizeOfStackReserve;  
	DWORD SizeOfStackCommit;  
	DWORD SizeOfHeapReserve;  
	DWORD SizeOfHeapCommit;  
	DWORD LoaderFlags;  
	DWORD NumberOfRvaAndSizes;  
	IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER,  *PIMAGE_OPTIONAL_HEADER;
DWORD   SizeOfCode 
所有的代碼節的總大小(已經向上舍入)。通常大部分文件只有一個代碼節,因此這個域就是.text 節的大小。 

DWORD   ImageBase 
當鏈接器生成可執行文件時,它假定這個文件會被映射到內存的一個特定位置。這個特定位置就被保存在這個域中。事先爲文件假定一個位置可以讓鏈接器對代碼進行優化。如果文件確實被加載器映射到了那個位置,那麼在運行前代碼並不需要做任何修正。對於用於 Windows NT 上的可執行文件,這個默認映像基址爲 0x10000。對於 DLL,它爲 0x400000。在 Windows 95 上,地址 0x10000 不能被用於加載 32 位 EXE,因爲它位於一個被所有進程所共享的線性地址區域。由於這個原因,Microsoft 把基於 Win32的可執行文件的默認的基地址改成了 0x400000。基地址被默認爲是 0x10000 的早期程序在 Windows 95 上要花費更長的時間來完成加載,因爲加載器必須進行基址重定位。 

WORD    Subsystem 
此程序的用戶界面的子系統類型。WINNT.H 定義了以下值: 
NATIVE  1  不需要子系統(例如設備驅動程序) 
WINDOWS_GUI  2  運行於 Windows GUI 子系統 
WINDOWS_CUI  3  運行於 Windows 字符模式子系統( 控制檯應用程序) 
OS2_CUI  5  運行於 OS/2 字符模式子系統(OS/2 1.x 版本的應用程序)
POSIX_CUI  7  運行於 Posix 字符模式子系統 

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]  
這是一個 IMAGE_DATA_DIRECTORY 結構的數組。它前面的元素包含了可執行文件的重要部分的起始 RVA 和大小。數組最後的一些元素當前並未使用。此數組的第一個元素總是導出表(如果存在的話)的地址和大小。第二個元素是導入表的地址和大小,等等。

typedef struct _IMAGE_DATA_DIRECTORY { 
	DWORD VirtualAddress;  
	DWORD Size;
} IMAGE_DATA_DIRECTORY,  *PIMAGE_DATA_DIRECTORY;

Offset (PE/PE32+) Description
96/112 Export table address and size
104/120 Import table address and size
112/128 Resource table address and size
120/136 Exception table address and size
128/144 Certificate table address and size
136/152 Base relocation table address and size
144/160 Debugging information starting address and size
152/168 Architecture-specific data address and size
160/176 Global pointer register relative virtual address
168/184 Thread local storage (TLS) table address and size
176/192 Load configuration table address and size
184/200 Bound import table address and size
192/208 Import address table address and size
200/216 Delay import descriptor address and size
208/224 The CLR header address and size
216/232 Reserved


3 節表 

在 PE 文件頭和每個節的原始數據之間是節表。節表就像是包含映像中每個節的信息的電話簿。映像中的節是按它們的起始地址(RVA)來排列的,而不是按字母表順序。 
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;
BYTE    Name[IMAGE_SIZEOF_SHORT_NAME] 
這是一個 8 字節的 ANSI 字符串(並不是 UNICODE),它是節的名稱。大多數節名都以“.“開始(例如“.text”),但這並不是必須的。你可以在彙編語言中用段指令來命名你的節,也可以在 Microsoft C/C++編譯器中用“#pragma data_seg”和 “#pragma code_seg”來命名你的節。不過要注意,如果節名長 8 字節的話,那就沒有最後的那個 NULL 字節。如果你是 printf 愛好者,你可以使用%.8s 來避免將名稱字符串複製到一個能以 NULL 結尾的緩衝區中。 

union { 
    DWORD   PhysicalAddress; 
    DWORD   VirtualSize; 
} Misc;  

這個域在 EXE 文件和 OBJ 文件中意義不同。在 EXE 文件中,它保存的是代碼或數據的實際大小。這是在尚未向上舍入到離它最近的文件對齊值的倍數時的大小。這個結構後面的 SizeOfRawData 域(看起來命名好像不太恰當)保存的是已經舍入後的值。Borland 的鏈接器把這兩個域的意義顛倒了過來,反而好像是正確的。 對 OBJ 文件來說,這個域指出了節的物理地址。第一個節的地址是 0。要找出 OBJ 文件中下一個節的地址,只需在當前節的物理地址上加上 SizeOfRawData 域的值就可以了。 

DWORD   VirtualAddress 
在 EXE 文件中,這個域保存了這個節應該被加載器映射到的地址的 RVA。要計算一個給定的節在內存中的實際起始地址,把映像的基地址與這個域的值相加就可以了。當使用 Microsoft 的工具時,第一個節的默認 RVA 是 0x1000。對於 OBJ 文件來說,這個域是無意義的,它被設置爲 0。 

DWORD   SizeOfRawData 
在 EXE 文件中,這個域保存了節的大小(已經向上舍入到離它最近的文件對齊值的倍數)。例如假設文件對齊值是 0x200。如果前面的 VirtualSize 域指出這個節的大小是0x35A 字節時,這個域會指明這個節的大小爲 0x400 字節。對於 OBJ 文件來說,這個域包含了由編譯器或彙編程序生成的節的精確大小。換句話說,在 OBJ 文件中,這個域與 EXE 中的 VirtualSize 域等價。 

DWORD   PointerToRawData 
這是基於文件的偏移,在這個偏移處可以找到由編譯器或彙編程序生成的原始數據。如果你的程序自己映射 PE 或 COOF 文件(而不是讓操作系統加載它)的話,這個域比VirtualAddress 域更重要。在這種情況下,你實際進行的是完全的線性映射,你會發現節的數據在這個偏移處,而不是在由 VirtualAddress 指定的 RVA 處。 

DWORD   PointerToRelocations 
在 OBJ 文件中,這是基於文件的偏移,在這個偏移處你可以找到這個節的重定位信息。OBJ 文件的每個節的重定位信息緊跟着這個節的原始數據。在 EXE 文件中,這個域(以
及這個結構中以後的域)都是無意義的,它們都被設置爲 0。在鏈接器創建 EXE 文件時,它已經處理了大部分的修正問題,只剩下基地址重定位和導入函數留在加載時解析。有關基址重定位和導入函數的信息被保存在它們各自的節中,因此對於 EXE 文件來說,並不需要在每個節中原始的數據之後都保存重定位信息。 

DWORD   PointerToLinenumbers 
這是基於文件的偏移,在這個偏移處你可以找到行號表。行號表使源文件中的行號與相應行生成的代碼關聯了起來。在現代的調試信息格式中,例如 CodeView 格式,行號信息作爲調試信息的一部分被保存。但是在 COFF 調試信息格式中,行號信息與符號名以及類型信息是分開存儲的。通常情況下,只有代碼節(例如.text)有行號。在 EXE
文件中,行號在節中的原始數據之後。在 OBJ 文件中,一個節的行號位於這個節的原始數據和重定位表之後。 

WORD    NumberOfRelocations 
節中重定位表中重定位信息的數目(前面的 PointerToRelocations field 域指向重定位表)。這個域好像只與 OBJ 文件有關。 

WORD    NumberOfLinenumbers 
節中行號表中行號信息的數目。(前面的 PointerToLinenumbers 域指向行號表)。 

DWORD   Characteristics 
大多數程序員稱爲標誌(Flag)的內容,在 PE/COFF 格式中稱爲特徵(Characteristic)。這個域用一組標誌用來指定節的屬性(例如代碼還是數據、可讀還是可寫等)。要獲
取一個節的所有可能的屬性的列表,可以參考 WINNT.H 中的 IMAGE_SC N_XXX_XXX 定義。以下是一些重要的標誌: 
0x00000020 這個節包含代碼。通常與可執行標誌(0x80000000)一起設定。 
0x00000040 這個節包含已初始化的數據。除了可執行的節和.bss 節之外,幾乎所有的節都設定了這個標誌。 
0x00000080 這個節包含未初始化的數據(例如.bss 節)。 
0x00000200 這個節包含備註或其它類型的信息。典型的使用這個標誌的節是由編譯器生成的.drectv 節,它包含編譯器傳遞給鏈接器的命令。 
0x00000800 這個節的內容並不放入最後的 EXE 文件中。這些節是編譯器或彙編程序用來給鏈接器傳遞信息的。 
0x02000000 這個節可以被丟棄,因爲一旦它被加載之後,進程就不再需要它了。最常見的可以被丟棄的節是基址重定位節(.reloc)。 
0x10000000 這個節是共享的。當用於 DLL 時,這個節中的數據在所有使用這個 DLL 的進程中是共享的。數據節默認是不共享的,這意味着使用某個 DLL 的所有進程都有這個節中的數據的私有副本。說得更專業一點就是,共享節告訴內存管理器對這個節的頁面映射進行一些額外設置以便使用這個 DLL 的所有進程都使用同一塊物理內存。要使一個節變成共享的,可以在鏈接時使用 SHARED 屬性。例如“LINK /SECTION:MYDATA,RWS ...”告訴鏈接器這個叫做 MYDATA 的節應該被設置成可讀、可寫和共享的。 
0x20000000 這個節是可執行的。只要設置了“包含代碼”標誌(0x00000020),通常也會設置這個標誌。 
0x40000000 這個節是可讀的。EXE 文件中的節總是設置這個標誌。 
0x80000000 這個節是可寫的。如果 EXE 文件的節沒有設置這個標誌,加載器會把映射的頁面都標記成只讀和只執行的。通常.data 和.bss 節被設置這個屬性。有趣的是.idata 節也設置了這個標誌。 


一個典型的 EXE 文件的節表 

01 .text     VirtSize: 00005AFA  VirtAddr:  00001000 
    raw data offs:   00000400  raw data size: 00005C00 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00009220  line #'s:      0000020C 
    characteristics: 60000020 
      CODE  MEM_EXECUTE  MEM_READ 
 
  02 .bss      VirtSize: 00001438  VirtAddr:  00007000 
    raw data offs:   00000000  raw data size: 00001600 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: C0000080 
      UNINITIALIZED_DATA  MEM_READ  MEM_WRITE 
 
  03 .rdata    VirtSize: 0000015C  VirtAddr:  00009000 
    raw data offs:   00006000  raw data size: 00000200 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: 40000040 
      INITIALIZED_DATA  MEM_READ 
 
  04 .data     VirtSize: 0000239C  VirtAddr:  0000A000 
    raw data offs:   00006200  raw data size: 00002400 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: C0000040 
      INITIALIZED_DATA  MEM_READ  MEM_WRITE 
 
  05 .idata    VirtSize: 0000033E  VirtAddr:  0000D000 
    raw data offs:   00008600  raw data size: 00000400 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: C0000040 
      INITIALIZED_DATA  MEM_READ  MEM_WRITE 
 
  06 .reloc    VirtSize: 000006CE  VirtAddr:  0000E000 
    raw data offs:   00008A00  raw data size: 00000800 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: 42000040 
      INITIALIZED_DATA  MEM_DISCARDABLE  MEM_READ 


一個典型的 OBJ 文件的節表 
01 .drectve  PhysAddr: 00000000  VirtAddr:  00000000 
    raw data offs:   000000DC  raw data size: 00000026 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: 00100A00 
      LNK_INFO  LNK_REMOVE 
 
  02 .debug$S  PhysAddr: 00000026  VirtAddr:  00000000 
    raw data offs:   00000102  raw data size: 000016D0 
    relocation offs: 000017D2  relocations:   00000032 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: 42100048 
      INITIALIZED_DATA  MEM_DISCARDABLE  MEM_READ 
 
  03 .data     PhysAddr: 000016F6  VirtAddr:  00000000 
    raw data offs:   000019C6  raw data size: 00000D87 
    relocation offs: 0000274D  relocations:   00000045 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: C0400040 
      INITIALIZED_DATA  MEM_READ  MEM_WRITE 
 
  04 .text     PhysAddr: 0000247D  VirtAddr:  00000000 
    raw data offs:   000029FF  raw data size: 000010DA 
    relocation offs: 00003AD9  relocations:   000000E9 
    line # offs:     000043F3  line #'s:      000000D9 
    characteristics: 60500020 
      CODE  MEM_EXECUTE  MEM_READ 
 
  05 .debug$T  PhysAddr: 00003557  VirtAddr:  00000000 
    raw data offs:   00004909  raw data size: 00000030 
    relocation offs: 00000000  relocations:   00000000 
    line # offs:     00000000  line #'s:      00000000 
    characteristics: 42100048 
      INITIALIZED_DATA  MEM_DISCARDABLE  MEM_READ


4 節數據

.text 節是由編譯器或彙編程序生成的所有通用代碼組成的。

在 PE 文件中,當你調用其它模塊中的函數(例如 USER32.DLL 中的 GetMessage 函數)時,由編譯器生成的 CALL 指令並不是直接把控制權傳到了那個 DLL 中(見圖 2)。相反,CALL 指令把控制權傳給了 
    JMP DWORD PTR [XXXXXXXX] 
這種形式的指令,而這些指令也在同一.text 節中。這種 JMP 指令通過.idata 節中的一個 DWORD變量間接跳轉。這個.idata 節中的 DWORD 變量包含了操作系統函數的入口點的實際地址。略一思考,我理解了爲什麼 DLL 的調用要以這種方式實現。通過把所有對 DLL 中的同一個函數的調用集中在一處,加載器就不需要對每條調用此函數的指令都進行修正。PE 加載器要做的就是把目標函數的正確地址放在.idata 節中相應的 DWORD 變量中。這樣就不需要修正任何調用此函數的指令。這與 NE 文件形成鮮明對比。在 NE 文件中,每個段都包含了一個這個段中需要修正的位置的列表。如果在這個段中調用 DLL 中某一函數 20 次,加載器必須在這個段中寫 20 次這個函數的地址。PE 文件的這種方法的不利之處是你不能用一個 DLL 函數的真實地址來初始化一個變量。


正如.text 是默認的代碼節一樣,.data 節是你的已初始化的數據所在的節。這個節由在編譯時初始化的全局變量和靜態變量組成。它也包含了字符串常量。鏈接器把 OBJ 文件和 LIB 文件中的所有.data 節組合成一個.data 節放入 EXE 文件中。局部變量位於線程的堆棧中,它們並不佔用.data 節或.bss 節的空間。 

.bss節存儲的是所有未初始化的全局變量和靜態變量。鏈接器把 OBJ 文件和 LIB 文件中的所有.bss 節組合成一個.bss 節放入 EXE 文件中。在節表中,.bss 節中的RawDataOffset 域被設置爲 0,這表明這個節不佔用文件的任何空間。TLINK 並不生成這個節。它通過擴展 DATA 節的虛擬大小來代替。

.CRT是另一個已初始化數據節,它由 Microsoft C/C++運行時庫使用,故此得名。爲什麼這個節中的數據不合併到標準的.data 中我不得而知。 

.rsrc 節包含了模塊中所有的資源。在早期的 Windows NT 中,由 16 位的 RC.EXE 生成的 RES文件的格式 Microsoft 的 PE 鏈接器並不認識。由 CVTRES 程序把 RES 文件轉換成 COFF 格式的 OBJ文件,把資源數據放在 OBJ 文件的.rsrc 節中。鏈接器只是把資源 OBJ 文件看成一個普通的 OBJ文件,這使得鏈接器並不需要知道關於資源方面的特別知識。最新的 Microsoft 鏈接器好像能直接處理 RES 文件。 

.idata節包含一個模塊從其它 DLL 中導入的函數(以及數據)的信息。這個節與 NE 文件的模塊參考表類似。關鍵區別是 PE 文件中每個導入的函數都在這個節中專門列出。要在 NE 文件中找到相同的信息,你必須到每個段中的原始數據最後的重定位信息中去挖掘。 

.edata節是 PE 文件爲其它模塊導出的函數和數據列表。它相當於 NE 文件中的入口表、常駐名稱表和非常駐名稱表的組合。與 16 位 Windows不同,很少需要從 EXE 文件中導出什麼,因此你通常只能在 DLL 中看到.edata 節。當使用 Microsoft 的工具時,.edata 節是通過 EXP 文件纔出現在 PE 文件中的。也就是說,鏈接器自己並不生成這種信息。相反,它依賴庫管理程序(LIB32)去掃描 OBJ 文件來生成 EXP 文件。而鏈接器把它加入到需要鏈接的模塊列表中。是的,就是這樣!這些麻煩的 EXP 文件其實就是 OBJ 文件,不過擴展名不同罷了。 

.reloc節存儲的是基址重定位表。基址重定位是對指令或者已經初始化的變量的值的一種調整,它是在加載器不能把文件加載到鏈接器設定的位置時才需要進行的。如果加載器把映像加載到了鏈接器設定的位置上,那麼加載器就完全忽略這個節中的重定位信息。如果你想碰碰運氣,期望加載器總是把映像加載到設定的基地址上,你可以通過/FIXED 選項告訴鏈接器移除重定位信息。雖然這可以節省可執行文件的空間,但它可能導致可執行文件在其它基於 Win32 實現的系統上不能運行。例如假定你爲 Windows NT 創建了一個 EXE 文件,並把它的基地址選在 0x10000。如果你告訴鏈接器移除重定位信息,這個 EXE 就不能在 Windows 95 上運行,因爲地址 0x10000已經被佔用了。 

當你使用編譯器指令__declspec(thread)時,你定義的數據並不被放入.data 節或者是.bss節,它被放入.tls節,tls 代表“線程局部存儲(Thread Local Storage)”,它與 Win32 函數中的 TlsAlloc 函數家族有關。當處理.tls節時,內存管理器要設置頁表,以便無論何時進程切換線程時,一組新的物理內存頁面被映射到.tls 節的地址空間。這允許基於線程的全局變量。在大多數情況下,使用這種機制比以線程爲基礎分配內存並把其指針保存在 TlsAlloc 分配的內存槽上更容易。 

對於.tls 節和__declspec(thread)變量有一個比較遺憾的地方。在 Windows NT 和 Windows 95 上,這種線程局部存儲機制不適用於通過調用 LoadLibrary 而動態加載的 DLL。對 EXE 或者隱含加載的 DLL 來說,一切正常。如果你不能隱含鏈接到 DLL,但是需要使用基於線程的數據,你就不得不使用 TlsAlloc 和TlsGetValue 並動態分配內存。 

儘管.rdata節經常位於.data 節和.bss 節之間,但你的程序通常看不到也並不使用這個節中的數據。然而.rdata 節至少用在兩個地方。第一個就是在由 Microsoft 的鏈接器生成的 EXE中,.rdata 節用於保存調試目錄,它僅存在於 EXE 文件中。(如果是 TLINK32.EXE,調試目錄則是在一個名爲.debug 的節中。)調試目錄是一個類型爲 IMAGE_DEBUG_DIRECTORY 結構的數組。
這些結構中保存了有關類型、大小以及位置等各種各樣的調試信息。三種主要類型的調試信息是:
CodeView®、COFF 和 FPO。
調試目錄並非必須位於.rdata 節的開始部分。要查找調試目錄表,使用數據目錄的第七個元素(IMAGE_DIRECTORY_ENTRY_DEBUG)中的 RVA。數據目錄在 PE 文件頭的末尾。要確定 Microsoft鏈接器生成的調試目錄的數目,用調試目錄的大小(在數據目錄的 Size 域可以找到)除以IMAGE_DEBUG_DIRECTORY 結構的大小即可。TLINK32 生成一個簡單的數,通常是 1。
.rdata 節中的另一個有用部分是描述字符串。如果在你的程序的 DEF 文件中指定了DESCRIPTION 項,則指定的描述字符 串就會出現在.rdata 節中。在NE 格式中,描述字符串總是出現在非常駐名稱表的首個元素的位置。描述字符串主要是爲了保存一個描述文件的有用字符串。不幸的是,我還沒有發現找到它的簡便方法。我曾經在一些 PE 文件中看到描述字符串在調試目錄的前面,但是在其它一些文件中它卻是在調試目錄的後面。我找不到一致的方法去尋找描
述字符串(甚至它是否存在)。 

類似.debug$S.debug$T這些節僅存在於 OBJ 文件中。它們保存了 CodeView 格式的符號和類型信息。這些節名源自以前的 16 位編譯器使用的用於調試目的的段的名稱($$SYMBOLS 和$$TYPES)。.debug$T 節的惟一目的是保存 PDB 文件的路徑名,這種 PDB 文件中包含工程中所有OBJ 文件的 CodeView 信息。鏈接器從 PDB 文件中讀取信息並創建 CodeView 信息,並把創建的包含 CodeView 信息的部分放在最終的 PE 文件最後。 

.drectve節僅存在於 OBJ 文件中。它包含編譯器傳給鏈接器的命令的文本表示。例如在我使用 Microsoft 編譯器生成的所有 OBJ 文件中都會看到以下的字符串出現在.drectve 節: 
    -defaultlib:LIBC -defaultlib:OLDNAMES 
當你在代碼中使用__declspec(export)時,編譯器簡單地生成與此等價的命令行並把它放進.drectve 節中(例如“-exprot:MyFunction”)。 
如果你由於某些原因要使用單獨的節,可以毫不猶豫地創建你自己的節。如果用的是C/C++編譯,使用#pragma c ode_seg 和#pragma data_seg 就可以了。在彙編語言中,只要在創建32 位段時(它最後成爲節)使用不同於標準節的名稱就可以了。如果你使用 TLINK32,你必須使用不同的類或關閉代碼段包裝。


PE 文件的導入表 

PE 文件的.idata 節包含了加載器用以確定目標函數的地址並且在可執行映像中修正它們所需的信息。 
.idata 節(或者稱爲導入表)以一個類型爲 IMAGE_IMPORT_DESCRIPTOR 結構的數組開始。
對於 PE 文件隱含鏈接到的每個 DLL 都有一個相應的 IMAGE_IMPOR T_DESCRIPTOR 結構。並沒有域用來指示這個數組中結構的數目。數組中的最後一個元素是通過這個結構中的所有域都是 NULL來表明的。IMAGE_IMPORT_DESCRIPTOR 結構如下所示。 

DWORD   Characteristics 
這個域在以前可能是一個標誌。現在 Microsoft 已經更改了它的意義但是並沒有同時更新 WINNT.H 文件。它實際是一個指針數組的偏移地址(RVA)。其中的每個指針都指
向一個 IMAGE_IMPORT_BY_NAME 結構。

DWORD   TimeDateStamp 
指示文件創建日期的日期/時間戳。 

DWORD  ForwarderChain 
這個域與函數轉發(Forward)有關。轉發就是把對一個 DLL 中的某個函數的調用轉到另一個 DLL 的某個函數上。例如在 Windows NT 上,KERNEL32.DLL 就將它的一些導出函數轉發到了 NTDLL.DLL 中。一個應用程序看起來好像調用的是 KERNEL32.DLL 中的函數,但實際上它調用的是 NTDLL.DLL 中的函數。這個域包含了 FirstThunk 數組(馬上就要講到)的索引。被這個域索引的函數會被轉發到另一個 DLL 上。不幸的是,函數是如何轉發的這種格式並未公開。轉發函數的例子很難找到。 

DWORD   Name 
這是一個以 NULL 結尾的 ASCII 字符串的 RVA,這個字符串包含導入的 DLL 的名稱。常見的例子是“KERNEL32.DLL”和“USER32.DLL”。 

PIMAGE_THUNK_DATA FirstThunk 
這個域是 IMAGE_THUNK_DATA 共用體的偏移地址(RVA)。幾乎在所有情況下,這個共用體都是作爲指向IMAGE_IMPORT_BY_NAME結構的指針。如果這個域不是這些指針之一,那推測它應該是那個被導入的 DLL 所導出的一個序數值。從文檔上看並不清楚是否可以只通過序數而不通過名稱就能導入函數。 

  IMAGE_IMPORT _DESCRIPTOR 結構中重要的部分是導入的 DLL 的名稱和兩個指向IMAGE_IMPORT_BY_NAME 結構的指針數組。在 EXE 文件中,這兩個數組(分別由 Characteristics域和 FirstThunk 域所指向)是並列的,並且由每個數組中的最後一個 NULL 指針標誌着數組結束。這個兩個數組中的指針均指向 IMAGE_IMPORT_BY_NAME 結構。

對於 PE 文件導入的每個函數都有一個相應的 IMAGE_IMPORT_BY_NAME 結構。這個結構非常簡單,格式如下: 
WORD    Hint; 
BYTE    Name[?]; 
第一個域是要導入的函數的導出序號。與 NE 文件不同,這個值不要求絕對正確。加載器只不過是在搜索導出函數時把它作爲建議的起始值。接下來的 ASCII 字符串是導入的函數的名稱。 
爲什麼會有兩個並列的指向 IMAGE_IMPORT_BY_NAME 結構的指針數組呢?第一個數組(由Characteristics 域指向的那一個)總是保留原樣,系統並不修改。它有時也被稱爲提示名稱表(hint-name table)。第二個數組(由 FirstThunk 域指向的那一個)要被 PE 加載器修改。加載器首先查找這個數組中每個指針所指向的 IMAGE_IMPORT_BY_NAME 結構所代表的函數的地址。然後它用找到的這個函數地址來覆蓋數組中相應的指向 IMAGE_IMPORT_BY_NAME 結構的指針。而
JMP DWORD PTR [XXXXXXXX]這條指令中的[ XXXXXXXX]部分就是這個 Fi rstThunk 數組中的某個元
素的值。由於被加載器覆蓋的這個指針數組最終保存的是導入函數的地址,因此它被稱爲導入地址表(Import Address Table,IAT)。 
一個典型的 EXE 文件的導入表 
GDI32.dll 
  Hint/Name Table: 00013064 
  TimeDateStamp:   2C51B75B 
  ForwarderChain:  FFFFFFFF 
  First thunk RVA: 00013214 
  Ordn  Name 
    48  CreatePen 
    57  CreateSolidBrush 
    62  DeleteObject 
   160  GetDeviceCaps 
    // 表的其餘部分省略…… 


PE 文件的導出表 

與導入一些函數相對的就是爲其它 EXE 或 DLL 導出一些函數。PE 文件把有關導出函數的信息保存在.edata 節中。通常由 Microsoft 的鏈接器生成的 PE 格式的 EXE 文件並不導出任何內容,因此它們並沒有.edata 節。但是 Borland 的 TLINK32 總是從 EXE 中至少導出一個函數。大多數的 DLL 都導出函數,因此它們都有.edata 節。.edata 節(導出表)的主要部分是由函數名稱、相應的入口點地址和導出序號值組成的表。在 NE 文件中,入口表、常駐名稱表和非常駐名稱表合起來與導出表相當。這些表被保存在 NE 文件頭中,而不是在單獨的段或資源中。 
在.edata 節的開始處是一個 IMAGE_EXPORT_DIRECTORY 結構。這個結構後面緊跟着的是它的域所指向的數據。 
DWORD   Characteristics 
這個域好像並未使用,總是 0。 
DWORD   TimeDateStamp 
指示文件創建日期的日期/時間戳。 
WORD    MajorVersion 
WORD    MinorVersion
 
這些域好像並未使用,總是 0。 
DWORD   Name 
包含這個 DLL 的名稱的 ASCII 字符串的 RVA。 
DWORD   Base 
導出函數的起始序數。例如如果文件導出的函數的序數分別爲 10、11、12,那麼這個域的值爲 10。要獲得某個函數的導出序數,你需要把這個域的值AddressOfNameOrdinals 數組中的相應元素的值相加。 
DWORD   NumberOfFunctions 
AddressOfFunctions 數組中的元素數目。這個值也是這個模塊導出的函數的數目。理論上,這個值可能與 NumberOfNames 域(下一個域)不同,但實際上它們總是一樣的。 
DWORD   NumberOfNames 
AddressOfNames 數組中的元素數目。這個值看起來總是與 NumberOfFunctions 域的值一樣,因此它也是導出的函數的數目。 
PDWORD  *AddressOfFunctions 
這個域是一個 RVA,並且指向一個函數地址數組。這裏的函數地址是這個模塊中每個導出的函數的入口點的地址(RVA)。 
PDWORD  *AddressOfNames 
這個域是一個 RVA,並且指向一個字符串指針數組。這裏的字符串是這個模塊中導出的函數的名稱的字符串。 
PWORD   *AddressOfNameOrdinals 
這個域是一個 RVA,並且指向一個 WORD 類型的數組。這裏的 WORD 是這個模塊中導出的函數的序號。但是,不要忘記加上 Base 域指定的起始序號。

  導出表的佈局有點奇怪。導出一個函數需要函數的名稱、相應的地址和導出序數這三部分內容。你可能認爲 PE 格式的設計者會把這三種信息放在一個結構中,然後用一個這種結構的數組就可以了。但事實是,每個要導出的函數的三部分內容之一都是某個數組中的一個元素。總共有三個這樣的數組(AddressOfFunctions,AddressOfNames, 
AddressOfNameOrdinals),它們是並列的。假如你要查找導出的第N個函數的信息,你需要在每個數組中都查找其第N個元素。

典型的 DLL 文件的導出表 
Name:            KERNEL32.dll 
  Characteristics: 00000000 
  TimeDateStamp:   2C4857D3 
  Version:         0.00 
  Ordinal base:    00000001 
  # of functions:  0000021F 
  # of Names:      0000021F 
 
  Entry Pt  Ordn  Name 
  00005090     1  AddAtomA 
  00005100     2  AddAtomW 
  00025540     3  AddConsoleAliasA 
  00025500     4  AddConsoleAliasW 
  00026AC0     5  AllocConsole 
  00001000     6  BackupRead 
  00001E90     7  BackupSeek 
  00002100     8  BackupWrite 
  0002520C     9  BaseAttachCompleteThunk 
  00024C50    10  BasepDebugDump 
  // 表中的其餘部分省略…… 


PE 文件的資源 

查找 PE 文件中的資源比 NE 文件稍微複雜一點。單個資源(例如菜單)的格式並沒有發生什麼大的變化,但你需要通過一個奇怪的層次結構才能找到它們。 
瀏覽資源目錄的層次結構就像是瀏覽硬盤一樣。有一個主目錄(根目錄),它下面有子目錄。各個子目錄還有它們自己的子目錄。這些更下層的子目錄可能指向了原始的資源數據(例如對話框模板)。在 PE 格式中,資源目錄層次結構中的根目錄和它的子目錄都是IMAGE_RESOURCE_DIRECTORY 類型的結構。 
DWORD   Characteristics 
理論上這個域可能是資源的標誌,但它好像總是 0。 
DWORD   TimeDateStamp 
指示資源創建日期的日期/時間戳。 
WORD    MajorVersion 
WORD    MinorVersion 

理論上這些域應該保存資源的版本號,但它們好像總是 0。 
WORD    NumberOfNamedEntries 
本結構後面使用名稱的數組元素的個數。 
WORD    NumberOfIdEntries 
本結構後面使用整數 ID 的數組元素的個數。 
IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[] 
這個域實際上並不是 IMAGE_RESOURCE_DIRECTORY 結構的一部分。它是緊跟在IMAGE_RESOURCE_DIRECTORY 結構後面的類型爲IMAGE_RESOURCE_DIRECTORY_ENTRY 結構的數組。這個數組中的元素數目是 NumberOfNamedEntries 和 NumberOfIdEntries 這兩個域的和。用名稱作爲標識的元素(而不是用整數 ID)在這個數組的前面一部分。
 
   一個目錄項(Directory Entry)或者指向一個子目錄(即另外一個IMAGE_RESOURCE_DIRECTORY),或者指向資源的原始數據。通常在你獲取資源的原始數據之前,
至少要經過三級目錄。頂級目錄(只有一個)總是位於資源節(.rsrc)的開頭。頂級目錄的子目錄對應於文件中各種類型的資源。例如如果一個 PE 文件中包含對話框、字符串表和菜單,那將會有三個子目錄:一個對話框目錄、一個字符串表目錄和一個菜單目錄。這些類型的子目錄中的每一個最終都會有一個 ID 子目錄。對於特定的資源類型的每個實例都會有一個子目錄。例如在上面的例子中,如果有三個對話框,那對話框目錄將會有三個 ID 子目錄。每個 ID 子目錄或者有一個以字符串表示的名稱(例如“MyDialog”),或者有一個整數 ID,這個 ID 就是在 RC 文件中用於標識資源的。圖 5 以可視化的形式顯示了資源目錄的層次結構。表 13 顯示的是 PEDUMP
輸出的 Windows NT 的 CLOCK.EXE 文件的資源。





發佈了44 篇原創文章 · 獲贊 0 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章