目前網絡上有關PE文件結構說明的文章太多了,本身的這篇文章只是單純的記錄本身對PE文件結構的學習、理解和總結。windows
PE(Portable Executable:可移植的執行體)是Win32環境自身所帶的可執行文件格式。它的一些特性繼承自Unix的Coff(Common Object File Format)文件格式。可移植的執行體意味着此文件格式是跨win32平臺的,即便Windows運行在非Intel的CPU上,任何win32平臺的PE裝載器都能識別和使用該文件格式。固然,移植到不一樣的CPU上PE執行體必然得有一些改變。除VxD和16位的Dll外,全部 win32執行文件都使用PE文件格式。所以,研究PE文件格式是咱們洞悉Windows結構的良機。數組
圖表結構:網絡
DOS頭是用來兼容MS-DOS操做系統的
NT頭包含windows PE文件的主要信息
節表:是PE文件後續節的描述
節:每一個節其實是一個容器,能夠包含代碼、數據等等,每一個節能夠有獨立的內存權限,好比代碼節默認有讀/執行權限,節的名字和數量能夠本身定義編輯器
一、PE文件在硬盤上和在內存裏是不徹底同樣的,被加載到內存之後其佔用的虛擬地址空間要比在硬盤上佔用的空間大一些,這是由於各個節在硬盤上是連續的,而在內存中是按頁對齊的。ide
二、PE結構內部,表示某個位置的地址採用了兩種方式,針對在硬盤上存儲文件中的地址,稱爲原始存儲地址或物理地址表示距離文件頭的偏移;另一種是針對加載到內存之後映象中的地址,稱爲相對虛擬地址(RVA),表示相對內存映象頭的偏移。函數
三、CPU的某些指令是須要使用絕對地址的,好比取全局變量的地址,傳遞函數的地址編譯之後的彙編指令中確定須要用到絕對地址而不是相對映象頭的偏移,所以PE文件會建議操做系統將其加載到某個內存地址(這個叫基地址),這種表示方式叫作虛擬地址(VA)學習
四、PE文件沒法加載到預期的地址,那麼系統會幫他從新選擇一個合適的基地址將他加載到此處,這時原有的VA就所有失效了,NT頭保存了PE文件加載所需的信息,在不知道PE會加載到哪一個基地址以前,VA是無效的,因此在PE文件頭中大部分是使用RVA來表示地址的spa
一、PE文件能夠導出函數讓其餘的PE文件使用,也能夠從其餘PE文件導入函數操作系統
二、PE文件經過導出表指明本身導出那些函數,經過導入表指明須要從哪些模塊導入哪些函數。線程
三、DOS頭和NT頭就是PE文件中兩個重要的文件頭
typedef struct _IMAGE_DOS_HEADER { // DOS .EXE header WORD e_magic; // Magic number WORD e_cblp; // Bytes on last page of file WORD e_cp; // Pages in file WORD e_crlc; // Relocations WORD e_cparhdr; // Size of header in paragraphs WORD e_minalloc; // Minimum extra paragraphs needed WORD e_maxalloc; // Maximum extra paragraphs needed WORD e_ss; // Initial (relative) SS value WORD e_sp; // Initial SP value WORD e_csum; // Checksum WORD e_ip; // Initial IP value WORD e_cs; // Initial (relative) CS value WORD e_lfarlc; // File address of relocation table WORD e_ovno; // Overlay number WORD e_res[4]; // Reserved words WORD e_oemid; // OEM identifier (for e_oeminfo) WORD e_oeminfo; // OEM information; e_oemid specific WORD e_res2[10]; // Reserved words LONG e_lfanew; // File address of new exe header } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
重點關注字段
e_magic:一個WORD類型,值是一個常數0x4D5A,用文本編輯器查看該值位‘MZ’,可執行文件必須都是'MZ'開頭。
e_lfanew:爲32位可執行文件擴展的域,用來表示DOS頭以後的NT頭相對文件起始地址的偏移。
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
Signature:相似於DOS頭中的e_magic,其高16位是0,低16是0x4550,用字符表示是'PE‘。
IMAGE_FILE_HEADER是PE文件頭
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;
Machine:該文件的運行平臺,是x8六、x64仍是I64
NumberOfSections:該PE文件中有多少個節,也就是節表中的項數。
TimeDateStamp:PE文件的建立時間,通常有鏈接器填寫。
PointerToSymbolTable:COFF文件符號表在文件中的偏移。
NumberOfSymbols:符號表的數量。
SizeOfOptionalHeader:緊隨其後的可選頭的大小。
Characteristics:可執行文件的屬性,能夠是下面這些值按位相或。
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_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
AddressOfEntryPoint:程序入口的RVA,對於exe這個地址能夠理解爲WinMain的RVA。對於DLL,這個地址能夠理解爲DllMain的RVA,若是是驅動程序,能夠理解爲DriverEntry的RVA。固然,實際上入口點並不是是WinMain,DllMain和DriverEntry,在這些函數以前還有一系列初始化要完成,固然,這些不是本文的重點。
BaseOfCode:代碼段起始地址的RVA。
BaseOfData:數據段起始地址的RVA。
ImageBase:映象(加載到內存中的PE文件)的基地址,這個基地址是建議,對於DLL來講,若是沒法加載到這個地址,系統會自動爲其選擇地址。
SectionAlignment:節對齊,PE中的節被加載到內存時會按照這個域指定的值來對齊,好比這個值是0x1000,那麼每一個節的起始地址的低12位都爲0。
FileAlignment:節在文件中按此值對齊,SectionAlignment必須大於或等於FileAlignment。
SizeOfImage:映象的大小,PE文件加載到內存中空間是連續的,這個值指定佔用虛擬空間的大小。
SizeOfHeaders:全部文件頭(包括節表)的大小,這個值是以FileAlignment對齊的。
CheckSum:映象文件的校驗和。
SizeOfStackReserve:運行時爲每一個線程棧保留內存的大小。
SizeOfStackCommit:運行時每一個線程棧初始佔用內存大小。
SizeOfHeapReserve:運行時爲進程堆保留內存大小。
SizeOfHeapCommit:運行時進程堆初始佔用內存大小。
NumberOfRvaAndSizes:數據目錄的項數,即下面這個數組的項數
DataDirectory:數據目錄,這是一個數組,數組的項定義以下:
typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
DataDirectory數據目錄
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0 // Export Directory #define IMAGE_DIRECTORY_ENTRY_IMPORT 1 // Import Directory #define IMAGE_DIRECTORY_ENTRY_RESOURCE 2 // Resource Directory #define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3 // Exception Directory #define IMAGE_DIRECTORY_ENTRY_SECURITY 4 // Security Directory #define IMAGE_DIRECTORY_ENTRY_BASERELOC 5 // Base Relocation Table #define IMAGE_DIRECTORY_ENTRY_DEBUG 6 // Debug Directory // IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 // (X86 usage) #define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE 7 // Architecture Specific Data #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // RVA of GP #define IMAGE_DIRECTORY_ENTRY_TLS 9 // TLS Directory #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10 // Load Configuration Directory #define IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT 11 // Bound Import Directory in headers #define IMAGE_DIRECTORY_ENTRY_IAT 12 // Import Address Table #define IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT 13 // Delay Load Import Descriptors #define IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR 14 // COM Runtime descriptor
導出表是用來描述模塊中的導出函數的結構,若是一個模塊導出了函數,那麼這個函數會被記錄在導出表中,這樣經過GetProcAddress函數就能動態獲取到函數的地址。函數導出的方式有兩種,一種是按名字導出,一種是按序號導出。這兩種導出方式在導出表中的描述方式也不相同。
導出表定義:
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;
圖表:
IMAGE_DIRECTORY_ENTRY_IMPORT就是導入表,在PE文件加載時,會根據這個表裏的內容加載依賴的DLL,並填充所需函數的地址
IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT叫作綁定導入表,在第一種導入表導入地址的修正是在PE加載時完成,若是一個PE文件導入的DLL或者函數多那麼加載起來就會略顯的慢一些,因此出現了綁定導入,在加載之前就修正了導入表,這樣就會快一些。
IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT叫作延遲導入表,一個PE文件也許提供了不少功能,也導入了不少其餘DLL,可是並不是每次加載都會用到它提供的全部功能,也不必定會用到它須要導入的全部DLL,所以延遲導入就出現了,只有在一個PE文件真正用到須要的DLL,這個DLL纔會被加載,甚至於只有真正使用某個導入函數,這個函數地址纔會被修正。
IMAGE_DIRECTORY_ENTRY_IAT是導入地址表,前面的三個表實際上是導入函數的描述,真正的函數地址是被填充在導入地址表中的。
Windows使用重定位機制保證代碼不管模塊加載到哪一個基址都能正確被調用。
編譯的時候由編譯器識別出哪些項使用了模塊內的直接VA,好比push一個全局變量、函數地址,這些指令的操做數在模塊加載的時候就須要被重定位。
連接器生成PE文件的時候將編譯器識別的重定位的項紀錄在一張表裏,這張表就是重定位表,保存在DataDirectory中,序號是 IMAGE_DIRECTORY_ENTRY_BASERELOC。
PE文件加載時,PE 加載器分析重定位表,將其中每一項按照如今的模塊基址進行重定位。
每一個重定位項應該是一個DWORD,裏面保存須要重定位的RVA,這樣只須要簡單操做便能找到須要重定位的項。
然而,Windows並無這樣設計,緣由是這樣存放太佔用空間了,試想一下,加入一個文件有n個重定位項,那麼就須要佔用4*n個字節。
因此Windows採用了分組的方式,按照重定位項所在的頁面分組,每組保存一個頁面起始地址的RVA,頁內的每項重定位項使用一個WORD保存重定位項在頁內的偏移,這樣就大大縮小了重定位表的大小。
定義:
typedef struct _IMAGE_BASE_RELOCATION { DWORD VirtualAddress; DWORD SizeOfBlock; // WORD TypeOffset[1]; } IMAGE_BASE_RELOCATION; typedef IMAGE_BASE_RELOCATION UNALIGNED * PIMAGE_BASE_RELOCATION;
VirtualAddress:頁起始地址RVA。
SizeOfBlock:表示該分組保存了幾項重定位項。
TypeOffset:這個域有兩個含義,頁內偏移用12位就能夠表示,剩下的高4位用來表示重定位的類型。而事實上,Windows只用了一種類型IMAGE_REL_BASED_HIGHLOW數值是 3。
哪些項目須要被重定位呢??
1.代碼中使用全局變量的指令,由於全局變量必定是模塊內的地址,並且使用全局變量的語句在編譯後會產生一條引用全局變量基地址的指令。
2.將模塊函數指針賦值給變量或做爲參數傳遞,由於賦值或傳遞參數是會產生mov和push指令,這些指令須要直接地址。
3.C++中的構造函數和析構函數賦值虛函數表指針,虛函數表中的每一項自己就是重定位項
.text默認的代碼區塊,它的內容全是指令代碼,連接器把全部目標文件的text塊鏈接成一個大的.text塊,
.data默認的讀/寫數據塊,全局變量,靜態變量通常放在這個區段
.rdata默認只讀數據區塊,但程序中不多用到該塊中的數據,通常兩種狀況用到,一是MS 的連接器產生EXE文件中用於存放調試目錄,二是用於存放說明字符串,若是程序的DEF文件中指定了DESCRIPTION,字符串就會出如今rdata中
.idata包含其餘外來的DLL的函數及數據信息,即輸入表,將.idata區塊合併成另外一個區塊已成爲一種慣例
.edata輸出表,當建立一個輸出API或數據的可執行文件時,鏈接器會建立一個.EXP文件,這個.EXP文件包含一個.edata區塊,其會被加載到可執行文件中,常常被合併到.text或.rdata 區塊中
.rsrc資源,包括模塊的所有資源,如圖標,菜單,位圖等,這個區塊是隻讀的,不管如何不該該把它命名爲.rsrc之外的名字,也不能合併到其餘的區塊裏
.bss未初始化的數據,不多在用,取而代之的是執行文件的.data區塊的的VirtualSize被擴展大的空間裏用來裝未初始化的數據.
.crt用於C++ 運行時(CRT)所添加的數據
.tlsTLS的意思是線程局部存儲器,用於支持經過_declspec(thread)聲明的線程局部存儲變量的數據,這包括數據的初始化值,也包括運行時所須要的額外變量
.reloc可執行文件的基址重定位,基址重定位通常僅Dll須要的
.sdata相對於全局指針的可被定位的 短的讀寫數據
.pdata異常表,包含CPU特定的IAMGE_RUNTIME_FUNTION_ENTRY結構數組,DataDirectory中的IMAGE_DIRECTORY_ENTRY_EXCEPTION指向它.
.didat延遲裝入輸入數據,在非Release模式下能夠找到
第一:當PE文件被執行,PE裝載器檢查DOS MZ header裏的PE header偏移量。若是找到,則跳轉到PE header。
第二:PE裝載器檢查PE header的有效性。若是有效,就跳轉到PE header的尾部。
第三:緊跟PE header的是節表。PE裝載器讀取其中的節索引信息,並採用文件映射方法將這些節映射到內存,同時附上節表裏指定的節屬性。
第四:PE文件映射入內存後,PE裝載器將處理PE文件中相似import table(引入表)邏輯部分。