1. 程式人生 > >DOS檔案頭、PE檔案頭、節表和表詳解

DOS檔案頭、PE檔案頭、節表和表詳解

PE(Portable Executeable File Format,可移植的執行體檔案格式),使用該格式的目標是使連結生成的EXE檔案能在不同的CPU工作指令下工作。

可執行檔案的格式是作業系統工作方法的真實寫照。Windows作業系統中可執行程式有好多種,比如COM、PIF、SCR、EXE等,這些檔案的格式大部分都繼承自PE。其中,EXE是最常見的PE檔案,動態連結庫(大部分以dll為副檔名的檔案)也是PE檔案。

PE格式是Windows下最常用的可執行檔案格式,在DOS時代COM檔案是最早的也是結構最簡單的可執行檔案,COM檔案中僅包含可執行程式碼,沒有附帶任何“支援性”資料,所以,第一句執行指令必須安排在檔案頭部:再就是沒有重定位的資訊,這樣程式碼中不能有跨段操作資料的指令,造成程式碼和資料,甚至包括堆疊只能限制在同一個64KB的段中,由於這個原因,DOS系統中又定義了一種可執行檔案—EXE檔案,EXE檔案在程式碼的前面加了一個檔案頭,檔案頭中包括各種說明資料,如檔案入口,堆疊位置,重定位表等,作業系統根據檔案頭的資訊將程式碼部分裝入記憶體,根據重定位表修正程式碼,最後在設定好堆疊後從檔案頭中指定的入口開始執行。
    當Windows3.X出現的時候,可執行檔案中出現了32位程式碼,程式執行時轉到保護模式之前需要在真實模式下做一些初始化,這樣真實模式的16位程式碼必須和32位程式碼一起放在可執行檔案中,舊的DOS可執行檔案格式無法滿足需要,所以Windows3.X執行檔案使用新的LE格式的可執行檔案(Linear executable/線性可執行檔案),Window9x中的VxD程式也是使用LE格式,因為這些驅動程式中也同時包括16位和32位程式碼。
    而在Windows 9x,Windows NT,Windows 2000下,純32位可執行檔案都使用微軟設計的一種新格式——PE格式(Portable Executable File Format/可移值的執行體)。

PE檔案的基本結構如圖示:


一、DOS ME頭IMAGE_DOS_HEADER

IMGAE_DOS_HEADER的具體定義如下:

IMAGE_DOS_HEADER STRUCT 
+00h WORD e_magic  // Magic DOS signature MZ(4Dh 5Ah)   DOS可執行檔案標記 
+02h  WORD e_cblp   // Bytes on last page of file  
+04h WORD e_cp   // Pages in file
+06h WORD e_crlc   // Relocations
+08h WORD e_cparhdr   // Size of header in paragraphs
+0ah WORD e_minalloc   // Minimun extra paragraphs needs
+0ch WORD e_maxalloc  // Maximun extra paragraphs needs
+0eh WORD e_ss   // intial(relative)SS value   DOS程式碼的初始化堆疊SS 
+10h WORD e_sp   // intial SP value   DOS程式碼的初始化堆疊指標SP 
+12h WORD e_csum   // Checksum 
+14h WORD e_ip   //  intial IP value   DOS程式碼的初始化指令入口[指標IP] 
+16h WORD e_cs   // intial(relative)CS value   DOS程式碼的初始堆疊入口 CS
+18h WORD e_lfarlc   // File Address of relocation table 
+1ah WORD e_ovno  //  Overlay number 
+1ch WORD e_res[4]  // Reserved words 
+24h WORD e_oemid   //  OEM identifier(for e_oeminfo) 
+26h WORD e_oeminfo  //  OEM information;e_oemid specific  
+29h WORD e_res2[10]  //  Reserved words 
+3ch LONG  e_lfanew  // Offset to start of PE header   指向PE檔案頭 
IMAGE_DOS_HEADER ENDS

第一個欄位e_magic被定義成字元“MZ”作為識別標誌,後面的一些欄位指明瞭入口地址、堆疊位置和重定位表位置等。

