PE檔案結構
作者:薑江
E-mail:jznsmail@163.net
Blog:http://blog.csdn.net/jznsmail/
QQ:457283
PE檔案布局
PE表頭(PE Header)
PE表頭包涵程式碼、資料地區大小、位置、適用的作業系統、堆棧初始大小等重要訊息。PE表頭並非在檔案的最開始。
檔案最開始數百個單元是DOS stub:一個極小的DOS程式,用來輸出像"This Program cannot be run in DOS mode"這樣的資訊。當Win32載入程式把一個PE檔案對應到記憶體時,記憶體對應檔(Memory mapped file)的第一個位單元對應到DOS Stub的第一個位單元。在DOS Stub表頭中可以通過一個結構找到真正的PE表頭。
pNTHeader = dosHeader + dosHeader->e_lfanew;
e_lfanew是一個相對的位移量,指向真正的PE表頭。
dosHeader是image的基址。
注意:應為記憶體向上增長,所以加位移量而不是減。
PE表頭是整個IMAGE_NT_HEADERS,這個結構有一個DWORD和兩個子結構:
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER OptionalHeader;
如果e_lfanew指向一個NE Signature而不是PE Signature表示一個Win16 NE可執行檔,如果是LE Signature表示一個VxD文檔。如果是LX Signatrue表OS/2文檔。
IMAGE_FILE_HEADER結構如下:
DWORD Machine;指示使用那種CPU,可以在Winnt.h中找到(我的標頭檔中定義如下)
#define IMAGE_FILE_MACHINE_UNKNOWN 0
#define IMAGE_FILE_MACHINE_I386 0x014c // Intel 386.
#define IMAGE_FILE_MACHINE_R3000 0x0162 // MIPS little-endian, 0x160 big-endian
#define IMAGE_FILE_MACHINE_R4000 0x0166 // MIPS little-endian
#define IMAGE_FILE_MACHINE_R10000 0x0168 // MIPS little-endian
#define IMAGE_FILE_MACHINE_WCEMIPSV2 0x0169 // MIPS little-endian WCE v2
#define IMAGE_FILE_MACHINE_ALPHA 0x0184 // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC 0x01F0 // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_SH3 0x01a2 // SH3 little-endian
#define IMAGE_FILE_MACHINE_SH3E 0x01a4 // SH3E little-endian
#define IMAGE_FILE_MACHINE_SH4 0x01a6 // SH4 little-endian
#define IMAGE_FILE_MACHINE_ARM 0x01c0 // ARM Little-Endian
#define IMAGE_FILE_MACHINE_THUMB 0x01c2
#define IMAGE_FILE_MACHINE_IA64 0x0200 // Intel 64
#define IMAGE_FILE_MACHINE_MIPS16 0x0266 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU 0x0366 // MIPS
#define IMAGE_FILE_MACHINE_MIPSFPU16 0x0466 // MIPS
#define IMAGE_FILE_MACHINE_ALPHA64 0x0284 // ALPHA64
#define IMAGE_FILE_MACHINE_AXP64 IMAGE_FILE_MACHINE_ALPHA64
WORD NumberOfSections;EXE的OBJ中的Sections個數
DWORD TimeDateStamp;連接器產生此檔案的時間。自從1969年12月31日4:00P.M之後的總秒數。
DWORD PointerToSymbolTable;COFF符號表的位移位置。只對COFF除錯有用。
DWORD NumberOfSymbols;COFF符號表中的符號個數。
DWORD SizeOfOptionalHeader;一個可有可無的表頭大小,在EXE檔案中,這也就是IMAGE_OPTIONAL_HEADER的大小。在OBJ檔案中大多數時候為0。
WORD Characteristics;描述該檔案的性質。比較重要的性質如下:
0x0001 檔案沒有重定位
0x0002 檔案是可執行檔
0x2000 檔案是動態串連庫
下面列出系統定義的所有性質:
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 // File is executable (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM 0x0010 // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE 0x0100 // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM 0x1000 // System File.
#define IMAGE_FILE_DLL 0x2000 // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 // Bytes of machine word are reversed.
IMAGE_OPTINAL_HEADER結構
該結構是在IMAGE_FILE_HEADER之外的一些附加資訊。
WORD Magic;定義image的狀態。如0x0107表示一個rom image,0x010B表示一個正常的exe image。
BYTE MajorLinkerVersion
BYTE MinorLinkerVersion PE檔案的連接器版本,以10進位表示。
其它的資訊可以參看Winnt.h標頭檔
Section Table
內含image的每個sections資訊。section以起始位置排列而不是字母順序。section table的每個地區儲存了一個地址,檔案的原始資料被映射到記憶體。sections是一個個記憶體範圍。程式和作業系統所需要的任何code和data都有一個相應的section儲存。
PE表頭最後是一個IMAGE_SECTION_HEADER結構數組,該數組元素個數紀錄在IMAGE_NT_HEADER.FileHeader.NumberOfSection中。
IMAGE_SECTION_HEADER是EXE檔案或OBJ檔案SECTION的完整資訊。
BYTE Name[IMAGE_SIZEOF_SHORT_NAME] 是8位的ANSI名稱(沒有NULL結束符),表示SECTION名稱(如.text)。
union{
DWORD PhysicalAddress;
DWORD VirtualSize;
}Misc;
在EXE檔案中代表code section或data section的虛擬記憶體大小(未進行alignment)。
對於OBJ檔案代表SECTION的實際地址。第一個SECTION從0開始。下一個SECTION起始地址為上一個SECTION地址加上SizeOfRawData值(進行調整後的section虛擬記憶體大小)。
DWORD VirtualAddress;在EXE中代表載入程式將SECTION映射到的虛擬位址。SECTION的真正起始地址是該地址加上基址。這個地址常被編譯器設定成0x1000。
在OBJ檔案中沒有意義,總是0。
DWORD SizeOfRawData; 在EXE檔案中代表section大小被對齊(alignment)後的值。
在OBJ中表示由編譯指定的真正SECTION大小。
DWORD PointerToRawData;從檔案頭開始的位移量,Section的初始資訊可以從這個位置獲得。
DWORD PointerToRelocations;在EXE中沒有意義,總為0。
在OBJ中這是從檔案頭開始的位移量來指向SECTION的重定位資訊。每個OBJ SECTION的重定位資訊緊跟在SECTION資訊後。
DWORD PointerToLinenumbers;行號表的位移地址。在exe檔案中,行號資訊放在檔案的末尾。在obj檔案中行號表放在每個section的原始資料以及重定位表之後。
WORD NumberofRelocations;重定位表中的重定位項目個數(PointerToRelocations指向)。只用於obj檔案。
WORD NumberOfLinenumbers;行號表中行號個數(由PointerToLinenumbers指向)。
DWORD Characteristics; 一組標誌,表示section中的屬性(如code或data可讀、可寫等)。可以參看winnt.h中IMAGE_SCN_XXX_XXX的定義。
Sections
.text section
包涵所有一般性的程式碼。在.text中除了編譯器產生出來的代碼以及runtime library的代碼外還有一些東西。在PE檔案中,當你調用另一組模組中的函數(如USER32.DLL中的GetMessage),編譯器產生的CALL指令並沒有把控制權直接傳給DLL中的函數,而是傳給一個JMP DWORD PTR [XXXXXXXX]的指令,該指令也在.text中。JMP指令跳轉到.idata中的一個DWORD中。這個DWORD含有真正的函數入口地址。
例如:
對DLL的調用方式為什麼要通過這種方式?
通過對同一個DLL函數的所有調用都集中到一處,載入程式就不需要修改每個調用DLL的指令,只需要把DLL函數的真真實位址放到.idata的那個DWORD中。這樣調用帶來一個缺點,那就是你不能以DLL函數的真正地址來初始化一個變數。
例如:
FARPROC pfnGetMessage = GetMessage;
這個變數實際存放的JMP DWORD PTR [XXXXXXXX]指令地址,而不是實際需要調用的函數地址。
對於用__declspec(dllimport)修飾的API函數,編譯器不產生JMP DWORD PTR [XXXXXXXX]指令,而是產生CALL DWORD PTR[XXXXXXXX]來調用位於.idata中的XXXXXXXX。
.data section
存放初始化資訊的地方。包括全域變數、字串常量和靜態變數,這些變數在編譯時間期就給定初值。連接器把obj和lib中的所有.data組合起來放到exe的.data中。而變數放在執行過程中的堆棧中。
.bss section
存放任何未初始化的靜態變數和全域變數。obj和lib中所有.bss組合起來放在exe檔案的.bss中。在section table中.bss的RawDataOffset總為0,表示這個section不佔用任何空間。
.CRT section
微軟C/C++ runtime library(CRT)所使用的另一個初始化的data section。這裡存放用於在main或WinMan之前執行的靜態C++類的建構函式。
.rsrc section
存放模組的資源。如.res檔案內容。
.idata section
包涵有關模組從其他DLLs輸入(import)函數和資料的相關資訊。
.edata section
存放PE檔案輸出函數的相關資訊。通常只在DLL中才看到.edata。
.reloc section
存放一個base relocation數組。base relocation是一組指令或者初始設定變數的調整值。如果載入程式沒有辦法把exe或者dll載入預設地址,就必須做這樣的調整。
.tls section
當使用__declspec(thread)時,定義的資訊並沒有放入.data或.bss,而是有一份拷貝放到.tls中。
.tls的全稱是thread local storage。每個線程可以擁有自己的一組靜態資料,使用這些資料的程式碼,不需要是那個線程正在執行。假設某個程式有數個線程處理相同的工作。如果聲明一個stl如:
__declspec(thread) int i = 0; //this is a global variable declaration
每個線程將因此擁有變數i的一個副本。
.rdata section
至少有4個用途:
1.在使用微軟串連程式產生的EXE中,.rdata含有debug directory(obj中不含)。
2.如果在程式的.DEF檔案中指定DESCRIPTION,被指定的字串就會出現在.rdata中。
3.GUID值都放在EXE或DLL的.rdata中
4.放置TLS(Thread Local Storage)的directory。被編譯器的runtime library使用。
.drectve section
只出現在obj檔案中,內含連接器命令參數的文字描述。
PE檔案的輸入
在被載入記憶體之前,存放在PE檔案的.idata中的資訊是給載入程式來決定函數地址並且修正它們,以便完成image用的。而在被載入之後,idata內涵的是指標,指向EXE/DLL的輸入函數。
.idata section(import table)以一個IMAGE_IMPORT_DESCRIPTIOR數組開始,被PE檔案串連的DLL都會在此有一個對應的IMAGE_IMPORT_DESCRIPTOR結構。
PE檔案的輸出
PE檔案的輸出函數相關資訊存放在.edata。.edata一開始是一個IMAGE_EXPORT_DIRECTORY結構。
PE文檔的基址重定位