PE文件結構現(xiàn)在讓我們開始挖掘PE文件的實際格式吧,。我要從文件的開頭處開始,描述存在于所有PE文件中的數(shù)據(jù)結構,。然后我會描述PE文件的節(jié)中具體的數(shù)據(jù)結構(例如導入表與資源),。下面我要討論的所有數(shù)據(jù)結構都被定義在WINNT.H文件中,除非我特別聲明,。
在許多情況下,,32位和64位數(shù)據(jù)結構都是成對出現(xiàn)的——例如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64。這些結構幾乎總是一樣的,,除了相應的64位結構中一些域的數(shù)據(jù)寬度更寬,。如果你想編寫可移植的代碼,,在WINNT.H文件中有相應的宏,這些宏可以選擇合適的32位或64位結構,,并且把它們用一個不能表明大小的別名來代替(在上面的例子中,,它就是IMAGE_NT_HEADERS)。結構的選擇依賴于你想在何種模式下編譯(具體來說就是是否定義了_WIN64),。只有在你所需編譯成的PE文件的大小屬性與你正在編譯的平臺的大小屬性不同時才需要使用具體的32位或64位結構,。
MS-DOS文件頭
每一個PE文件都以一個小的MS-DOS?可執(zhí)行文件開始。早期的Windows需要這個小占位程序,,因為那時很多用戶還未使用Windows,。當可執(zhí)行文件在沒有安裝Windows的機器上運行時,這個程序至少可以輸出一條消息,,用來指明它需要運行在Windows平臺上,。
PE文件開頭是傳統(tǒng)的MS-DOS文件頭,其中前面的一部分被稱為IMAGE_DOS_HEADER,。此結構中最重要的兩個域是e_magic和e_lfanew,。e_lfanew域保存的是真正的PE文件頭的偏移。e_magic域需要被設置成0x5A4D,。它被定義為IMAGE_DOS_SIGNATURE,。如果用ASCII碼表示,0x5A4D就是“MZ”,,這是Mark Zbikowski的姓名縮寫,,他是最初的MS-DOS設計者之一。
IMAGE_NT_HEADERS文件頭
IMAGE_NT_HEADERS結構是存儲PE文件細節(jié)的主要位置,。它的偏移由文件開頭的IMAGE_DOS_HEADER結構的e_lfanew域給出,。實際有兩種版本的IMAGE_NT_HEADER結構,一種供32位可執(zhí)行文件使用,,另一種供64位使用,。它們之間的差別非常小,因此我在討論中把它們看作相同的結構,。區(qū)別這兩種結構惟一正確的,、由Microsoft官方認可的方法是通過IMAGE_OPTION_HEADER結構(很快就會講到)的Magic域。
IMAGE_NT_HEADER結構由三個域組成:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; IMAGE_FILE_HEADER FileHeader; IMAGE_OPTIONAL_HEADER32 OptionalHeader; } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 在一個合法的PE文件中,,Signature域被設置成0x00004550,。用ASCII碼表示為“PE\0\0”。它被定義為IMAGE_NT_SIGNATURE,。第二個域是一個類型為IMAGE_FILE_HEADER的結構,,這個結構在PE文件出現(xiàn)之前就已經(jīng)出現(xiàn)了。它包含了關于文件的一些基本信息。最重要的是,,其中有一個域指明了跟在這個結構后面的可選文件頭的大小,。在PE文件中,這個可選文件頭是必須的,,但它仍然被稱為IMAGE_OPTIONAL_HEADER,。 下表列出了IMAGE_FILE_HEADER結構的各個域及相應的描述。這個結構也可以在COFF格式的OBJ文件開頭找到,。
下表列出了常用的IMAGE_FILE_xxx值:
下表列出了IMAGE_OPTIONAL_HEADER結構的成員:
IMAGE_OPTIONAL_HEADER結構末尾的DataDirectory數(shù)組就像是可執(zhí)行文件中重要位置的地址簿,。DataDirectory的每個元素結構如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // 數(shù)據(jù)的RVA
DWORD Size; // 數(shù)據(jù)的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
節(jié)表section table
緊跟著IMAGE_NT_HEADERS結構的是節(jié)表(section table),。節(jié)表是一個IMAGE_SECTION_HEADER結構數(shù)組。此結構提供了與它相關的節(jié)的信息,,其中包括位置,、長度和屬性。下表給出了對此結構的描述,。節(jié)表中此結構的數(shù)目由IMAGE_NT_HEADERS.FileHeader.NumberOfSections給出,。
下表列出了常用的節(jié)屬性標志:
可執(zhí)行文件中的節(jié)在文件中的對齊值對文件的大小有重要影響。在Visual Studio 6.0中,,鏈接器默認的節(jié)對齊值是4KB,,除非使用/OPT:NOWIN98選項或/ALIGN選項。對于Visual Studio .NET鏈接器,,雖然仍是默認使用/OPT:NOWIN98選項,,但它要確定可執(zhí)行文件是否小于某一固定值,如果小于的話,它將使用0x200字節(jié)的對齊值,。
另一個比較有用的對齊值來自.NET文件規(guī)范,。這個規(guī)范說.NET可執(zhí)行文件在內存中的對齊值應該為8KB,而不是x86上的4KB,。這是為了確保用x86入口點代碼創(chuàng)建的.NET可執(zhí)行文件可以運行在IA-64中,。如果在內存中節(jié)的對齊值為4KB,那么IA-64將不能加載這個文件,,因為在64位Windows上頁面是按8KB對齊的
我們來看一下在可執(zhí)行文件和OBJ文件中經(jīng)常遇到的一些節(jié),。除非特別說明,否則下表中的節(jié)名都來自Microsoft,。
導出表當一個EXE或DLL導出函數(shù)或變量時,,其它EXE或DLL就可以使用這些導出的函數(shù)或變量。為了簡單起見,我把導出的函數(shù)和導出的變量統(tǒng)稱為“符號”,。當導出一些符號時,,最起碼導出符號的地址需要能夠以一種已定義好的方式被獲取。每個導出的符號都有一個與之關聯(lián)的序數(shù),,它可以用來查找這個符號,。同時,幾乎總有一個ASCII碼格式的字符串名稱與這個導出的符號關聯(lián),。一般來說,,導出的符號名與源文件中的符號名是一樣的,盡管它們可以被修改的不一樣,。
通常,,當可執(zhí)行文件導入符號時,它使用的是符號的名稱而不是它的序號,。但是當通過名稱導入時,,系統(tǒng)僅使用這個名稱去查找所需符號對應的導出序數(shù),然后根據(jù)這個序數(shù)值去獲取相應的地址,。如果先使用的是序數(shù)值的話查找過程會快一點,。通過名稱導出和導入只是為了讓程序員使用方便罷了。
在.DEF文件中的Exports節(jié)中使用ORDINAL關鍵字可以告訴鏈接器創(chuàng)建一個導入庫,,這個導入庫強制函數(shù)只能通過序數(shù)導入而不能通過名稱導入,。
我首先介紹IMAGE_EXPORT_DIRECTORY結構,如下表所示:
導出目錄(Export Directory)指向三個數(shù)組和一個ASCII碼字符串表,。其中只有導出地址表是必需的,,它是一個由指向導出函數(shù)的指針組成的數(shù)組。導出序數(shù)是這個數(shù)組的索引(見下圖),。
讓我們通過例子來看一下導出表的工作原理,。下圖顯示了KERNEL32.DLL導出表的部分內容:
exports table:
Name: KERNEL32.dll
Characteristics: 00000000
TimeDateStamp: 3B7DDFD8 -> Fri Aug 17 23:24:08 2001
Version: 0.00
Ordinal base: 00000001
# of functions: 000003A0
# of Names: 000003A0
Entry Pt Ordn Name
00012ADA 1 ActivateActCtx
000082C2 2 AddAtomA
···remainder of exports omitted
假設你調用GetProcAddress來獲取KERNEL32中的AddAtomA這個API的地址。這時系統(tǒng)開始查找KERNEL32的IMAGE_EXPORT_DIRECTORY結構,。它從那里獲取了導出名稱表的起始地址,,知道了在這個數(shù)組中有0x3A0個元素,它通過二進制搜索來查找字符串“AddAtomA”,。
假設加載器發(fā)現(xiàn)AddAtomA是這個數(shù)組中的第二個元素,。然后它從導出序數(shù)表(Export Ordinal Table)中讀取相應的第二個值。這個值就是AddAtomA的導出序數(shù),。將這個導出序數(shù)作為EAT的索引(加上Base域的值),,它最終獲取AddAtomA的相對虛擬地址(RVA)是0x82C2,。將此值與KERNEL32的加載地址相加就得到了AddAtomA的實際地址。
導出轉發(fā)
導出表一個特別聰明的地方是它能將一個導出函數(shù)轉發(fā)(Forwarding)到其它DLL,。例如在Windows NT?,、Windows? 2000和Windows XP中,KERNEL32中的HeapAlloc函數(shù)被轉發(fā)到了NTDLL導出的RtlAllocHeap函數(shù)上,。轉發(fā)是在鏈接時通過.DEF文件中的EXPORTS節(jié)中的一種特殊語法形式來實現(xiàn)的,。對于HeapAlloc這個例子,KERNEL32的.DEF文件一定包含下面的內容:
EXPORTS ··· HeapAlloc = NTDLL.RtlAllocHeap 怎樣才能區(qū)別轉發(fā)的函數(shù)與正常導出的函數(shù)呢,?這需要一些技巧,。通常EAT中包含的是導出符號的RVA。但是如果這個RVA位于導出表中(通過相應的DataDirectory中的VirtualAddress域和Size域進行判斷),,那么它就是轉發(fā)的,。
當轉發(fā)一個符號時,它的RVA很明顯不能是當前模塊中的代碼或數(shù)據(jù)的地址,。實際上,,它的RVA指向一個由DLL和轉發(fā)到的符號名稱組成的字符串。在前面的例子中,,這個字符串就是NTDLL.RtlAllocHeap,。
導入表與導出函數(shù)或變量相反的就是導入它們。為了與前面保持一致,,我仍然使用“符號”這個術語來指代導入的函數(shù)和變量,。
導入數(shù)據(jù)被保存在IMAGE_IMPORT_DESCRIPTOR結構中。對應著導入表的數(shù)據(jù)目錄項就指向由這個結構組成的數(shù)組,。每個IMAGE_IMPORT_DESCRIPTOR結構都與一個導入的可執(zhí)行文件對應,。這個數(shù)組的最后一個元素的所有域都被設置為0。下表是這個結構的內容:
每個IMAGE_IMPORT_DESCRIPTOR結構指向兩個數(shù)組,,這兩個數(shù)組實際上是一樣的,。它們有好幾種叫法,但最常用的名稱是導入地址表(Import Address Table,,IAT)和導入名稱表(Import Name Talbe,,INT)。下圖顯示的是可執(zhí)行文件從USER32.DLL中導入一些API時的情況,。
這兩個數(shù)組的元素均為IMAGE_THUNK_DATA類型的結構,,這個結構是一個與指針大小相同的共用體(或者稱為聯(lián)合)。每個IMAGE_THUNK_DATA結構對應著從可執(zhí)行文件中導入的一個函數(shù),。這兩個數(shù)組最后都以一個值為0的IMAGE_THUNK_DATA結構作為結尾,。這個共用體(實際是一個DWORD值)可以有如下幾種含義:
DWORD ForwarderString;// 轉發(fā)函數(shù)字符串的RVA(見上文)
DWORD Function; // 導入函數(shù)的內存地址
DWORD Ordinal; // 導入函數(shù)的序數(shù)
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和導入函數(shù)名稱的RVA(見下文)
IAT中的IMAGE_THUNK_DATA結構的用途可以分為兩種。在可執(zhí)行文件中,,它們或者是導入函數(shù)的序數(shù),,或者是一個IMAGE_IMPORT_BY_NAME結構的RVA。IMAGE_IMPORT_BY_NAME結構只是一個WORD類型的值,,它后面跟著導入函數(shù)的名稱字符串,。這個WORD類型的值是一個“提示(hint)”,它提示加載器導入函數(shù)的序號可能是什么,。當加載器加載可執(zhí)行文件時,,它用導入函數(shù)的實際地址來覆蓋IAT中的每個元素。這一點是理解下文的關鍵,。我強烈建議你讀一讀本期雜志中Russell Osterlund的文章——揭開Windows加載器的神秘面紗,,這篇文章詳細講述了Windows加載器的行為。
在可執(zhí)行文件被加載之前,,是否存在一種方法能夠區(qū)分IMAGE_THUNK_DATA結構中到底包含的是導入函數(shù)的序數(shù)呢,,還是IMAGE_IMPORT_BY_NAME結構的RVA呢?答案在IMAGE_THUNK_DATA結構的最高位,。如果它為1,,那么低31位(在64位可執(zhí)行文件中是低63位)中是導入函數(shù)的序數(shù)。如果最高位為0,,那么IMAGE_THUNK_DATA結構的值就是IMAGE_IMPORT_BY_NAME結構的RVA,。
另一個數(shù)組INT,本質上與IAT是一樣的,。它也是一個IMAGE_THUNK_DATA結構數(shù)組,。關鍵的區(qū)別在于當加載器將可執(zhí)行文件加載進內存時,,它并不覆蓋INT。為什么對于從DLL中導入的每組API都需要有兩個并列的數(shù)組呢,?答案在于一個稱為綁定(binding)的概念,。當在綁定過程(后面我會講到)中覆蓋可執(zhí)行文件的IAT時,需要以某種方式保存原來的信息,。而作為這個信息的副本的INT,,正是這個用途。
INT對于可執(zhí)行文件的加載并不是必需的,。但是如果它不存在的話,,那么這個可執(zhí)行文件就不能被綁定。Microsoft鏈接器總是生成INT,,但是長期以來,,Borland鏈接器(TLINK)都不生成它。這樣,,由Borland鏈接器生成的可執(zhí)行文件就不能被綁定,。
在早期的Microsoft鏈接器中,導入節(jié)并不是專門針對于鏈接器的,。組成可執(zhí)行文件導入節(jié)的所有數(shù)據(jù)都來自導入庫,。你可以對一個導入庫文件運行DUMPBIN或PEDUMP來看一下。你會發(fā)現(xiàn)一些節(jié)名類似于.idata$3和.idata$4的節(jié),。鏈接器只是簡單地遵守它的規(guī)則來組合節(jié),,所有的結構和數(shù)組就神奇般地各就其位了。幾年前Microsoft引進了一種新的導入庫格式,,這種導入庫特別小,,以便讓鏈接器能在創(chuàng)建導入數(shù)據(jù)時更具主動性。
綁定
當可執(zhí)行文件被綁定時(例如通過Bind程序),,其IAT中的IMAGE_THUNK_DATA結構中是導入函數(shù)的實際地址,。也就是說,磁盤上的可執(zhí)行文件的IAT中存儲的就是其導入的DLL中的函數(shù)在內存中的實際地址,。當加載一個被綁定的可執(zhí)行文件時,,Windows加載器可以跳過查找每個導入函數(shù)并覆蓋IAT這一步。因為IAT中已經(jīng)是正確的地址了,。但是這只有正確對齊時才行,。我在2000年5月的Under the Hood專欄中講了一些測試標準,你可以通過它們來確定綁定可執(zhí)行文件能夠對加載性能有多大提高,。
你也許會懷疑將可執(zhí)行文件綁定是否保險,。你可能會想,如果綁定了可執(zhí)行文件,,但它導入的DLL發(fā)生了變化,,這時怎么辦呢?當這種情況發(fā)生時,,IAT中的地址已經(jīng)失效了,。加載器會檢查這種情況并隨機應變。如果IAT中的地址已經(jīng)失效,,加載器會根據(jù)INT中的信息重新解析導入函數(shù)的地址,。
在安裝程序時對其進行綁定應該是最可能發(fā)生的情況了。Windows Installer中的BindImage這個動作可以替你做這件事,。同樣,,IMAGEHLP.DLL中也提供了BindImageEx這個API。不管用哪一種方法,,綁定都是個比較好的做法,。如果加載器確定綁定信息是有效的,那么可執(zhí)行文件就會被加載的更快,。如果綁定信息失效,,它也并不會比不綁定效果差。
對加載器來說,,使綁定生效的一個關鍵步驟是確定IAT中的綁定信息是否有效,。當可執(zhí)行文件被綁定時,有關它導入的DLL的信息也被放在可執(zhí)行文件中,。加載器檢查這個信息以快速確定綁定的有效性,。在綁定的最初實現(xiàn)中并未添加這個信息,因此可執(zhí)行文件可能按老的綁定方式進行綁定,,或者按新的綁定方式進行綁定,。我在這里講的是新的綁定方式。
確定綁定信息有效性的一個關鍵數(shù)據(jù)結構是IMAGE_BOUND_IMPORT_DESCRIPTOR,。被綁定的可執(zhí)行文件中有一個此結構的列表,。每個IMAGE_BOUND_IMPORT_DESCRIPTOR結構表示一個綁定到的DLL的日期/時間戳。這個列表的RVA由數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT的元素給出,。IMAGE_BOUND_IMPORT_DESCRIPTOR結構中的成員如下:
一般情況下,,每個導入的DLL對應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構簡單地組成一個數(shù)組。但是當綁定的API轉發(fā)到了另一個DLL上時,,這個轉發(fā)到的DLL的有效性也需要檢查,。在這種情況下,IMAGE_BOUND_FORWARDER_REF結構就與IMAGE_BOUND_IMPORT_DESCRIPTOR結構交叉在了一起,。下面舉一個例子來說明,。
假設你鏈接到了KERNEL32.DLL中的HeapAlloc這個API上,而它實際上被轉發(fā)到了NTDLL中的RtlAllocateHeap上,,然后你綁定這個可執(zhí)行文件,。那么在這個可執(zhí)行文件中,對應于KERNEL32.DLL這個導入的DLL就有一個相應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構,,同時它后面是一個對應于NTDLL.DLL的IMAGE_BOUND_FORWARDER_REF結構,。緊跟在它們后面的可能是與你導入并綁定到的其它DLL對應的IMAGE_BOUND_IMPORT_DESCRIPTOR結構。
延遲加載數(shù)據(jù)
前面我已經(jīng)講過延遲加載(Delayload)一個DLL就是隱含導入與通過LoadLibrary和GetProcAddress顯式導入這兩種方式的混合?,F(xiàn)在讓我們來看一下延遲加載所需的數(shù)據(jù)結構以及它的工作原理,。
一定要記住延遲加載并不是操作系統(tǒng)的功能。它完全是由鏈接器和運行時庫添加的附加代碼和數(shù)據(jù)來實現(xiàn)的,。正因為如此,,WINNT.H中并沒有幾個地方涉及到延遲加載。但是你會發(fā)現(xiàn)延遲加載數(shù)據(jù)和正常導入數(shù)據(jù)二者的定義是平行的,。
DataDirectory中的IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT元素指向延遲加載數(shù)據(jù),。這個元素中實際是一個ImgDelayDescr結構數(shù)組的RVA,這個結構被定義在Visual C++的DelayImp.H文件中,。下表是這個結構的內容,。對應于每一個導入的DLL都有一個相應的ImgDelayDescr結構。
我們從ImgDelayDescr結構中可以獲取的主要內容就是它包含了DLL的IAT和INT的地址,。這些表與正常情況下的表是一樣的,只不過它們是由運行時庫代碼進行讀寫而不是由操作系統(tǒng),。當你調用延遲加載的DLL中的函數(shù)時,,運行時庫代碼就調用LoadLibrary加載相應的DLL(如果需要的話),然后調用GetProcAddress來獲取函數(shù)地址,,最后將獲取的地址存儲在延遲加載IAT中,,以便將來可以直接調用這個函數(shù)。
延遲加載所使用的數(shù)據(jù)結構在設計時有一個失誤的地方需要解釋一下,。在Visual C++ 6.0中——這是它最初的形式,,ImgDelayDescr結構中的所有包含地址的域使用的都是虛擬地址,而不是RVA,。也就是說,,它們包含了延遲加載數(shù)據(jù)所在位置的實際地址。這些域都是DWORD類型的,,也就是x86上一個指針的大小,。
現(xiàn)在要全面支持IA-64了。突然,,4字節(jié)已經(jīng)不夠保存一個完整的地址了,。哎呀!在這個時候,,Microsoft做了一件正確的事,,把包含地址的域都改為包含RVA了。如前面所示,,我使用的是已經(jīng)修訂過的結構定義和名稱,。
還有一個問題就是確定ImgDelayDescr使用的是RVA還是虛擬地址。這個結構中有一個域包含了相關的標志。當grAttrs域為1時,,這個結構中的成員中包含的是RVA,。從Visual Studio? .NET和64位編譯器開始,這是惟一選項,。如果grAttrs不是1,,ImgDelayDescr結構中的域包含的都是虛擬地址。
資源節(jié)在PE文件的所有節(jié)中,,在資源節(jié)中定位數(shù)據(jù)是最復雜的,。在這里我只講述一些獲取諸如圖標、位圖以及對話框之類的資源的原始數(shù)據(jù)所需的一些數(shù)據(jù)結構,。我不涉及它們的實際格式,,那已經(jīng)超出了本文的范圍。
資源可以在一個叫做.rsrc的節(jié)中找到,。DataDirectory中索引為IMAGE_DIRECTORY_ENTRY_RESOURCE的元素包含了資源的RVA和大小,。由于多方面的原因,資源被組織得與文件系統(tǒng)類似——有目錄和葉結點,。
DataDirectory中的資源指針指向了一個IMAGE_RESOURCE_DIRECTORY類型的結構,。這個結構中包含了目前尚未使用的Characteristics域、TimeDateStamp域以及版本號域(MajorVersion和MinorVersion),。這個結構中真正有用的域是NumberOfNamedEntries和NumberOfIdEntries,。
每個IMAGE_RESOURCE_DIRECTORY結構后面是一個IMAGE_RESOURCE_DIRECTORY_ENTRY結構數(shù)組。另外,,IMAGE_RESOURCE_DIRECTORY結構中的NumberOfNamedEntries和NumberOfIdEntries這兩個域保存的就是這個數(shù)組中IMAGE_RESOURCE_DIRECTORY_ENTRY結構的數(shù)目,。(如果你感覺這些數(shù)據(jù)結構的名稱讓你看得頭疼,說句實在話,,我將它們寫下來也挺難受的?。?/div>
每個目錄項(即IMAGE_RESOURCE_DIRECTORY_ENTRY結構)或者指向另一個資源目錄,或者指向具體的資源數(shù)據(jù),。當它指向另一個資源目錄時,,這個結構中的第二個DWORD的最高位為1,其余的31位是那個資源目錄的偏移,。這個偏移是相對于資源節(jié)開頭來說的,,而不是RVA。
當它指向實際的某種資源時,,第二個DWORD的最高位為0,,其余的31位是具體資源(例如對話框)的偏移。同上面一樣,,這個偏移同樣是相對于資源節(jié)開頭來說的,,而不是RVA。
每個目錄項可以通過名稱或者ID值來標識。它們就是你在.RC文件中為具體資源指定的名稱或ID值,。當目錄項的第一個DWORD的最高位為1時,,其余的31位是資源名稱(字符串)的偏移;如果最高位為0,,那么其低16位是資源標識(ID)的值,。
理論已經(jīng)足夠了!現(xiàn)在讓我們看一個實際的例子,。下面是PEDUMP輸出的ADVAPI32.DLL的資源節(jié)的部分內容:
Resources (RVA: 6B000)
ResDir (0) Entries:03 (Named:01, ID:02) TimeDate:00000000
———————————————————————————————
ResDir (MOFDATA) Entries:01 (Named:01, ID:00) TimeDate:00000000
ResDir (MOFRESOURCENAME) Entries:01 (Named:00, ID:01) TimeDate:00000000
ID: 00000409 DataEntryOffs: 00000128
DataRVA: 6B6F0 DataSize: 190F5 CodePage: 0
———————————————————————————————
ResDir (STRING) Entries:01 (Named:00, ID:01) TimeDate:00000000
ResDir (C36) Entries:01 (Named:00, ID:01) TimeDate:00000000
ID: 00000409 DataEntryOffs: 00000138
DataRVA: 6B1B0 DataSize: 0053C CodePage: 0
———————————————————————————————
ResDir (RCDATA) Entries:01 (Named:00, ID:01) TimeDate:00000000
ResDir (66) Entries:01 (Named:00, ID:01) TimeDate:00000000
ID: 00000409 DataEntryOffs: 00000148
DataRVA: 85908 DataSize: 0005C CodePage: 0
其中以“ResDir”開頭的每一行對應于一個IMAGE_RESOURCE_DIRECTORY結構,。“ResDir“后面的括號中是資源目錄的名稱,。在這個例子中,,資源目錄的名稱分別為0、MOFDATA,、MOFRESOURCENAME、STRING,、C36,、RCDATA和66。名稱后面是以名稱標識的和以ID標識的資源目錄的總個數(shù)(后面的括號中是它們分別的個數(shù)),。在這個例子中,,頂級目錄一共有3個直接的子目錄,所有其它目錄都只有一個下級子目錄,。
頂級目錄類似于文件系統(tǒng)中的根目錄,。根目錄下的每個子目錄項(也就是第二級目錄)代表資源的類型(字符串表、對話框,、菜單等等),。它們下面還有第三級子目錄。
對于某種具體的資源類型來說,,一般有三級目錄,。例如如果有五個對話框,那么第二級的DIALOG目錄下面將會有五個子目錄項,。這五個子目錄項本身也都是目錄,。在這五個目錄下面都只有一項內容,它就是具體資源的原始數(shù)據(jù)的偏移地址,。很簡單,,不是嗎?
如果你更喜歡通過讀源代碼來學習的話,,你可以仔細看一下PEDUMP中轉儲資源的那部分代碼(PEDUMP的源代碼可以從2002年2月本文的第一部分中下載),。除了顯示所有的資源目錄以及它們的元素個數(shù)外,PEDUMP還可以顯示幾種常見的資源類型,例如對話框等,。
基址重定位在可執(zhí)行文件中的許多地方,,你都會發(fā)現(xiàn)內存地址的蹤跡。當鏈接器在生成可執(zhí)行文件時,,它假定這個可執(zhí)行文件會被加載到內存中的某一個地址處(即首選地址),。只有在可執(zhí)行文件被加載到其首選地址時,所有這些內存地址才是正確的,。這個首選地址由IMAGE_FILE_HEADER結構中的ImageBase域給出,。
如果加載器由于某種原因需要把可執(zhí)行文件加載到其它地址處時,所有這些地址都變成不正確的了,。這將會額外增加加載器的工作量,。在2000年5月的Under The Hood專欄(前面已經(jīng)提到)中我已經(jīng)講過當幾個DLL首選加載地址相同時會導致性能損失,以及如何使用REBASE工具來解決這個問題,。
基址重定位(Base Relocations)信息告訴加載器可執(zhí)行文件不能被加載到其首選地址時需要進行修改的每一個位置,。對于加載器來說,幸運的是它并不需要知道地址使用的細節(jié)問題,。它只知道有一個地址列表,,其中的每一個地址都需要以同樣的方式進行修改。
讓我們來看一個x86平臺上的可執(zhí)行文件的例子,。假設有以下指令,,它將一個全局變量(地址0x0040D434)的值加載到ECX寄存器中:
00401020: 8B 0D 34 D4 40 00 mov ecx,dword ptr [0x0040D434]
這條指令在地址0x00401020處,長為6個字節(jié),。前兩個字節(jié)(0x8B 0x0D)是指令的機器碼,。剩下的四個字節(jié)是一個DWORD值的地址(0x0040D434)。在這個例子中,,這條指令實際來自一個首選地址為0x00400000的可執(zhí)行文件,,因此這個全局變量的RVA為0xD434。
如果這個可執(zhí)行文件被加載到了0x00400000處,,這條指令當然可以正確執(zhí)行,。但是現(xiàn)在我們假設它被加載到了0x00500000處。如果真是這樣,,那么這條指令的最后的四個字節(jié)需要被改成0x0050D434,。
那么加載器是如何做的呢?它比較首選加載地址與實際加載地址,,然后計算出△(delta,,音譯為德耳塔,數(shù)學中的常用符號,,表示差值的意思),。在這個例子中,,△為0x00100000。這個△被加到變量原來的地址值(大小為DWORD)上,,形成新的地址,。在前面的例子中,關于地址0x00401022處,,即指令中的DWORD值處,,將會有一個相應的重定位信息。
簡而言之,,基址重定位信息只是可執(zhí)行文件中的一個地址列表,,當加載進內存時,這些地址中的值都要再加上△,。為了提高系統(tǒng)性能,,可執(zhí)行文件的頁面只有在需要時才會被加載進內存(可執(zhí)行文件的加載與內存映射文件類似),基址重定位信息的格式就反映了這個特性,?;分囟ㄎ恍畔⑺诘墓?jié)通常被稱為.reloc節(jié),但是查找它的正確方法是通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BASERELOC的那個元素,。
基址重定位信息是一些非常簡單的IMAGE_BASE_RELOCATION結構,。此結構中的VirtualAddress域包含了需要進行重定位的內存范圍的起始RVA。SizeOfBlock域給出了重定位信息的大小,,其中包括IMAGE_BASE_RELOCATION自身的大小。
緊跟著IMAGE_BASE_RELOCATION結構后面是一組可變數(shù)目的WORD值,。這些WORD值的數(shù)目可以從IMAGE_BASE_RELOCATION結構的SizeOfBlock域推出,。其中每個WORD值由兩部分組成。高4位指明了重定位的類型,,由WINNT.H中的一系列IMAGE_REL_BASED_xxx值給出,。低12位是相對于IMAGE_BASE_RELOCATION結構的VirtualAddress域的偏移,這是應該進行重定位的地方,。
在前面那個關于基址重定位的例子中,,我把情況簡化了。實際上有多種類型的重定位方式,。對于x86平臺上的可執(zhí)行文件來說,,所有的重定位類型都是IMAGE_REL_BASED_HIGHLOW。你經(jīng)常會在一組重定位信息之后看到類型為IMAGE_REL_BASED_ABSOLUTE的重定位信息,。它們實際上并沒有什么作用,,只是為了填充空間以便下一個IMAGE_BASE_RELOCATION結構能夠按4字節(jié)的邊界對齊。
對于IA-64平臺上的可執(zhí)行文件來說,,重定位類型好像總是IMAGE_REL_BASED_DIR64,。與x86平臺一樣的是,,通常也會有作為填充的IMAGE_REL_BASED_ABSOLUTE類型的重定位信息。有趣的一點是,,盡管IA-64平臺上每個頁面是8KB,,但基址重定位信息仍舊是分成4KB的塊。
在Visual C++ 6.0中,,鏈接器在創(chuàng)建發(fā)行版的EXE文件時并不生成重定位信息,。這是由于EXE文件是最先被加載到進程的地址空間中的,因此可以絕對保證它能被加載到其首選地址上,。DLL就沒有這么幸運了,,因此DLL中總是存在基址重定位信息,除非你使用/FIXED鏈接器選項明確忽略它們,。在Visual Studio .NET中,,鏈接器在生成調試版和發(fā)行版的EXE文件時都不產生基址重定位信息。
調試目錄當創(chuàng)建可執(zhí)行文件并生成相應的調試信息時,,通常文件中會包含這種信息格式的細節(jié)以及它的位置,。操作系統(tǒng)運行可執(zhí)行文件時并不需要調試信息,但它對于開發(fā)工具非常有用,。一個EXE文件可以包含多種格式的調試信息,,調試目錄(Debug Directory)結構指出哪種格式可用。
可以通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_DEBUG的元素找到調試目錄,。它是由IMAGE_DEBUG_DIRECTORY結構組成的數(shù)組,,其中每一個結構對應一種類型的調試信息,如下表所示,。調試目錄中元素的數(shù)目可以使用數(shù)據(jù)目錄中的Size域計算得出,。
到目前為止,,最流行的調試信息格式是PDB文件,。PDB文件實質上是CodeView格式調試信息的發(fā)展。一個類型為IMAGE_DEBUG_TYPE_CODEVIEW的調試目錄標志著PDB信息的存在,。如果你檢查由這個元素指向的數(shù)據(jù),,會發(fā)現(xiàn)一個短的CodeView格式的頭部。這個調試數(shù)據(jù)主要是一個外部PDB文件的路徑,。在Visual Studio 6.0中,,調試頭開始處是一個NB10簽名。在Visual Studio .NET中,,這個頭開始處是RSDS,。
在Visual Studio 6.0中,可以使用/DEBUGTYPE:COFF鏈接器選項來生成COFF調試信息,。Visual Studio .NET將這項功能移除了,。對于經(jīng)過優(yōu)化的x86代碼,由于函數(shù)可能沒有正常的棧幀,,所有使用幀指針省略(Frame Pointer Omission,,F(xiàn)PO)調試信息。FPO數(shù)據(jù)允許調試器定位局部變量和參數(shù),。
有兩種OMAP調試信息僅用于Microsoft的程序,。Microsoft內部使用一種工具對可執(zhí)行文件中的代碼進行重新排列以減少分頁。(它所做的不僅僅是Working Set Tuner所能做到的,。)OMAP信息讓工具可以在調試信息中的原始地址與重排后的代碼中的新地址之間進行轉換,。
順便說一下,DBG文件也包含了一個類似于我上面講的調試目錄,。DBG文件流行于Windows NT 4.0時代,,它們主要包含COFF調試信息,但是Windows XP偏愛PDB文件而將它們淘汰了,。
.NET頭部對于開發(fā)工具生成的用于Microsoft .NET環(huán)境下的可執(zhí)行文件來說,它們首先是PE文件,。但是在大多數(shù)情況下.NET文件中正常的代碼和數(shù)據(jù)是微不足道的,。.NET可執(zhí)行文件的主要目的是將.NET特定的信息,例如元數(shù)據(jù)和中間語言(IL),,加載進內存,。另外.NET可執(zhí)行文件鏈接到了MSCOREE.DLL文件上。這個DLL是.NET進程的起點,。當加載.NET可執(zhí)行文件時,,它的入口點通常是一個小的占位程序。這個占位程序只是跳轉到MSCOREE.DLL的一個導出函數(shù)(_CorExeMain或_CorDllMain)上,。從那里開始,,MSCOREE獲取控制權,,開始使用可執(zhí)行文件中的元數(shù)據(jù)和IL。這類似于(.NET版之前的)Visual Basic中的應用程序使用MSVBVM60.DLL所采用的方式,。.NET信息的起點是IMAGE_COR20_HEADER結構,,它當前被定義在.NET Framework SDK中的CorHDR.H文件以及最新的WINNT.H文件中。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR的項指向IMAGE_COR20_HEADER結構,。下表列出了IMAGE_COR20_HEADER結構中的域,。關于IMAGE_COR20_HEADER指向的元數(shù)據(jù)、方法IL以及其它內容將在后續(xù)文件中詳細講述,。
TLS初始化當使用__declspec(thread)定義線程局部變量時,,編譯器將它們放入一個名為.tls的節(jié)中,。當系統(tǒng)創(chuàng)建新線程時,它從進程堆中分配內存來保存用于新線程的線程局部變量,。這部分內存使用.tls節(jié)中的值進行初始化,。系統(tǒng)將分配的內存的地址保存在TLS數(shù)組中,F(xiàn)S:[2Ch]指向這個數(shù)組(在x86平臺上),。
如果數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_TLS的元素不為0,,那就表示可執(zhí)行文件中存在線程局部存儲(TLS),。而這個元素指向一個IMAGE_TLS_DIRECTORY結構,如下表所示,。
注意到IMAGE_TLS_DIRECTORY中的地址都是虛擬地址而不是RVA這一點很重要,。因此如果可執(zhí)行文件不能被加載到其首選加載地址時,它們都要進行基址重定位,。同時,,IMAGE_TLS_DIRECTORY結構本身并不在.tls節(jié)中,它位于.rdata節(jié)中,。
程序異常數(shù)據(jù) 一些平臺(包括IA-64)并不使用x86平臺上的基于幀的異常處理,,它們使用的是基于表的異常處理。在這種異常處理中有一個表,,它包含了可能會被異常展開(unwinding)影響到的每一個函數(shù)的信息,。這些信息主要包括每個函數(shù)的開始地址、結束地址以及在哪里并如何處理異常,。當發(fā)生異常時,,系統(tǒng)搜索整個表來尋找處理它的相應項并處理,。異常表是一個由IMAGE_RUNTIME_FUNCTION_ENTRY結構組成的數(shù)組,。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_EXCEPTION的元素引向此數(shù)組。這個結構的格式因平臺而異,。對于IA-64平臺,,它的結構如下:
DWORD BeginAddress;
DWORD EndAddress;
DWORD UnwindInfoAddress;
UnwindInfoAddress數(shù)據(jù)的結構并未在WINNT.H文件中給出,。但是它的具體格式可以在Intel的"IA-64 Software Conventions and Runtime Architecture Guide"一書第11章中找到。
PEDUMP程序 現(xiàn)在我的PEDUMP程序與1994年時的相比已經(jīng)有了很大改進,。它可以顯示本文中講的所有結構,,其中包括:
除了可以轉儲可執(zhí)行文件外,PEDUMP還可以轉儲COFF格式的OBJ文件,、COFF導入庫(新格式以及老格式),、COFF符號表和DBG文件。
PEDUMP是一個命令行程序,。對前面提到的各種文件運行PEDUMP時,,如果不加任何選項,它默認輸出的是最有用的數(shù)據(jù)結構信息,。有好幾個命令行選項可以用來添加其它的輸出信息:
關于PEDUMP的源代碼有幾個地方值得注意,。首先它可以按32位或64位可執(zhí)行文件編譯和運行。如果你手邊有Itanium機器可以試一下,。另外,,無論PEDUMP以何種方式編譯,它都可以同時轉儲32位和64位文件,。換句話說,,32位版的PEDUMP可以轉儲32位和64位文件,64位版的PEDUMP也可以轉儲32位和64位文件,。
在考慮使PEDUMP可以同時處理32位和64位文件時,,我想避免為32位結構和64位結構分別寫一個函數(shù)。因此我使用了C++模板,。
在好幾個文件(特別是EXEDUMP.CPP)中,,你都會發(fā)現(xiàn)各種模板函數(shù)。大多數(shù)情況下,,模板函數(shù)的參數(shù)最終會被擴展為IMAGE_NT_HEADERS32結構或 IMAGE_NT_HEADERS64結構,。當調用這些函數(shù)時,由代碼自身確定是32位還是64位文件并用相應參數(shù)類型去調用相應的函數(shù),,引起相應的模板展開,。
伴隨PEDUMP源代碼的還有一個Visual C++ 6.0工程文件。工程配置除了傳統(tǒng)的x86 debug和 release外,,還有相應的64位配置,。要想使它正常工作,你需要把64位工具(當前在Platform SDK中)的路徑添加到Tools | Options | Directories 選項卡最上面的Executable files路徑中,,同時還要設置相應的64位Include目錄和Lib目錄的路徑,。在我的機器上這個工程文件可以正常工作,但是在你的機器上可能需要進行少量修改才行。
為了使PEDUMP可以處理的內容盡可能全面,,這就需要使用最新的Windows頭文件,。我在開發(fā)這個程序時使用的是2001年6月的Platform SDK,需要的這些文件都在.\include\prerelease和.\Include\Win64\crt目錄中,。在2001年8月的SDK中,, WINNT.H文件已經(jīng)被更新,因此也就不需要prerelease目錄中的文件了,。最終可以成功創(chuàng)建這個程序,。你需要做的可能只是盡是安裝最新的Platform SDK或在創(chuàng)建64位版的程序時對工程目錄進行一些修改。
結束語
可移植可執(zhí)行文件格式是一種結構非常好且相對簡單的可執(zhí)行文件格式,。特別好的一點是PE文件可以被直接映射進內存,,這樣它在磁盤上的數(shù)據(jù)結構與運行時Windows使用的結構一致。 |
|