對於PE檔案來說,有用的是最後的e_lfanew欄位,這個欄位指出了真正的PE檔案頭在檔案中的位置,這個位置總是以8位元組為單位對齊的。


從圖中我們可以看到e_lfanew的值為000000E8,也就是說000000E8處是我們的PE檔案頭的位置。

二、PE檔案頭

PE檔案頭是由IMAGE_NT_HEADERS結構定義的:

IMAGE_NT_HEADERS STRUCT 
+0h DWORD Signature                    PE檔案標識
+4h   IMAGE_FILE_HEADER  FileHeader
+18h IMAGE_OPTIONAL_HEADER32 OptionalHeader  
IMAGE_NT_HEADERS ENDS

PE檔案頭的第一個雙字是一個標誌,它被定義為00004550,也就是字元P E加上兩個0,這也是PE這個稱呼的由來。從名稱來看似乎後面的這個PE檔案表頭結構是可選的,但實際上這個名稱是名不符實的,因為它總是存在於每個PE檔案中。

1.IMAGE_FILE_HEADER結構

IMAGE_FILE_HEADER STRUCT
+04h    WORD          Machine;   // 執行平臺
+06h      WORD          NumberOfSections; // 檔案的區塊數目
+08h    DWORD         TimeDateStamp;  // 檔案建立日期和時間
+0Ch      DWORD         PointerToSymbolTable; // 指向符號表(主要用於除錯)
+10h     DWORD         NumberOfSymbols;  // 符號表中符號個數(同上)
+14h      WORD          SizeOfOptionalHeader;  // IMAGE_OPTIONAL_HEADER32 結構大小
+16h      WORD          Characteristics;  // 檔案屬性
IMAGE_FILE_HEADER ENDS


為大家詳細解釋各個成員的含義和用法:


①Machine:可執行檔案的目標CPU型別。

更多定義參見Windows.inc檔案。

②NumberOfSection: 區塊的數目。(注:區塊表是緊跟在 IMAGE_NT_HEADERS 後邊的)

③TimeDataStamp: 表明檔案是何時被建立的。

這個值是自1970年1月1日以來用格林威治時間(GMT)計算的秒數,這個值是比檔案系統(FILESYSTEM)的日期時間更加精確的指示器。

④PointerToSymbolTable: COFF 符號表的檔案偏移位置,現在基本沒用了。

⑤NumberOfSymbols: 如果有COFF 符號表,它代表其中的符號數目,COFF符號是一個大小固定的結構,如果想找到COFF 符號表的結束位置,則需要這個變數。

⑥SizeOfOptionalHeader: 緊跟著IMAGE_FILE_HEADER 後邊的資料結構(IMAGE_OPTIONAL_HEADER)的大小。(對於32位PE檔案,這個值通常是00E0h;對於64位PE32+檔案,這個值是00F0h )。

⑦Characteristics: 檔案屬性,有選擇的通過幾個值可以運算得到。( 這些標誌的有效值是定義於 winnt.h 內的 IMAGE_FILE_** 的值,具體含義見下表。普通的EXE檔案這個欄位的值一般是 0100h,DLL檔案這個欄位的值一般是 210Eh。)多種屬性可以通過 “或運算” 使得同時擁有!


2.IMAGE_OPTIONAL_HEADER32結構

