下表描述了貫穿於本文中的一些概念:php
名稱 | 描述 |
地址 | 是「虛擬地址」而不是「物理地址」。爲何不是「物理地址」呢?由於數據在內存的位置常常在變,這樣能夠節省內存開支、避開錯誤的內存位置等的優點。同時用戶並不須要知道具體的「真實地址」,由於系統本身會爲程序準備好內存空間的(只要內存足夠大) |
鏡像文件 | 包含以EXE文件爲表明的「可執行文件」、以DLL文件爲表明的「動態連接庫」。爲何用「鏡像」?這是由於他們經常被直接「複製」到內存,有「鏡像」的某種意思。看來西方人挺有想象力的哦^0^ |
RVA | 英文全稱Relatively Virtual Address。偏移(又稱「相對虛擬地址」)。相對鏡像基址的偏移。 |
節 | 節是PE文件中代碼或數據的基本單元。原則上講,節只分爲「代碼節」和「數據節」。 |
VA | 英文全稱Virtual Address。基址 |
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。web
圖1 解釋了Microsoft PE可執行文件格式:算法
PE文件整體上分爲「頭」和「節」。「頭」是「節」的描述、簡化、說明,「節」是「頭」的具體化。編程
PE文件的頭分爲DOS頭、NT頭、節頭。注意,這是本人的分法,在此以前並無這種分法。這樣分法會更加合理,更易理解。由於這三個部分正好構成SizeOfHeaders所指的範圍,因此將它們合爲「頭」。這裏的3個頭與別的文章的頭的定義會有所區別。小程序
節頭緊跟在NT頭後面。數組
用記事本打開任何一個鏡像文件,其頭2個字節必爲字符串「MZ」,這是Mark Zbikowski的姓名縮寫,他是最初的MS-DOS設計者之一。而後是一些在MS-DOS下的一些參數,這些參數是在MS-DOS下運行該程序時要用到的。在這些參數的末尾也就是文件的偏移0x3C(第60字節)處是是一個4字節的PE文件簽名的偏移地址。該地址有一個專用名稱叫作「E_lfanew」。這個簽名是「PE00」(字母「P」和「E」後跟着兩個空字節)。緊跟着E_lfanew的是一個MS-DOS程序。那是一個運行於MS-DOS下的合法應用程序。當可執行文件(通常指exe、com文件)運行於MS-DOS下時,這個程序顯示「This program cannot be run in DOS mode(此程序不能在DOS模式下運行)」這條消息。用戶也能夠本身更改該程序,有些還原軟件就是這麼幹的。同時,有些程序既能運行於DOS又能運行於Windows下就是這個緣由。Notepad.exe整個DOS頭大小爲224個字節,大部分不能在DOS下運行的Win32文件都是這個值。MS-DOS程序是無關緊要的,若是你想使文件大小盡量的小能夠省掉MS-DOS程序,同時把前面的參數都清0。緩存
緊跟着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 |
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處理器 |
Characteristics域包含鏡像文件屬性的標誌。如下加粗的是經常使用的屬性。當前定義瞭如下值(由低位往高位):
位置 | 描述 |
0 | 它代表此文件不包含基址重定位信息,所以必須被加載到其首選基地址上。若是基地址不可用,加載器會報錯。 |
1 | 它代表此鏡像文件是合法的。看起來有點畫蛇添足,但又不能少。 |
2 | 保留,必須爲0。 |
3 | |
4 | |
5 | 應用程序能夠處理大於2GB的地址。 |
6 | 保留,必須爲0。 |
7 | |
8 | 機器類型基於32位體系結構。 |
9 | 調試信息已經今後鏡像文件中移除。 |
10 | 若是此鏡像文件在可移動介質上,徹底加載它並把它複製到交換文件中。幾乎不用 |
11 | 若是此鏡像文件在網絡介質上,徹底加載它並把它複製到交換文件中。幾乎不用 |
12 | 此鏡像文件是系統文件,而不是用戶程序。 |
13 | 此鏡像文件是動態連接庫(DLL)。 |
14 | 此文件只能運行於單處理器機器上。 |
15 | 保留,必須爲0。 |
爲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 |
爲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。
在鏡像文件中,每一個節的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 | 描述節特徵的標誌。參考「節標誌」。 |
節頭中的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,則表示出現了錯誤。
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一致。(這裏的數據節是狹義的數據節,是特指代碼段、數據目錄所指定的數據除外的那一部分,也就是咱們編程時定義的常量、變量、未初始化數據等)
②全部的代碼、數據都必須在節裏面,不然就算是代碼基址、數據基址、數據目錄都有指定,而節頭裏沒有指定,加載器也會報錯,不能運行
③導入函數、導出函數、資源、重定位表等是爲了輔助程序主體的,這些都由系統負責處理
下表描述了保留的節以及它們的屬性,後面是對出如今可執行文件中的節的詳細描述。這些節是微軟的編譯產品所定義的不是系統定義的,實際能夠不拘泥於此。
節名 | 內容 |
.bss | 未初始化的數據 |
.data | 代碼節 |
.edata | 導出表 |
.idata | 導入表 |
.idlsym | 包含已註冊的SEH,它們用以支持IDL屬性 |
.pdata | 異常信息 |
.rdata | 只讀的已初始化數據(用於常量) |
.reloc | 重定位信息 |
.rsrc | 資源目錄 |
.sbss | 與GP相關的未初始化數據 |
.sdata | 與GP相關的已初始化數據 |
.srdata | 與GP相關的只讀數據 |
.text | 默認代碼節 |
文件A的函數K被文件B調用時,函數K就稱爲導出函數。導出函數一般出如今DLL中,也能夠是exe文件。
下表描述了導出節的通常結構。
表名 | 描述 |
導出目錄表 | 它給出了其它各類導出表的位置和大小。 |
導出地址表 | 一個由導出函數的RVA組成的數組。它們是導出的函數和數據在代碼節和數據節內的實際地址。其它鏡像文件能夠經過使用這個表的索引(序數)來調用函數。 |
導出名稱指針表 | 一個由指向導出函數名稱的指針組成的數組,按升序排列。大小寫敏感。 |
導出序數表 | 一個由對應於導出名稱指針表中各個成員的序數組成的數組。它們的對應是經過位置來體現的,所以導出名稱指針表與導出序數表成員數目必須相同。 |
導出名稱表 | 一系列以NULL結尾的ASCII碼字符串。導出名稱指針表中的成員都指向這個區域。它們都是公用名稱,函數導入與導出就是經過它們。 |
當其它鏡像文件經過名稱導入函數時,Win32加載器經過導出名稱指針表來搜索匹配的字符串。若是找到,它就查找導出序數表中相應的成員(也就是說,將找到的導出名稱指針表的索引做爲導出序數表的索引來使用)來獲取與導入函數相關聯的序數。獲取的這個序數是導出地址表的索引,這個索引對應的元素給出了所需函數的實際位置。每一個導出函數均可以經過序數進行訪問。
當其它鏡像文件經過序數導入函數時,就再也不須要經過導出名稱指針表來搜索匹配的字符串。所以直接使用序數效率會更高。可是導出名稱容易記憶,它不須要用戶記住各個符號在表中的索引。
導出目錄表是導出函數信息的開始部分,它描述了導出函數信息中其他部分的內容。
偏移 | 大小 | 英文名 | 描述 |
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。 |
導出地址表的格式爲下表所述的兩種格式之一。若是指定的地址不是位於導出節(其地址和長度由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,此時代表這裏沒有導出函數。這是爲了能與之前版本兼容,省去修改的麻煩。
導出名稱指針表是由導出名稱表中的字符串的地址(RVA)組成的數組。二進制進行排序的,以便於搜索。
只有當導出名稱指針表中包含指向某個導出名稱的指針時,這個導出名稱纔算被定義。換句話說,導出名稱指針表的值有可能爲0,這是爲了能與前面版本兼容。
導出序數表是由導出地址表的索引組成的一個數組,每一個序數長16位。必須從序數值中減去Ordinal Base域的值獲得的纔是導出地址表真正的索引。注意,導出地址表真正的索引真正的索引是從0開始的。因而可知,微軟弄出Ordinal Base是找麻煩的。導出序數表的值和導出地址表的索引的值都是無符號數。
導出名稱指針表和導出名稱序數表是兩個並列的數組,將它們分開是爲了使它們能夠分別按照各自的邊界(前者是4個字節,後者是2個字節)對齊。在進行操做時,由導出名稱指針這一列給出導出函數的名稱,而由導出序數這一列給出這個導出函數對應的序數。導出名稱指針表的成員和導出序數表的成員經過同一個索引相關聯。
導出名稱表的結構就是長度可變的一系列以NULL結尾的ASCII碼字符串。 導出名稱表包含的是導出名稱指針表實際指向的字符串。這個表的RVA是由導出名稱指針表的第1個值來肯定的。這個表中的字符串都是函數名稱,其它文件能夠經過它們調用函。
①用序數調用
當可執行文件用序數調用函數時,該序數就是導出函數地址表的真實索引。若是索引是錯誤的就有可能出現不可預知的錯誤。最著名的例子就是Windows XP在升級Server 2補丁以後,有不少程序都不能運行就是這個緣由。微軟用序數這種方法被大多數危險程序(病毒、木馬)所引用,一樣的微軟本身也用這種方法來使用一些隱含的函數。最後受害者仍是廣大的用戶,由於使用序數方法的絕大部分程序是有着不可告人的目的的。
②用函數名調用
當可執行文件用函數名調用時,加載器會經過AddressOfNames以2進制的方法找到第一個相同的函數名。假如找到的是第X個函數名,則在AddressOfNameOrdinals中取出第X個值,該值再減去Ordinal Base則爲函數地址的真實索引。
首先,您得了解什麼是導入函數。一個導入函數是被某模塊調用的但又不在調用者模塊中的函數,於是命名爲「import(導入)」。導入函數實際位於一個或者更多的DLL裏。調用者模塊裏只保留一些函數信息,包括函數名及其駐留的DLL名。如今,咱們怎樣才能找到PE文件中保存的信息呢? 轉到 data directory 尋求答案吧。
文件中導入信息的典型佈局以下:
典型的導入節佈局
導入目錄表是由導入目錄項組成的數組,每一個導入目錄項對應着一個導入的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。這個表的內容與導入查找表的內容徹底同樣。 |
導入查找表是由長度爲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。 |
提示/名稱表中的每個元素結構以下:
偏移 | 大小 | 域 | 描述 |
0 | 2 | Hint | 指出名稱指針表的索引。當搜索匹配字符串時首選使用這個值。若是匹配失敗,再在DLL的導出名稱指針表中進行2進制搜索。 |
2 | 可變 | Name | 包含導入函數名稱的ASCII碼字符串。這個字符串必須與DLL導出的函數名稱匹配。同時這個字符串區分大小寫而且以NULL結尾。 |
* | 0或1 | Pad | 爲了讓提示/名稱表的下一個元素出如今偶數地址,這裏可能須要填充0個或1個NULL字節。 |
導入地址表的結構和內容與導入查找表徹底同樣,直到文件被綁定。在綁定過程當中,用導入函數的32位(PE32)或64位(PE32+)地址覆蓋導入地址表中的相應項。這些地址是導入函數的實際內存地址,儘管技術上仍把它們稱爲「虛擬地址」。加載器一般會處理綁定。
.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 |
基址重定位表包含了鏡像中全部須要重定位的內容。NT頭中的數據目錄中的Base Relocation Table(基址重定位表)域給出了基址重定位表所佔的字節數。基址重定位表被劃分紅許多塊,每一塊表示一個4K頁面範圍內的基址重定位信息,它必須從32位邊界開始。
每一個基址重定位塊的開頭都是以下結構:
偏移 | 大小 | 域 | 描述 |
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域指定的地址處開始的偏移。這個偏移指出須要進行基址重定位的位置。 |
爲了進行基址重定位,須要計算鏡像的首選基地址與實際被加載到的基地址之差。若是鏡像自己就被加載到了其首選基地址,那麼這個差爲零,所以也就不須要進行基址重定位了。
值 | 描述 |
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 | 進行基址重定位時將差值加到指定偏移處的一。 |
加載配置結構最初用於Windows NT操做系統自身幾種很是有限的場合——在鏡像文件頭或NT頭中描述各類特性太困難或這些信息尺寸太大。當前版本的Microsoft連接器和Windows XP以及後續版本的Windows使用的是這個結構的新版本,將之用於包含保留的SEH技術的基於x86的32位系統上。它提供了一個安全的結構化異常處理程序列表,操做系統在進行異常派送時要用到這些異常處理程序。若是異常處理程序的地址在鏡像的VA範圍以內,而且鏡像被標記爲支持保留的SEH,那麼這個異常處理程序必須在鏡像的已知安全異常處理程序列表中,不然操做系統將終止這個應用程序。這是爲了防止利用「x86異常處理程序劫持」來控制操做系統,它在之前已經被利用過。
Microsoft的連接器自動提供一個默認的加載配置結構來包含保留的SEH數據。若是用戶的代碼已經提供了一個加載配置結構,那麼它必須包含新添加的保留的SEH域。不然,連接器將不能包含保留的SEH數據,這樣鏡像文件就不能被標記爲包含保留的SEH。
對應於預保留的SEH加載配置結構的數據目錄項必須爲加載配置結構指定一個特別的大小,由於操做系統加載器老是但願它爲這樣一個特定值。事實上,這個大小隻是用於檢查這個結構的版本。爲了與Windows XP以及之前版本的Windows兼容,x86鏡像文件中這個結構的大小必須爲64。
用於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處理程序的數目。 |
資源節能夠當作是一個磁盤的分區,盤符是資源目錄表,下面有3層目錄(資源目錄項),最後是文件(資源數據)。
①資源目錄表是一個16字節組成的結構。其第一個字節又稱爲「根節點」。其前的12字節雖然有定義,但加載器並不理會,因此任何值均可以。
②第1層目錄(資源目錄項)是資源類型,微軟已經定義了21種。其結構是一個16字節的數組。資源目錄項分爲名稱項和ID項,這取決於資源目錄表。資源目錄表指出跟着它的名稱項和ID項各有多少個(表中全部的名稱項在全部的ID項前面)。表中的全部項按升序排列:名稱項是按不區分大小寫的字符串,而ID項則是按數值。第0-3字節表示資源類型的名稱字符串的地址或是32位整數,第4-7字節表示第二層目錄(資源目錄項)相對於根節點的偏移。
一系列資源目錄表按以下方式與各層相聯繫:每一個目錄表後面跟着一系列目錄項,它們給出那個層(類型、名稱或語言)的名稱或標識(ID)及其數據描述或另外一個目錄表的地址。若是這個地址指向一個數據描述,那麼那個數據就是這棵樹的葉子。若是這個地址指向另外一個目錄表,那麼那個目錄表列出了下一層的目錄項。
一個葉子的類型、名稱和語言ID由從目錄表到這個葉子的路徑決定。第1個表決定類型ID,第2個表(由第一個表中的目錄項指向)決定名稱ID,第3個表決定語言ID。
.rsrc節的通常結構以下:
數據 | 描述 |
資源目錄表 | 全部的頂層(類型)結點都被列於第1個表中。這個表中的項指向第2層表。每一個第2層樹的類型ID相同可是名稱ID不一樣。第3層樹的類型ID和名稱ID都相同但語言ID不一樣。每一個單個的表後面緊跟着目錄項,每一項都有一個名稱或數字標識和一個指向數據描述或下一層表的指針。 |
資源目錄項 | |
資源目錄字符串 | 按2字節邊界對齊的Unicode字符串,它是做爲由資源目錄項指向的字符串數據來使用的。 |
資源數據描述 | 一個由記錄組成的數組,由表指向它,描述了資源數據的實際大小和位置。這些記錄是資源描述樹中的葉子。 |
資源數據 | 資源節的原始數據。資源據描述域中的大小和位置信息將資源數據分紅單個的區域。 |
偏移 | 大小 | 域 | 描述 |
0 | 4 | Characteristics | 資源標誌。保留供未來使用。當前它被設置爲0。 |
4 | 4 | Time/Date Stamp | 資源數據被資源編譯器建立的時間。 |
8 | 2 | Major Version | 主版本號,由用戶設定。 |
10 | 2 | Minor Version | 次版本號,由用戶設定。 |
12 | 2 | Number of Name Entries | 緊跟着這個表頭的目錄項的個數,這些目錄項使用名稱字符串來標識類型、名稱或語言項。 |
14 | 2 | Number of ID Entries | 緊跟着這個表頭的目錄項的個數,這些目錄項使用數字來標識類型、名稱或語言項。 |
具體的狀況是資源目錄表後面緊跟着以名稱項和ID項所組成的數組。資源目錄表與資源目錄項之間不能有空隙。名稱項組成的數組在ID項組成的數組前面,且兩個數組不能有空隙。
偏移 | 大小 | 域 | 描述 |
0 | 4 | Name RVA | 表示類型、名稱或語言ID項的名稱字符串的地址。 |
0 | 4 | Integer ID | 表示類型、名稱或語言ID項的32位整數。 |
4 | 4 | Data Entry RVA | 最高位爲0。低31位是資源數據項的地址。 |
4 | 4 | Subdirectory RVA | 最高位爲1。低31位是另外一個資源目錄表(下一層)的地址。 |
資源目錄字符串區由按字邊界對齊的Unicode字符串組成。這些字符串被存儲在最後一個資源目錄項以後、第一個資源數據項以前。這樣可以使這些長度可變的字符串對長度固定的目錄項的對齊狀況影響最小。每一個資源目錄字符串格式以下:
偏移 | 大小 | 域 | 描述 |
0 | 2 | Length | 字符串的長度,不包括Length域自己。 |
2 | 可變 | Unicode String | 可變 Unicode String 按字邊界對齊的可變長度的Unicode字符串。 |
每一個資源數據項描述了資源數據區中一個實際單元的原始數據。資源數據項格式以下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Data RVA | 資源數據區中一個單元的資源數據的地址。 |
4 | 4 | Size | 由Data RVA域指向的資源數據的大小(以字節計)。 |
8 | 4 | Codepage | 用於解碼資源數據中的代碼點值的代碼頁。一般這個代碼頁應該是Unicode代碼頁。 |
12 | 4 | 保留,必須爲0 | 保留,必須爲0 |
能夠給鏡像文件添加屬性證書表使它與屬性證書相關聯。有多種不一樣類型的屬性證書,最經常使用的是Authenticode簽名。
屬性證書表包含一個或多個長度固定的表項,能夠經過NT頭中的數據目錄中的Certificate Table(證書表)域找到它們。這個表的每一個表項給出了相應證書的開始位置和長度。存儲在這個節中的每一個證書都有一個相應的證書表項。證書表項的數目能夠經過將證書表的大小除以證書表中每一項的大小(8)獲得。注意證書表的大小僅包括它的表項,並不包括這些表項實際指向的證書。
每一個表項格式以下:
偏移 | 大小 | 域 | 描述 |
0 | 4 | Certificate Data | 指向證書實際數據的文件指針。它指向的地址老是按8字節倍數邊界(即最低3個位都是0)對齊。 |
0 | 4 | Size of Certificate | 這是一個無符號整數,它指出證書的大小(以字節計)。 |
注意證書老是從8進制字(從任意字節邊界開始的16個連續字節)邊界開始。若是一個證書的長度不是8進制字長度的偶數倍,那麼就一直用0填充到下一個八進制字邊界。可是證書長度並不包括這些填充的0。所以任何處理證書的軟件必須向上舍入到下一個8進制字才能找到另外一個證書。
證書的起始位置和長度由證書表中相應的表項給出。每一個證書都有唯一一個與它對應的表項。
來源:看雪論壇http://bbs.pediy.com/showthread.php?t=145912