PE結構詳解(64位和32位的差別)
1 基本概念
下表描述了貫穿於本文中的一些概念:
名稱 | 描述 |
地址 | 是“虛擬地址”而不是“實體地址”。為什麼不是“實體地址”呢?因為資料在記憶體的位置經常在變,這樣可以節省記憶體開支、避開錯誤的記憶體位置等的優勢。同時使用者並不需要知道具體的“真實地址”,因為系統自己會為程式準備好記憶體空間的(只要記憶體足夠大) |
映象檔案 | 包含以EXE檔案為代表的“可執行檔案”、以DLL檔案為代表的“動態連結庫”。為什麼用“映象”?這是因為他們常常被直接“複製”到記憶體,有“映象”的某種意思。看來西方人挺有想象力的哦^0^ |
RVA | 英文全稱Relatively Virtual Address。偏移(又稱“相對虛擬地址”)。相對映象基址的偏移。 |
節 | 節是PE檔案中程式碼或資料的基本單元。原則上講,節只分為“程式碼節”和“資料節”。 |
VA | 英文全稱Virtual Address。基址 |
2 概覽
x86都是32位的,IA-64都是64位的。64位Windows需要做的只是修改PE格式的少數幾個域。這種新的格式被稱為PE32+。它並沒有增加任何新域,僅從PE格式中刪除了一個域。其餘的改變就是簡單地把某些域從32位擴充套件到64位。在大部分情況下,你都能寫出同時適用於32位和64位PE檔案的程式碼。
EXE檔案與DLL檔案的區別完全是語義上的。它們使用的是相同的PE格式。惟一的不同在於一個位,這個位用來指示檔案應該作為EXE還是DLL。甚至DLL檔案的副檔名也完全也是人為的。你可以給DLL一個完全不同的副檔名,例如.OCX控制元件和控制面板小程式(.CPL)都是DLL。
圖1 解釋了Microsoft PE可執行檔案格式:
PE檔案總體上分為“頭”和“節”。“頭”是“節”的描述、簡化、說明,“節”是“頭”的具體化。
3 檔案頭
PE檔案的頭分為DOS頭、NT頭、節頭。注意,這是本人的分法,在此之前並沒有這種分法。這樣分法會更加合理,更易理解。因為這三個部分正好構成SizeOfHeaders所指的範圍,所以將它們合為“頭”。這裡的3個頭與別的文章的頭的定義會有所區別。
節頭緊跟在NT頭後面。
3.1 DOS頭(PE檔案簽名的偏移地址就是大小)
用記事本開啟任何一個映象檔案,其頭2個位元組必為字串“MZ”,這是Mark Zbikowski的姓名縮寫,他是最初的MS-DOS設計者之一。然後是一些在MS-DOS下的一些引數,這些引數是在MS-DOS下執行該程式時要用到的。在這些引數的末尾也就是檔案的偏移0x3C(第60位元組)處是是一個4位元組的PE檔案簽名的偏移地址
3.2 NT頭(244或260個位元組)
緊跟著PE檔案簽名之後,是NT頭。NT頭分成3個部分,因為第2部分在32與64位系統裡有區別,第3部分雖然也是頭,但實際很不像“頭”。
第1部分(20個位元組)
偏移 | 大小 | 英文名 | 中文名 | 描述 |
0 | 2 | Machine | 機器數 | 標識CPU的數字。參考3.2.1節“機器型別”。 |
2 | 2 | NumberOfSections | 節數 | 節的數目。Windows載入器限制節的最大數目為96。 |
4 | 4 | TimeDateStamp | 時間/日期標記 | UTC時間1970年1月1日00:00起的總秒數的低32位,它指出檔案何時被建立。 |
8 | 8 | 已經廢除 | ||
16 | 2 | SizeOfOptionalHeader | 可選頭大小 | 第2部分+第3部分的總大小。這個大小在32位和64位檔案中是不同的。對於32位檔案來說,它是224;對於64位檔案來說,它是240。 |
18 | 2 | FillCharacteristics | 檔案特徵值 | 指示檔案屬性的標誌。參考3.2.2節“特徵”。 |
第2部分(96或112個位元組)
偏移 | 大小 | 英文名 | 中文名 | 描述 |
0 | 2 | Magic | 魔數 | 這個無符號整數指出了映象檔案的狀態。 0x10B表明這是一個32位映象檔案。 0x107表明這是一個ROM映象。 0x20B表明這是一個64位映象檔案。 |
2 | 1 | MajorLinkerVersion | 連結器的主版本號 | 連結器的主版本號。 |
3 | 1 | MinorLinkerVersion | 連結器的次版本號 | 連結器的次版本號。 |
4 | 4 | SizeOfCode | 程式碼節大小 | 一般放在“.text”節裡。如果有多個程式碼節的話,它是所有程式碼節的和。必須是FileAlignment的整數倍,是在檔案裡的大小。 |
8 | 4 | SizeOfInitializedData | 已初始化數大小 | 一般放在“.data”節裡。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在檔案裡的大小。 |
12 | 4 | SizeOfUninitializedData | 未初始化數大小 | 一般放在“.bss”節裡。如果有多個這樣的節話,它是所有這些節的和。必須是FileAlignment的整數倍,是在檔案裡的大小。 |
16 | 4 | AddressOfEntryPoint | 入口點 | 當可執行檔案被載入進記憶體時其入口點RVA。對於一般程式映象來說,它就是啟動地址。為0則從ImageBase開始執行。對於dll檔案是可選的。 |
20 | 4 | BaseOfCode | 程式碼基址 | 當映象被載入進記憶體時程式碼節的開頭RVA。必須是SectionAlignment的整數倍。 |
24 | 4 | BaseOfData | 資料基址 | 當映象被載入進記憶體時資料節的開頭RVA。(在64位檔案中此處被併入緊隨其後的ImageBase中。)必須是SectionAlignment的整數倍。 |
28/24 | 4/8 | ImageBase | 映象基址 | 當載入進記憶體時映象的第1個位元組的首選地址。它必須是64K的倍數。DLL預設是10000000H。Windows CE 的EXE預設是00010000H。Windows 系列的EXE預設是00400000H。 |
32 | 4 | SectionAlignment | 記憶體對齊 | 當載入進記憶體時節的對齊值(以位元組計)。它必須≥FileAlignment。預設是相應系統的頁面大小。 |
36 | 4 | FileAlignment | 檔案對齊 | 用來對齊映象檔案的節中的原始資料的對齊因子(以位元組計)。它應該是界於512和64K之間的2的冪(包括這兩個邊界值)。預設是512。如果SectionAlignment小於相應系統的頁面大小,那麼FileAlignment必須與SectionAlignment相等。 |
40 | 2 | MajorOperatingSystemVersion | 主系統的主版本號 | 作業系統的版本號可以從“我的電腦”→“幫助”裡面看到,Windows XP是5.1。5是主版本號,1是次版本號 |
42 | 2 | MinorOperatingSystemVersion | 主系統的次版本號 | |
44 | 2 | MajorImageVersion | 映象的主版本號 | |
46 | 2 | MinorImageVersion | 映象的次版本號 | |
48 | 2 | MajorSubsystemVersion | 子系統的主版本號 | |
50 | 2 | MinorSubsystemVersion | 子系統的次版本號 | |
52 | 2 | Win32VersionValue | 保留,必須為0 | |
56 | 4 | SizeOfImage | 映象大小 | 當映象被載入進記憶體時的大小,包括所有的檔案頭。向上舍入為SectionAlignment的倍數。 |
60 | 4 | SizeOfHeaders | 頭大小 | 所有頭的總大小,向上舍入為FileAlignment的倍數。可以以此值作為PE檔案第一節的檔案偏移量。 |
64 | 4 | CheckSum | 校驗和 | 映象檔案的校驗和。計算校驗和的演算法被合併到了Imagehlp.DLL 中。以下程式在載入時被校驗以確定其是否合法:所有的驅動程式、任何在引導時被載入的DLL以及載入進關鍵Windows程序中的DLL。 |
68 | 2 | Subsystem | 子系統型別 | 執行此映象所需的子系統。參考後面的“Windows子系統”部分。 |
70 | 2 | DllCharacteristics | DLL標識 | 參考後面的“DLL特徵”部分。 |
72 | 4/8 | SizeOfStackReserve | 堆疊保留大小 | 最大棧大小。CPU的堆疊。預設是1MB。 |
76/80 | 4/8 | SizeOfStackCommit | 堆疊提交大小 | 初始提交的堆疊大小。預設是4KB。 |
80/88 | 4/8 | SizeOfHeapReserve | 堆保留大小 | 最大堆大小。編譯器分配的。預設是1MB。 |
84/96 | 4/8 | SizeOfHeapCommit | 堆疊交大小 | 初始提交的區域性堆空間大小。預設是4KB。 |
88/104 | 4 | LoaderFlags | 保留,必須為0 | |
92/108 | 4 | NumberOfRvaAndSizes | 目錄項數目 | 資料目錄項的個數。由於以前發行的Windows NT的原因,它只能為16。 |
第3部分資料目錄(128個位元組)
偏移 |
大小 | 英文名 | 描述 |
96/112 | 8 | Export Table | 匯出表的地址和大小。參考5.1節“.edata” |
104/120 | 8 | Import Table | 匯入目錄表的地址和大小。參考5.2.1節“.idata” |
112/128 | 8 | Resource Table | 資源表的地址和大小。參考5.6節“.rsrc” |
120/136 | 8 | Exception Table | 異常表的地址和大小。參考5.3節“.pdata” |
128/144 | 8 | Certificate Table | 屬性證書表的地址和大小。參考6節“屬性證書表” |
136/152 | 8 | Base Relocation Table | 基址重定位表的地址和大小。參考5.4節“.reloc” |
144/160 | 8 | Debug | 除錯資料起始地址和大小。 |
152/168 | 8 | Architecture | 保留,必須為0 |
160/176 | 8 | Global Ptr | 將被儲存在全域性指標暫存器中的一個值的RVA。這個結構的Size域必須為0 |
168/184 | 8 | TLS Table | 執行緒區域性儲存(TLS)表的地址和大小。 |
176/192 | 8 | Load Config Table | 載入配置表的地址和大小。參考5.5節“載入配置結構” |
184/200 | 8 | Bound Import | 繫結匯入查詢表的地址和大小。參考5.2.2節“匯入查詢表” |
192/208 | 8 | IAT | 匯入地址表的地址和大小。參考5.2.4節“匯入地址表” |
200/216 | 8 | Delay Import Descriptor | 延遲匯入描述符的地址和大小。 |
208/224 | 8 | CLR Runtime Header | CLR執行時頭部的地址和大小。(已廢除) |
216/232 | 8 |
保留,必須為0 |
3.2.1 機器型別
Machine域可以取以下各值中的一個來指定CPU型別。映象檔案僅能運行於指定處理器或者能夠模擬指定處理器的系統上。
值 | 描述 |
0x0 | 適用於任何型別處理器 |
0x1d3 | Matsushita AM33處理器 |
0x8664 | x64處理器 |
0x1c0 | ARM小尾處理器 |
0xebc | EFI位元組碼處理器 |
0x14c | Intel 386或後繼處理器及其相容處理器 |
0x200 | Intel Itanium處理器 |
0x9041 | Mitsubishi M32R小尾處理器 |
0x266 | MIPS16處理器 |
0x366 | 帶FPU的MIPS處理器 |
0x466 | 帶FPU的MIPS16處理器 |
0x1f0 | PowerPC小尾處理器 |
0x1f1 | 帶符點運算支援的PowerPC處理器 |
0x166 | MIPS小尾處理器 |
0x1a2 | Hitachi SH3處理器 |
0x1a3 | Hitachi SH3 DSP處理器 |
0x1a6 | Hitachi SH4處理器 |
0x1a6 | Hitachi SH5處理器 |
0x1c2 | Thumb處理器 |
0x169 | MIPS小尾WCE v2處理器 |
3.2.2 特徵
Characteristics域包含映象檔案屬性的標誌。以下加粗的是常用的屬性。當前定義了以下值(由低位往高位):
位置 | 描述 |
0 | 它表明此檔案不包含基址重定位資訊,因此必須被載入到其首選基地址上。如果基地址不可用,載入器會報錯。 |
1 | 它表明此映象檔案是合法的。看起來有點多此一舉,但又不能少。 |
2 | 保留,必須為0。 |
3 | |
4 | |
5 | 應用程式可以處理大於2GB的地址。 |
6 | 保留,必須為0。 |
7 | |
8 | 機器型別基於32位體系結構。 |
9 | 除錯資訊已經從此映象檔案中移除。 |
10 | 如果此映象檔案在可移動介質上,完全載入它並把它複製到交換檔案中。幾乎不用 |
11 | 如果此映象檔案在網路介質上,完全載入它並把它複製到交換檔案中。幾乎不用 |
12 | 此映象檔案是系統檔案,而不是使用者程式。 |
13 | 此映象檔案是動態連結庫(DLL)。 |
14 | 此檔案只能運行於單處理器機器上。 |
15 | 保留,必須為0。 |
Windows子系統
為NT頭第2部分的Subsystem域定義了以下值以確定執行映象所需的Windows子系統(如果存在):
值 | 描述 |
0 | 未知子系統 |
1 | 裝置驅動程式和Native Windows程序 |
2 | Windows圖形使用者介面(GUI)子系統(一般程式) |
3 | Windows字元模式(CUI)子系統(從命令提示符啟動的) |
7 | Posix字元模式子系統 |
9 | Windows CE |
10 | 可擴充套件韌體介面(EFI)應用程式 |
11 | 帶引導服務的EFI驅動程式 |
12 | 帶執行時服務的EFI驅動程式 |
13 | EFI ROM映象 |
14 | XBOX |
DLL特徵
為NT頭的DllCharacteristics域定義了以下值:
位置 | 描述 |
1 | 保留,必須為0。 |
2 | |
3 | |
4 | |
5 | 官方文件缺失 |
6 | 官方文件缺失 |
7 | DLL可以在載入時被重定位。 |
8 | 強制進行程式碼完整性校驗。 |
9 | 映象兼容於NX。 |
10 | 可以隔離,但並不隔離此映象。 |
11 | 不使用結構化異常(SE)處理。 |
12 | 不繫結映象。 |
13 | 保留,必須為0。 |
14 | WDM驅動程式。 |
15 | 官方文件缺失 |
16 | 可以用於終端伺服器。 |
每個資料目錄給出了Windows使用的表或字串的地址和大小。這些資料目錄項全部被被載入進記憶體以備系統執行時使用。資料目錄是按照如下格式定義的一個8位元組結構:
typedef struct
DWORD VirtualAddress; //資料的RVA
DWORD Size; //資料的大小
typedef ENDS
第1個域——VirtualAddress,實際上是表的RVA。相對映象基址偏移地址。NT頭第2部分的ImageBase
第2個域給出了表的大小(以位元組計)。資料目錄組成了NT頭的最後一部分。
Certificate Table域指向屬性證書表。它的第一個域是一個檔案指標,而不是通常的RVA。
3.3 節頭
在映象檔案中,每個節的RVA值必須由連結器決定。這樣能夠保證這些節位置相鄰且按升序排列,並且這些RVA值必須是NT頭中SectionAlignment域的倍數。
每個節頭(節表項)格式如下,共40個位元組:
偏移 | 大小 | 英文名 | 描述 |
0 | 8 | Name | 這是一個8位元組ASCII編碼的字串,不足8位元組時用NULL填充,必須使其達到8位元組。如果它正好是8位元組,那就沒有最後的NULL字元。可執行映象不支援長度超過8位元組的節名。 |
8 | 4 | VirtualSize | 當載入進記憶體時這個節的總大小。如果此值比SizeOfRawData大,那麼多出的部分用0填充。這是節的資料在沒有進行對齊處理前的實際大小,不需要記憶體對齊。 |
12 | 4 | VirtualAddress | 記憶體中節相對於映象基址的偏移。必須是SectionAlignment的整數倍。 |
16 | 4 | SizeOfRawData | 磁碟檔案中已初始化資料的大小。它必須是NT頭中FileAlignment域的倍數。當節中僅包含未初始化的資料時,這個域應該為0。 |
20 | 4 | PointerToRawData | 節中資料起始的檔案偏移。它必須是NT頭中FileAlignment域的倍數。當節中僅包含未初始化的資料時,這個域應該為0。 |
24 | 4 | PointerToRelocations | 重定位項開頭的檔案指標。對於可執行檔案或沒有重定位項的檔案來說,此值應該為0。 |
28 | 4 | 已經廢除。 | |
32 | 2 | NumberOfRelocations | 節中重定位項的個數。對於可執行檔案或沒有重定位項的檔案來說,此值應該為0。 |
34 | 2 | 已經廢除。 | |
36 | 4 | Characteristics | 描述節特徵的標誌。參考“節標誌”。 |
3.3.1 節標誌
節頭中的Characteristics標誌指出了節的屬性。(以下加粗的是常用的屬性值)
位置 | 描述 |
1 | 已經廢除 |
2 | |
3 | |
4 | |
5 | |
6 | 此節包含可執行程式碼。程式碼段才用“.text” |
7 | 此節包含已初始化的資料。“.data” |
8 | 此節包含未初始化的資料。“.bss” |
9 | 已經廢除 |
10 | |
11 | |
12 | |
13 | |
14 | |
15 | |
16 | 此節包含通過全域性指標(GP)來引用的資料。 |
17 | 已經廢除 |
18 | |
19 | |
20 | |
21 | |
22 | |
23 | |
24 | |
25 | 此節包含擴充套件的重定位資訊。 |
26 | 此節可以在需要時被丟棄。 |
27 | 此節不能被快取。 |
28 | 此節不能被交換到頁面檔案中。 |
29 | 此節可以在記憶體中共享。 |
30 | 此節可以作為程式碼執行。 |
31 | 此節可讀。(幾乎都設定此節) |
32 | 此節可寫。 |
第25標誌表明節中重定位項的個數超出了節頭中為每個節保留的16位所能表示的範圍(也就是65535個函式)。如果設定了此標誌並且節頭中的NumberOfRelocations域的值是0xffff,那麼實際的重定位項個數被儲存在第一個重定位項的VirtualAddress域(32位)中。如果設定了第25標誌但節中的重定位項的個數少於0xffff,則表示出現了錯誤。
4 一些注意資訊
1.PE頭是怎麼計算的?
SizeOfHeaders所指的頭是從檔案的第1個位元組開始算起的,而不是從PE標記開始算起的。快速的計算方法是從檔案的偏移0x3C(第59位元組)處獲得一個4位元組的PE檔案簽名的偏移地址,這個偏移地址就是本文所定義的DOS頭的大小。NT頭在32位系統是244位元組,在64位系統是260位元組。節頭的大小由NT頭的第1部分的NumberOfSections(節的數量)*40位元組(每個節頭是40位元組)得出。如此,DOS頭、NT頭、節頭3個頭的大小加起來並向上舍入為FileAlignment(檔案對齊)的正整數倍的最小值就是SizeOfHeaders(頭大小)值。
2.節數量的問題
Windows讀取NumberOfSections的值然後檢查節表裡的每個結構,如果找到一個全0結構就結束搜尋,否則一直處理完NumberOfSections指定數目的結構。沒有規定節頭必須以全0結構結束。所以載入器使用了雙重標準——全0、達到NumberOfSections數量就不再搜尋了。
3.未初始化問題
①未初始化資料在檔案中是不佔空間的,但在記憶體裡還是會佔空間的,它們依然依據指定的大小存在記憶體裡。所以說未初始化資料只在檔案大小上有優勢,在記憶體裡與已初始化資料是一樣的。
②未初始化資料的方法有2種:1是通過節頭的VirtualSize>SizeOfRawData。未初始化資料的大小就是VirtualSize-SizeOfRawData的值。2是節特徵的標誌置為“此節包含未初始化的資料”,這時SizeOfUninitializedData才會非0。現在 都使用第1種,把它們整合到.data裡面可以加快速度。
4.已初始化問題
資料目錄裡面所對應的塊中除了屬性證書表、除錯資訊和幾個廢除的目錄項外,全都屬於SizeOfInitializedData(已初始化資料大小)範圍。當然,已初始化資料不只這些,還可以是常見的程式碼段等等。
5.節對齊的問題
如果NT頭的SectionAlignment域的值小於相應作業系統(有些資料說是根據CPU來的,這不一定。因為CPU本身就允許改分頁大小,只是大部分時候作業系統是用CPU預設值的。x86平臺預設頁面大小是4K。IA-64平臺預設頁面大小是8K。MIPS平臺預設頁面大小是4K。Itanium平臺預設頁面大小是8K。)平臺的頁面大小,那麼映象檔案有一些附加的限制。對於這種檔案,當映象被載入到記憶體中時,節中資料在檔案中的位置必須與它在記憶體中的位置相等,因此節中資料的物理偏移與RVA相同。
6.映象大小
SizeOfImage所代表的記憶體映象大小沒有包含屬性證書表和除錯資訊,這是因為載入器並不將屬性證書和除錯資訊對映進記憶體。同時載入器規定,屬性證書和除錯資訊必須被放在映象檔案的最後,並且屬性證書表在除錯資訊節之前。
7.資料的組織
CPU的段主要分為4個:程式碼段、資料段、堆疊段、附加段。而作業系統給程式設計師留下只有程式碼段和資料段,堆疊段和附加段就由系統自行處理了,我們不用管。PE檔案的資料組織方式是以BaseOfCode、BaseOfData為基準,以節為主體,以資料目錄為輔助。
①BaseOfCode、BaseOfData是與後面相應的程式碼節、資料節的VirtualAddress一致。(這裡的資料節是狹義的資料節,是特指程式碼段、資料目錄所指定的資料除外的那一部分,也就是我們程式設計時定義的常量、變數、未初始化資料等)
②所有的程式碼、資料都必須在節裡面,否則就算是程式碼基址、資料基址、資料目錄都有指定,而節頭裡沒有指定,載入器也會報錯,不能執行
③匯入函式、匯出函式、資源、重定位表等是為了輔助程式主體的,這些都由系統負責處理
5 特殊的節
下表描述了保留的節以及它們的屬性,後面是對出現在可執行檔案中的節的詳細描述。這些節是微軟的編譯產品所定義的不是系統定義的,實際可以不拘泥於此。
節名 | 內容 |
.bss | 未初始化的資料 |
.data | 程式碼節 |
.edata | 匯出表 |
.idata | 匯入表 |
.idlsym | 包含已註冊的SEH,它們用以支援IDL屬性 |
.pdata | 異常資訊 |
.rdata | 只讀的已初始化資料(用於常量) |
.reloc | 重定位資訊 |
.rsrc | 資源目錄 |
.sbss | 與GP相關的未初始化資料 |
.sdata | 與GP相關的已初始化資料 |
.srdata | 與GP相關的只讀資料 |
.text | 預設程式碼節 |
5.1 .edata節
檔案A的函式K被檔案B呼叫時,函式K就稱為匯出函式。匯出函式通常出現在DLL中,也可以是exe檔案。
下表描述了匯出節的一般結構。
表名 | 描述 |
匯出目錄表 | 它給出了其它各種匯出表的位置和大小。 |
匯出地址表 | 一個由匯出函式的RVA組成的陣列。它們是匯出的函式和資料在程式碼節和資料節內的實際地址。其它映象檔案可以通過使用這個表的索引(序數)來呼叫函式。 |
匯出名稱指標表 | 一個由指向匯出函式名稱的指標組成的陣列,按升序排列。大小寫敏感。 |
匯出序數表 | 一個由對應於匯出名稱指標表中各個成員的序陣列成的陣列。它們的對應是通過位置來體現的,因此匯出名稱指標表與匯出序數表成員數目必須相同。 |
匯出名稱表 | 一系列以NULL結尾的ASCII碼字串。匯出名稱指標表中的成員都指向這個區域。它們都是公用名稱,函式匯入與匯出就是通過它們。 |
當其它映象檔案通過名稱匯入函式時,Win32載入器通過匯出名稱指標表來搜尋匹配的字串。如果找到,它就查詢匯出序數表中相應的成員(也就是說,將找到的匯出名稱指標表的索引作為匯出序數表的索引來使用)來獲取與匯入函式相關聯的序數。獲取的這個序數是匯出地址表的索引,這個索引對應的元素給出了所需函式的實際位置。每個匯出函式都可以通過序數進行訪問。
當其它映象檔案通過序數匯入函式時,就不再需要通過匯出名稱指標表來搜尋匹配的字串。因此直接使用序數效率會更高。但是匯出名稱容易記憶,它不需要使用者記住各個符號在表中的索引。
5.1.1 匯出目錄表
匯出目錄表是匯出函式資訊的開始部分,它描述了匯出函式資訊中其餘部分的內容。
偏移 | 大小 | 英文名 | 描述 |
0 | 4 | Export Flags | 保留,必須為0。 |
4 | 4 | Time/Date StampMajor Version | 匯出函式被建立的日期和時間。這個值與NT頭的第一部分TimeDateStamp相同。 |
8 | 2 | Major Version | 主版本號。 |
10 | 2 | Minor Version | 次版本號。 |
12 | 4 | Name RVA | 包含這個DLL全名的ASCII碼字串RVA。以一個NULL位元組結尾。 |
16 | 4 | Ordinal Base | 匯出函式的起始序數值。它通常被設定為1。 |
20 | 4 | NumberOfFunctions | 匯出函式中所有元素的數目。 |
24 | 4 | NumberOfNames | 匯出名稱指標表中元素的數目。它同時也是匯出序數表中元素的數目。 |
28 | 4 | AddressOfFunctions | 匯出地址表RVA。 |
32 | 4 | AddressOfNames | 匯出名稱指標表RVA。 |
36 | 4 | AddressOfNameOrdinals | 匯出序數表RVA。 |
5.1.2 匯出地址表(Export Address Table,EAT)
匯出地址表的格式為下表所述的兩種格式之一。如果指定的地址不是位於匯出節(其地址和長度由NT頭給出)中,那麼這個域就是一個Export RVA;否則這個域是一個Forwarder RVA,它給出了一個位於其它DLL中的符號的名稱。
偏移 | 大小 | 域 | 描述 |
0 | 4 | Export RVA | 當載入進記憶體時,匯出函式RVA。 |
0 | 4 | Forwarder RVA | 這是指向匯出節中一個以NULL結尾的ASCII碼字串的指標。這個字串必須位於Export Table(匯出表)資料目錄項給出的範圍之內。這個字串給出了匯出函式所在DLL的名稱以及匯出函式的名稱(例如“MYDLL.expfunc”),或者DLL的名稱以及匯出函式的序數值(例如“MYDLL.#27”)。 |
Forwarder RVA匯出了其它映象中定義的函式,使它看起來好像是當前映象匯出的一樣。因此對於當前映象來說,這個符號同時既是匯入函式又是匯出函式。
例如對於Windows XP系統中的Kernel32.dll檔案來說,它匯出的“HeapAlloc”被轉發到“NTDLL.RtlAllocateHeap”。這樣就允許應用程式使用Windows XP系統中的Ntdll.dll模組而不需要實際包含任何相關的匯入資訊。應用程式的匯入表只與Kernel32.dll有關。
匯出地址表的的值有時為0,此時表明這裡沒有匯出函式。這是為了能與以前版本相容,省去修改的麻煩。
5.1.3 匯出名稱指標表
匯出名稱指標表是由匯出名稱表中的字串的地址(RVA)組成的陣列。二進位制進行排序的,以便於搜尋。
只有當匯出名稱指標表中包含指向某個匯出名稱的指標時,這個匯出名稱才算被定義。換句話說,匯出名稱指標表的值有可能為0,這是為了能與前面版本相容。
5.1.4 匯出序數表
匯出序數表是由匯出地址表的索引組成的一個數組,每個序數長16位。必須從序數值中減去Ordinal Base域的值得到的才是匯出地址表真正的索引。注意,匯出地址表真正的索引真正的索引是從0開始的。由此可見,微軟弄出Ordinal Base是找麻煩的。匯出序數表的值和匯出地址表的索引的值都是無符號數。
匯出名稱指標表和匯出名稱序數表是兩個並列的陣列,將它們分開是為了使它們可以分別按照各自的邊界(前者是4個位元組,後者是2個位元組)對齊。在進行操作時,由匯出名稱指標這一列給出匯出函式的名稱,而由匯出序數這一列給出這個匯出函式對應的序數。匯出名稱指標表的成員和匯出序數表的成員通過同一個索引相關聯。
5.1.5 匯出名稱表(Export Name Table,ENT)
匯出名稱表的結構就是長度可變的一系列以NULL結尾的ASCII碼字串。 匯出名稱表包含的是匯出名稱指標表實際指向的字串。這個表的RVA是由匯出名稱指標表的第1個值來確定的。這個表中的字串都是函式名稱,其它檔案可以通過它們呼叫函。
5.1.6 舉例
①用序數呼叫
當可執行檔案用序數呼叫函式時,該序數就是匯出函式地址表的真實索引。如果索引是錯誤的就有可能出現不可預知的錯誤。最著名的例子就是Windows XP在升級Server 2補丁之後,有很多程式都不能執行就是這個原因。微軟用序數這種方法被大多數危險程式(病毒、木馬)所引用,同樣的微軟自己也用這種方法來使用一些隱含的函式。最後受害者還是廣大的使用者,因為使用序數方法的絕大部分程式是有著不可告人的目的的。
②用函式名呼叫
當可執行檔案用函式名呼叫時,載入器會通過AddressOfNames以2進位制的方法找到第一個相同的函式名。假如找到的是第X個函式名,則在AddressOfNameOrdinals中取出第X個值,該值再減去Ordinal Base則為函式地址的真實索引。
5.2.idata節
首先,您得了解什麼是匯入函式。一個匯入函式是被某模組呼叫的但又不在呼叫者模組中的函式,因而命名為“import(匯入)”。匯入函式實際位於一個或者更多的DLL裡。呼叫者模組裡只保留一些函式資訊,包括函式名及其駐留的DLL名。現在,我們怎樣才能找到PE檔案中儲存的資訊呢? 轉到 data directory 尋求答案吧。
檔案中匯入資訊的典型佈局如下:
典型的匯入節佈局
5.2.1 匯入目錄表
匯入目錄表是由匯入目錄項組成的陣列,每個匯入目錄項對應著一個匯入的DLL。最後一個匯入目錄項是空的(全部域的值都為NULL),用來指明目錄表的結尾。
每個匯入目錄項的格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Import Lookup Table RVA | 匯入查詢表的RVA。這個表包含了每一個匯入函式的名稱或序數。 |
4 | 4 | Time/Date Stamp | 當映象與相應的DLL繫結之後,這個域被設定為這個DLL的日期/時間戳。 |
8 | 4 | Forwarder Chain | 第一個轉發項的索引。 |
12 | 4 | Name RVA | 包含DLL名稱的ASCII碼字串RVA。 |
16 | 4 | Import Address RVA | 匯入地址表的RVA。這個表的內容與匯入查詢表的內容完全一樣。 |
5.2.2 匯入查詢表
匯入查詢表是由長度為32位(PE32)或64位(PE32+)的數字組成的陣列。其中的每一個元素都是位域,其格式如下表所示。在這種格式中,位31(PE32)或位63(PE32+)是最高位。這些項描述了從給定的DLL匯入的所有函式。最後一個項被設定為0(NULL),用來指明表的結尾。
偏移 | 大小 | 位域 | 描述 |
31/63 | 1 | Ordinal/Name Flag | 如果這個位為1,說明是通過序數匯入的。否則是通過名稱匯入的。測試這個位的掩碼為0x80000000(PE32)或)0x8000000000000000(PE32+)。 |
15-0 | 16 | Ordinal Number | 序數值(16位長)。只有當Ordinal/Name Flag域為1(即通過序數匯入)時才使用這個域。位30-15(PE32)或62-15(PE32+)必須為0。 |
30-0 | 31 | Hint/Name Table RVA | 提示/名稱表項的RVA(31位長)。只有當Ordinal/Name Flag域為0(即通過名稱匯入)時才使用這個域。對於PE32+來說,位62-31必須為0。 |
5.2.3 提示/名稱表
提示/名稱表中的每一個元素結構如下:
偏移 | 大小 | 域 | 描述 |
0 | 2 | Hint | 指出名稱指標表的索引。當搜尋匹配字串時首選使用這個值。如果匹配失敗,再在DLL的匯出名稱指標表中進行2進位制搜尋。 |
2 | 可變 | Name | 包含匯入函式名稱的ASCII碼字串。這個字串必須與DLL匯出的函式名稱匹配。同時這個字串區分大小寫並且以NULL結尾。 |
* | 0或1 | Pad | 為了讓提示/名稱表的下一個元素出現在偶數地址,這裡可能需要填充0個或1個NULL位元組。 |
5.2.4 匯入地址表
匯入地址表的結構和內容與匯入查詢表完全一樣,直到檔案被繫結。在繫結過程中,用匯入函式的32位(PE32)或64位(PE32+)地址覆蓋匯入地址表中的相應項。這些地址是匯入函式的實際記憶體地址,儘管技術上仍把它們稱為“虛擬地址”。載入器通常會處理繫結。
5.3 .pdata節(可有可無,誰也不希望自己的函數出問題的吧!)
.pdata節是由用於異常處理的函式表項組成的陣列。NT頭中的Exception Table(異常表)域指向它。在將它們放進最終的映象檔案之前,這些項必須按函式地址(下列每個結構的第一個域)排序。下面描述了函式表項的3種格式,使用哪一種取決於目標平臺。
對於32位的MIPS映象來說,其函式表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函式的VA |
4 | 4 | End Address | 函式結尾的VA |
8 | 4 | Exception Handler | 指向要執行的異常處理程式的指標 |
12 | 4 | Handler Data | 指向要傳遞給異常處理程式的附加資料的指標 |
16 | 4 | Prolog End Address | 函式prolog程式碼結尾的VA |
對於ARM、PowerPC、SH3和SH4 Windows CE平臺來說,其函式表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函式的VA |
4 | 8位 | Prolog Length | 函式prolog程式碼包含的指令數 |
4 | 22位 | Function Length | 函式程式碼包含的指令數 |
4 | 1位 | 32-bit Flag | 如果此位為1,表明函式由32位指令組成。否則,函式由16位指令組成。 |
4 | 1位 | Exception Flag | 如果此位為1,表明存在用於此函式的異常處理程式;否則,不存在異常處理程式。 |
對於x64和Itanium平臺來說,其函式表項格式如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Begin Address | 相應函式的RVA |
4 | 4 | End Address | 函式結尾的RVA |
8 | 4 | Unwind Information | 用於異常處理的展開(Unwind)資訊的RVA |
5.4 .reloc節
基址重定位表包含了映象中所有需要重定位的內容。NT頭中的資料目錄中的Base Relocation Table(基址重定位表)域給出了基址重定位表所佔的位元組數。基址重定位表被劃分成許多塊,每一塊表示一個4K頁面範圍內的基址重定位資訊,它必須從32位邊界開始。
5.4.1 基址重定位塊
每個基址重定位塊的開頭都是如下結構:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Page RVA | 將映象基址與這個域(頁面RVA)的和加到每個偏移地址處最終形成一個VA,這個VA就是要進行基址重定位的地方。 |
4 | 4 | Block Size | 基址重定位塊所佔的總位元組數,其中包括Page RVA域和Block Size域以及跟在它們後面的Type/Offset域。 |
Block Size域後面跟著數目不定的Type/Offset位域。它們中的每一個都是一個WORD(2位元組),其結構如下:
偏移 | 大小 | 域 | 描述 |
0 | 4位 | Type | 它佔這個WORD的最高4位,這個值指出需要應用的基址重定位型別。參考5.4.2節“基址重定位型別”。 |
0 | 12位 | Offset | 它佔這個WORD的其餘12位,這個值是從基址重定位塊的Page RVA域指定的地址處開始的偏移。這個偏移指出需要進行基址重定位的位置。 |
為了進行基址重定位,需要計算映象的首選基地址與實際被載入到的基地址之差。如果映象本身就被載入到了其首選基地址,那麼這個差為零,因此也就不需要進行基址重定位了。
5.4.2 基址重定位型別
值 | 描述 |
0 | 基址重定位被忽略。這種型別可以用來對其它塊進行填充。 |
1 | 基址重定位時將差值的高16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的高半部分。 |
2 | 基址重定位時將差值的低16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的低半部分。 |
3 | 基址重定位時將所有的32位差值加到指定偏移處的一個32位域上。 |
4 | 進行基址重定位時將差值的高16位加到指定偏移處的一個16位域上。這個16位域是一個32位字的高半部分,而這個32位字的低半部分被儲存在緊跟在這個Type/Offset位域後面的一個16位字中。也就是說,這一個基址重定位項佔了兩個Type/Offset位域的位置。 |
5 | 對MIPS平臺的跳轉指令進行基址重定位。 |
6 | 保留,必須為0 |
7 | 保留,必須為0 |
9 | 對MIPS16平臺的跳轉指令進行基址重定位。 |
10 | 進行基址重定位時將差值加到指定偏移處的一。 |
5.5 載入配置結構(不清楚,大概又是多餘的吧)
載入配置結構最初用於Windows NT作業系統自身幾種非常有限的場合——在映象檔案頭或NT頭中描述各種特性太困難或這些資訊尺寸太大。當前版本的Microsoft連結器和Windows XP以及後續版本的Windows使用的是這個結構的新版本,將之用於包含保留的SEH技術的基於x86的32位系統上。它提供了一個安全的結構化異常處理程式列表,作業系統在進行異常派送時要用到這些異常處理程式。如果異常處理程式的地址在映象的VA範圍之內,並且映象被標記為支援保留的SEH,那麼這個異常處理程式必須在映象的已知安全異常處理程式列表中,否則作業系統將終止這個應用程式。這是為了防止利用“x86異常處理程式劫持”來控制作業系統,它在以前已經被利用過。
Microsoft的連結器自動提供一個預設的載入配置結構來包含保留的SEH資料。如果使用者的程式碼已經提供了一個載入配置結構,那麼它必須包含新新增的保留的SEH域。否則,連結器將不能包含保留的SEH資料,這樣映象檔案就不能被標記為包含保留的SEH。
5.5.1 載入配置目錄
對應於預保留的SEH載入配置結構的資料目錄項必須為載入配置結構指定一個特別的大小,因為作業系統載入器總是希望它為這樣一個特定值。事實上,這個大小隻是用於檢查這個結構的版本。為了與Windows XP以及以前版本的Windows相容,x86映象檔案中這個結構的大小必須為64。
5.5.2 載入配置結構佈局
用於32位和64位PE檔案的載入配置結構佈局如下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Characteristics | 指示檔案屬性的標誌,當前未用。 |
4 | 4 | TimeDateStamp | 日期/時間戳。這個值表示從UTC時間1970年1月1日午夜(00:00:00)以來經過的總秒數,它是根據系統時鐘算出的。可以用C執行時函式time來獲取這個時間戳。 |
8 | 2 | MajorVersion | 主版本號 |
10 | 2 | MinorVersion | 次版本號 |
12 | 4 | GlobalFlagsClear | 當載入器啟動程序時,需要被清除的全域性載入器標誌。 |
16 | 4 | GlobalFlagsSet | 當載入器啟動程序時,需要被設定的全域性載入器標誌。 |
20 | 4 | CriticalSectionDefaultTimeout | 用於這個程序處於無約束狀態的臨界區的預設超時值。 |
24 | 8 | DeCommitFreeBlockThreshold | 返回到系統之前必須釋放的記憶體數量(以位元組計)。 |
32 | 8 | DeCommitTotalFreeThreshold | 空閒記憶體總量(以位元組計)。 |
40 | 8 | LockPrefixTable | [僅適用於x86平臺]這是一個地址列表的VA。這個地址列表中儲存的是使用LOCK字首的指令的地址,這樣便於在單處理器機器上將這些LOCK字首替換為NOP指令。 |
48 | 8 | MaximumAllocationSize | 最大的分配粒度(以位元組計)。 |
56 | 8 | VirtualMemoryThreshold | 最大的虛擬記憶體大小(以位元組計)。 |
64 | 8 | ProcessAffinityMask | 將這個域設定為非零值等效於在程序啟動時將這個設定的值作為引數去呼叫SetProcessAffinityMask函式(僅適用於.exe檔案)。 |
72 | 4 | ProcessHeapFlags | 程序堆的標誌,相當於函式的第一個引數。這些標誌用於在程序啟動過程中建立的堆。 |
76 | 2 | CSDVersion | Service Pack版本標識。 |
78 | 2 | Reserved | 必須為0 |
80 | 8 | EditList | 保留,供系統使用。 |
60/88 | 4/8 | SecurityCookie | 指向cookie的指標。cookie由Visual C++編譯器的GS實現所使用。 |
64/96 | 4/8 | SEHandlerTable | [僅適用於x86平臺]這是一個地址列表的VA。這個地址列表中儲存的是映象中每個合法的、獨一無二的SE處理程式的RVA,並且它們已經按RVA排序。 |
68/104 | 4/8 | SEHandlerCount | [僅適用於x86平臺]表中獨一無二的SE處理程式的數目。 |
5.6 .rsrc節
資源節可以看成是一個磁碟的分割槽,碟符是資源目錄表,下面有3層目錄(資源目錄項),最後是檔案(資源資料)。
①資源目錄表是一個16位元組組成的結構。其第一個位元組又稱為“根節點”。其前的12位元組雖然有定義,但載入器並不理會,所以任何值都可以。
②第1層目錄(資源目錄項)是資源型別,微軟已經定義了21種。其結構是一個16位元組的陣列。資源目錄項分為名稱項和ID項,這取決於資源目錄表。資源目錄表指出跟著它的名稱項和ID項各有多少個(表中所有的名稱項在所有的ID項前面)。表中的所有項按升序排列:名稱項是按不區分大小寫的字串,而ID項則是按數值。第0-3位元組表示資源型別的名稱字串的地址或是32位整數,第4-7位元組表示第二層目錄(資源目錄項)相對於根節點的偏移。
一系列資源目錄表按如下方式與各層相聯絡:每個目錄表後面跟著一系列目錄項,它們給出那個層(型別、名稱或語言)的名稱或標識(ID)及其資料描述或另一個目錄表的地址。如果這個地址指向一個數據描述,那麼那個資料就是這棵樹的葉子。如果這個地址指向另一個目錄表,那麼那個目錄表列出了下一層的目錄項。
一個葉子的型別、名稱和語言ID由從目錄表到這個葉子的路徑決定。第1個表決定型別ID,第2個表(由第一個表中的目錄項指向)決定名稱ID,第3個表決定語言ID。
.rsrc節的一般