IMAGE_OPTIONAL_HEADER32 STRUCT
+18h    WORD    Magic;         // 標誌字, ROM 映像(0107h),普通可執行檔案(010Bh)
+1Ah    BYTE      MajorLinkerVersion;     // 連結程式的主版本號
+1Bh    BYTE      MinorLinkerVersion;     // 連結程式的次版本號
+1Ch    DWORD   SizeOfCode;     // 所有含程式碼的節的總大小
+20h    DWORD   SizeOfInitializedData;    // 所有含已初始化資料的節的總大小
+24h    DWORD   SizeOfUninitializedData; // 所有含未初始化資料的節的大小
+28h    DWORD   AddressOfEntryPoint;    // 程式執行入口RVA
+2Ch    DWORD   BaseOfCode;      // 程式碼的區塊的起始RVA
+30h    DWORD   BaseOfData;      // 資料的區塊的起始RVA
+34h    DWORD   ImageBase;      // 程式的首選裝載地址
+38h    DWORD   SectionAlignment;      // 記憶體中的區塊的對齊大小
+3Ch    DWORD   FileAlignment;      // 檔案中的區塊的對齊大小
+40h    WORD    MajorOperatingSystemVersion;  // 要求作業系統最低版本號的主版本號
+42h    WORD    MinorOperatingSystemVersion;  // 要求作業系統最低版本號的副版本號
+44h    WORD    MajorImageVersion;       // 可運行於作業系統的主版本號
+46h    WORD    MinorImageVersion;       // 可運行於作業系統的次版本號
+48h    WORD    MajorSubsystemVersion;  // 要求最低子系統版本的主版本號
+4Ah    WORD    MinorSubsystemVersion;  // 要求最低子系統版本的次版本號
+4Ch    DWORD   Win32VersionValue;       // 莫須有欄位,不被病毒利用的話一般為0
+50h    DWORD   SizeOfImage;       // 映像裝入記憶體後的總尺寸
+54h    DWORD   SizeOfHeaders;       // 所有頭 + 區塊表的尺寸大小
+58h    DWORD   CheckSum;       // 映像的校檢和
+5Ch    WORD    Subsystem;       // 可執行檔案期望的子系統
+5Eh    WORD    DllCharacteristics;       // DllMain()函式何時被呼叫,預設為 0
+60h    DWORD   SizeOfStackReserve;       // 初始化時的棧大小
+64h    DWORD   SizeOfStackCommit;       // 初始化時實際提交的棧大小
+68h    DWORD   SizeOfHeapReserve;        // 初始化時保留的堆大小
+6Ch    DWORD   SizeOfHeapCommit;        // 初始化時實際提交的堆大小
+70h    DWORD   LoaderFlags;        // 與除錯有關,預設為 0
+74h    DWORD   NumberOfRvaAndSizes;  // 下邊資料目錄的項數,這個欄位自Windows NT 釋出以來一直是16
+78h    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];  // 資料目錄表
IMAGE_OPTIONAL_HEADER32 ENDS

事實上,這個結構中的大部分欄位都不重要,大家可以從註釋中理解它們的含義,我將比較重要的欄位在下邊跟大家詳細講解:

①AddressOfEntryPoint欄位

指出檔案被執行時的入口地址,這是一個RVA地址。如果在一個可執行檔案上附加了一段程式碼並想讓這段程式碼首先被執行,那麼只需要將這個入口地址指向附加的程式碼就可以了。

②ImageBase欄位

指出檔案的優先裝入地址。也就是說當檔案被執行時,如果可能的話,Windows優先將檔案裝入到由ImageBase欄位指定的地址中,只有指定的地址已經被**模組使用時,檔案才被裝入到**地址中。連結器產生可執行檔案的時候對應這個地址來生成機器碼,所以當檔案被裝入這個地址時不需要進行重定位操作,裝入的速度最快,如果檔案被裝載到**地址的話,將不得不進行重定位操作,這樣就要慢一點。

對於EXE檔案來說,由於每個檔案總是使用獨立的虛擬地址空間,優先裝入地址不可能被**模組佔據,所以EXE總是能夠按照這個地址裝入,這也意味著EXE檔案不再需要重定位資訊。對於DLL檔案來說,由於多個DLL檔案全部使用宿主EXE檔案的地址空間,不能保證優先裝入地址沒有被**的DLL使用,所以DLL檔案中必須包含重定位資訊以防萬一。因此,在前面介紹的 IMAGE_FILE_HEADER 結構的 Characteristics 欄位中,DLL 檔案對應的 IMAGE_FILE_RELOCS_STRIPPED 位總是為0,而EXE檔案的這個標誌位總是為1。

在連結的時候,可以通過對link.exe指定/base:address選項來自定義優先裝入地址,如果不指定這個選項的話,一般EXE檔案的預設優先裝入地址被定為00400000h,而DLL檔案的預設優先裝入地址被定為10000000h。

③SectionAlignment 欄位和 FileAlignment欄位

SectionAlignment欄位指定了節被裝入記憶體後的對齊單位。也就是說,每個節被裝入的地址必定是本欄位指定數值的整數倍。而FileAlignment欄位指定了節儲存在磁碟檔案中時的對齊單位。

④Subsystem欄位

指定使用介面的子系統,它的取值如表所示。這個欄位決定了系統如何為程式建立初始的介面,連結時的/subsystem:**選項指定的就是這個欄位的值,在前面章節的程式設計中我們早已知道:如果將子系統指定為Windows CUI,那麼系統會自動為程式建立一個控制檯視窗,而指定為Windows GUI的話,視窗必須由程式自己建立。介面子系統的取值和含義如下:


⑤DataDirectory欄位

這個欄位可以說是最重要的欄位之一,它由16個相同的IMAGE_DATA_DIRECTORY結構組成,雖然PE檔案中的資料是按照裝入記憶體後的頁屬性歸類而被放在不同的節中的,但是這些處於各個節中的資料按照用途可以被分為匯出表、匯入表、資源、重定位表等資料塊,這16個IMAGE_DATA_DIRECTORY結構就是用來定義多種不同用途的資料塊的IMAGE_DATA_DIRECTORY結構的定義很簡單,它僅僅指出了某種資料塊的位置和長度。

IMAGE_DATA_DIRECTORY STRUCT
VirtualAddress DWORD ?    ;資料的起始RVA
isize DWORD ?    ;資料塊的長度
IMAGE_DATA_DIRECTORY ENDS

資料目錄列表的含義如下:


在PE檔案中尋找特定的資料時就是從這些IMAGE_DATA_DIRECTORY結構開始的,比如要存取資源,那麼必須從第3個IMAGE_DATA_DIRECTORY結構(索引為2)中得到資源資料塊的大小和位置;同理,如果要檢視PE檔案匯入了哪些DLL檔案的哪些API函式,那就必須首先從第2個IMAGE_DATA_DIRECTORY結構得到匯入表的位置和大小。

好了,我們繼續接著上一篇來講解節表和表。

三、節表和節

1.首先我們先來了解Windows是如何將PE檔案對映到記憶體的。

在執行一個PE檔案的時候,windows 並不在一開始就將整個檔案讀入記憶體的,而是採用與記憶體對映檔案類似的機制。也就是說,windows 裝載器在裝載的時候僅僅建立好虛擬地址和PE檔案之間的對映關係。當且僅當真正執行到某個記憶體頁中的指令或者訪問某一頁中的資料時,這個頁面才會被從磁碟提交到實體記憶體,這種機制使檔案裝入的速度和檔案大小沒有太大的關係。

但是要注意的是,系統裝載可執行檔案的方法又不完全等同於記憶體對映檔案。當使用記憶體對映檔案的時候,系統對“原著”相當忠實,如果將磁碟檔案和記憶體映像比較的話,可以發現不管是資料本身還是資料之間的相對位置的都是完全相同的。而我們知道,在裝載可執行檔案的時候,有些資料在裝入前會被預處理,如重定位等,正因此,裝入以後,資料之間的相對位置可能發生微妙的變化。


Windows 裝載器在裝載DOS部分、PE檔案頭部分和節表(區塊表)部分是不進行任何特殊處理的,而在裝載節(區塊)的時候則會自動按節(區塊)的屬性做不同的處理。

①記憶體頁的屬性:

對於磁碟對映檔案來說,所有的頁都是按照磁碟對映檔案函式指定的屬性設定的。但是在裝載可執行檔案時,與節對應的記憶體頁屬性要按照節的屬性來設定。所以,在同屬於一個模組的記憶體頁中,從不同節對映過來的的記憶體頁的屬性是不同的。

②節的偏移地址:

節的起始地址在磁碟檔案中是按照 IMAGE_OPTIONAL_HEADER32 結構的 FileAlignment 欄位的值進行對齊的,而當被載入到記憶體中時是按照同一結構中的 SectionAlignment 欄位的值對其的,兩者的值可能不同,所以一個節被裝入記憶體後相對於檔案頭的偏移和在磁碟檔案中的偏移可能是不同的。

注意,節事實上就是相同屬性資料的組合!當節被裝入到記憶體中的時候,相同一個節所對應的記憶體頁都將被賦予相同的頁屬性, 事實上,Windows 系統對記憶體屬性的設定是以頁為單位進行的,所以節在記憶體中的對齊單位必須至少是一個頁的大小。(對於32位作業系統來說,這個值一般是4KB==1000H; 對於64位作業系統這個值一般是8KB==2000H)。節在磁碟中就沒有最小4K的限制,為了減少磁碟檔案的大小,檔案對齊的單位一般要小於記憶體對齊的單位(FileAlignment的值一般為200h,一個扇區),這樣,在磁碟中就不必為每個節最後的零頭資料補足4KB的大小了。

③節的尺寸:

對節的尺寸的處理主要分為兩個方面:

第一個方面,正如剛剛我們所講的,由於磁碟映像和記憶體映像中節對齊儲存單位的不同而導致了長度擴充套件不同(填充的0數量不同嘛~);

第二個方面,是對於包含未初始化資料的節的處理問題。既然是未初始化,那麼沒有必要為其在磁碟中浪費空間資源,但在記憶體中不同,因為程式一執行,之前未初始化的資料便有可能要被賦值初始化,那麼就必須為他們留下空間。

④不進行對映的節:

有些節並不需要被對映到記憶體中,例如.reloc節,重定位資料對於檔案的執行程式碼來說是透明的,無作用的,它只是提供Windows 裝載器使用,執行程式碼根本不會去訪問到它們,所以沒有必要將他們對映到實體記憶體中。

2.節表

PE檔案中所有節的屬性都被定義在節表中,節表由一系列的IMAGE_SECTION_HEADER結構排列而成,每個結構用來描述一個節,結構的排列順序和它們描述的節在檔案中的排列順序是一致的。全部有效結構的最後以一個空的IMAGE_SECTION_HEADER結構作為結束,所以節表中IMAGE_SECTION_HEADER結構數量等於節的數量加一。節表總是被存放在緊接在PE檔案頭的地方。

另外,節表中 IMAGE_SECTION_HEADER 結構的總數總是由PE檔案頭 IMAGE_NT_HEADERS 結構中的 FileHeader.NumberOfSections 欄位來指定的。

IMAGE_SECTION_HEADER STRUCT
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 8個位元組的節區名稱
union Misc
  DWORD PhysicalAddress;       
  DWORD VirtualSize;            //節區的尺寸
ends
DWORD VirtualAddress;         // 節區的 RVA 地址
DWORD SizeOfRawData;            // 在檔案中對齊後的尺寸
DWORD PointerToRawData;        // 在檔案中的偏移量
DWORD PointerToRelocations;     // 在OBJ檔案中使用,重定位的偏移
DWORD PointerToLinenumbers;   // 行號表的偏移(供除錯使用地)
WORD NumberOfRelocations;      // 在OBJ檔案中使用,重定位項數目
WORD NumberOfLinenumbers;    // 行號表中行號的數目
DWORD Characteristics;       // 節屬性如可讀,可寫,可執行等
IMAGE_SECTION_HEADER ENDS

真正有用的幾個欄位說明如下:

①Name1:區塊名。這是一個由8位的ASCII 碼名,用來定義區塊的名稱。多數區塊名都習慣性以一個“.”作為開頭(例如:.text),這個“.” 實際上是不是必須的。值得我們注意的是,如果區塊名達到8 個位元組,後面就沒有0字元了。並且前邊帶有一個“$” 的區塊名字會從聯結器那裡得到特殊的待遇,前邊帶有“$” 的相同名字的區塊在載入時候將會被合併,在合併之後的區塊中,他們是按照“$” 後邊的字元的字母順序進行合併的。
每個區塊的名稱都是唯一的,不能有同名的兩個區塊。但事實上節的名稱不代表任何含義,他的存在僅僅是為了正規統一程式設計的時候方便程式設計師檢視方便而設定的一個標記而已。所以將包含程式碼的區塊命名為“.Data” 或者說將包含資料的區塊命名為“.Code” 都是合法的。
當我們要從PE 檔案中讀取需要的區塊時候,不能以區塊的名稱作為定位的標準和依據,正確的方法是按照 IMAGE_OPTIONAL_HEADER32 結構中的資料目錄欄位結合進行定位。

②Virtual Size:對錶對應的區塊的大小,這是區塊的資料在沒有進行對齊處理前的實際大小。

③Virtual Address:該區塊裝載到記憶體中的RVA 地址。這個地址是按照記憶體頁來對齊的,因此它的數值總是 SectionAlignment 的值的整數倍。

④PointerToRawData:指出節在磁碟檔案中所處的位置。這個數值是從檔案頭開始算起的偏移量。

⑤SizeOfRawData:該區塊在磁碟中所佔的大小,這個數值等於VirtualSize欄位的值按照FileAlignment的值對齊以後的大小。

依靠上面4個欄位的值,裝載器就可以從PE檔案中找出某個節(從PointerToRawData偏移開始的SizeOfRawData位元組)的資料,並將它對映到記憶體中去(對映到從模組基地址偏移VirtualAddress的地方,並佔用以VirtualSize的值按照頁的尺寸對齊後的空間大小)。

⑥Characteristics:該區塊的屬性。該欄位是按位來指出區塊的屬性(如程式碼/資料/可讀/可寫等)的標誌。

3.RVA和檔案偏移的轉換

RVA 是相對虛擬地址(Relative Virtual Address)的縮寫,顧名思義,它是一個“相對地址”。PE 檔案中的各種資料結構中涉及地址的欄位大部分都是以 RVA 表示的。

RVA 是當PE 檔案被裝載到記憶體中後,某個資料位置相對於檔案頭的偏移量。舉個例子,如果 Windows 裝載器將一個PE 檔案裝入到 00400000h 處的記憶體中,而某個區塊中的某個資料被裝入 0040**xh 處,那麼這個資料的 RVA 就是(0040**xh – 00400000h )= **xh,反過來說,將 RVA 的值加上檔案被裝載的基地址,就可以找到資料在記憶體中的實際地址。


很明顯,DOS 檔案頭、PE 檔案頭和區塊表的偏移位置與大小均沒有變化。而各個區塊對映到記憶體後,其偏移位置就發生了變化。

當處理PE 檔案時候,任何的 RVA 必須經過到檔案偏移的換算,才能用來定位並訪問檔案中的資料,但換算卻無法用一個簡單的公式來完成,事實上,唯一可用的方法就是最土最笨的方法:
步驟一:迴圈掃描區塊表得出每個區塊在記憶體中的起始 RVA(根據IMAGE_SECTION_HEADER 中的VirtualAddress 欄位),並根據區塊的大小(根據IMAGE_SECTION_HEADER 中的SizeOfRawData 欄位)算出區塊的結束 RVA(兩者相加即可),最後判斷目標 RVA 是否落在該區塊內。
步驟二:通過步驟一定位了目標 RVA 處於具體的某個區塊中後,那麼用目標 RVA 減去該區塊的起始 RVA ,這樣就能得到目標 RVA 相對於起始地址的偏移量 RVA2.
步驟三:在區塊表中獲取該區塊在檔案中所處的偏移地址(根據IMAGE_SECTION_HEADER 中的PointerToRawData 欄位), 將這個偏移值加上步驟二得到的 RVA2 值,就得到了真正的檔案偏移地址。