久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

深入剖析Win32可移植可執(zhí)行文件格式(第二部分)

 quasiceo 2013-12-07

深入剖析Win32可移植可執(zhí)行文件格式(第二部分)  

2008-05-03 20:39:57|  分類: windows操作系統(tǒng) |字號 訂閱

深入剖析Win32可移植可執(zhí)行文件格式
第二部分
作者:Matt Pietrek
上個月在本文的第一部分中,我首先對可移植可執(zhí)行文件進(jìn)行了全面的介紹,。我講了PE文件的歷史和組成PE文件頭的數(shù)據(jù)結(jié)構(gòu),,還講了節(jié)表。PE文件頭和節(jié)表告訴你在可執(zhí)行文件中都包含什么類型的代碼和數(shù)據(jù),,以及在哪里能找到它們,。
本月我要講一下常見的節(jié)。最后講一下我的最新的經(jīng)過徹底改進(jìn)的PEDUMP程序,,它可以在2002年2月的專欄中下載,。如果你不熟悉PE文件的基本概念,應(yīng)該首先讀一下本文的第一部分,。
上個月我講了節(jié)是怎樣的一個邏輯上屬于一起的代碼或數(shù)據(jù)塊,。例如可執(zhí)行文件的所有導(dǎo)入信息都在一個節(jié)中。現(xiàn)在讓我們來看一下在可執(zhí)行文件和OBJ文件中經(jīng)常遇到的一些節(jié),。除非特別說明,,否則下表中的節(jié)名都來自Microsoft的工具。
名稱
描述
.text
默認(rèn)的代碼節(jié),。
.data
默認(rèn)的可讀/可寫數(shù)據(jù)節(jié),。全局變量通常在這個節(jié)中。
.rdata
默認(rèn)的只讀數(shù)據(jù)節(jié),。字符串常量和C++/COM虛表就放在這個節(jié)中,。
.idata
導(dǎo)入表。實際上,,鏈接器經(jīng)常把.idata節(jié)合并到其它節(jié)中(或者是明確指定的,,或者是通過鏈接器的默認(rèn)行為)。默認(rèn)情況下,,鏈接器僅在創(chuàng)建發(fā)行版的程序時才把.idata節(jié)合并到其它節(jié)中,。
.edata
導(dǎo)出表。當(dāng)創(chuàng)建要導(dǎo)出函數(shù)或數(shù)據(jù)的可執(zhí)行文件時,,鏈接器會創(chuàng)建一個.EXP文件,。這個.EXP文件包含一個.edata節(jié),,這個節(jié)被添加到最后的可執(zhí)行文件中。與.idata節(jié)一樣,,.edata節(jié)也經(jīng)常被合并到.text節(jié)或.rdata節(jié)中,。
.rsrc
資源節(jié)。這個節(jié)是只讀的,。它不應(yīng)該被命名為其它名稱,,也不應(yīng)該被合并到其它節(jié)中。
.bss
未初始化的數(shù)據(jù)節(jié),。在最新的鏈接器創(chuàng)建的可執(zhí)行文件中很少見到,。鏈接器擴展可執(zhí)行文件的.data節(jié)的VirtualSize域以便容納未初始化的數(shù)據(jù)。
.crt
添加到可執(zhí)行文件中的數(shù)據(jù),,用來支持C++運行時庫(CRT)。一個比較好的例子就是用于調(diào)用靜態(tài)C++對象的構(gòu)造函數(shù)和析構(gòu)函數(shù)的指針,。要獲取更詳細(xì)的信息,,可以參考2001年1月的Under The Hood專欄
.tls
這個節(jié)中的數(shù)據(jù)用來支持使用__declspec(thread)語法創(chuàng)建的線程局部存儲變量,。它包括數(shù)據(jù)的初始值,,以及運行時需要的附加變量。
.reloc
可執(zhí)行文件中的基址重定位節(jié),。通常DLL需要基址重定位信息而EXE并不需要,。在創(chuàng)建發(fā)行版的程序時,鏈接器并不為EXE文件生成基址重定位信息,??梢允褂?FIXED鏈接器選項移除基址重定位信息。
.sdata
通過全局指針(Global Pointer)相對尋址的“短(Short)”可讀/可寫數(shù)據(jù),。用于IA-64和其它使用全局指針寄存器的平臺上,。IA-64平臺上正常大小的全局變量在這個節(jié)中。
.srdata
通過全局指針相對尋址的“短(Short)”只讀數(shù)據(jù),。用于IA-64和其它使用全局指針寄存器的平臺上,。
.pdata
異常表。它包含一個IMAGE_RUNTIME_FUNCTION_ENTRY結(jié)構(gòu)數(shù)組,,這個結(jié)構(gòu)與平臺體系結(jié)構(gòu)相關(guān),。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_EXCEPTION的項指向它。用于使用基于表的異常處理的平臺,,例如IA-64,。惟一不使用基于表的異常處理的平臺是x86(它使用的是基于堆棧的異常處理)。
.debug$S
OBJ文件中的Codeview格式的調(diào)試符號(Symbol)信息,。這是一列可變長度的CodeView格式的調(diào)試符號記錄,。
.debug$T
OBJ文件中的Codeview格式的調(diào)試類型(Type)記錄,。這是一列可變長度的CodeView格式的調(diào)試類型記錄。
.debug$P
可以在使用預(yù)編譯頭(Precompiled Headers)生成的OBJ文件中找到這個節(jié),。
.drectve
這個節(jié)包含鏈接器指令,,并且只存在于OBJ文件中。這些指令是傳遞到鏈接器命令行的ASCII碼字符串,,例如:-defaultlib:LIBC,。指令之間用空格分開。
.didat
延遲加載導(dǎo)入數(shù)據(jù),??梢栽诜前l(fā)行版本的可執(zhí)行文件中找到。在發(fā)行版本中,,延遲加載數(shù)據(jù)被合并到其它節(jié)中,。
導(dǎo)出表
當(dāng)一個EXE或DLL導(dǎo)出函數(shù)或變量時,其它EXE或DLL就可以使用這些導(dǎo)出的函數(shù)或變量,。為了簡單起見,,我把導(dǎo)出的函數(shù)和導(dǎo)出的變量統(tǒng)稱為“符號”。當(dāng)導(dǎo)出一些符號時,,最起碼導(dǎo)出符號的地址需要能夠以一種已定義好的方式被獲取,。每個導(dǎo)出的符號都有一個與之關(guān)聯(lián)的序數(shù),它可以用來查找這個符號,。同時,,幾乎總有一個ASCII碼格式的字符串名稱與這個導(dǎo)出的符號關(guān)聯(lián)。一般來說,,導(dǎo)出的符號名與源文件中的符號名是一樣的,,盡管它們可以被修改的不一樣。
通常,,當(dāng)可執(zhí)行文件導(dǎo)入符號時,,它使用的是符號的名稱而不是它的序號。但是當(dāng)通過名稱導(dǎo)入時,,系統(tǒng)僅使用這個名稱去查找所需符號對應(yīng)的導(dǎo)出序數(shù),,然后根據(jù)這個序數(shù)值去獲取相應(yīng)的地址。如果先使用的是序數(shù)值的話查找過程會快一點,。通過名稱導(dǎo)出和導(dǎo)入只是為了讓程序員使用方便罷了,。
在.DEF文件中的Exports節(jié)中使用ORDINAL關(guān)鍵字可以告訴鏈接器創(chuàng)建一個導(dǎo)入庫,這個導(dǎo)入庫強制函數(shù)只能通過序數(shù)導(dǎo)入而不能通過名稱導(dǎo)入,。
我首先介紹IMAGE_EXPORT_DIRECTORY結(jié)構(gòu),,如下表所示:
大小
描述
DWORD
Characteristics
導(dǎo)出標(biāo)志。當(dāng)前未定義任何值。
DWORD
TimeDateStamp
導(dǎo)出數(shù)據(jù)的創(chuàng)建時間,。這個域的定義與IMAGE_NT_HEADERS.FileHeader.TimeDateStamp相同(從GMT時間1970年1月1日00:00以來的總秒數(shù)),。
WORD
MajorVersion
導(dǎo)出數(shù)據(jù)的主版本號。未用,,設(shè)置為0,。
WORD
MinorVersion
導(dǎo)出數(shù)據(jù)的次版本號。未用,,設(shè)置為0,。
DWORD
Name
與導(dǎo)出符號相關(guān)的DLL的名稱ASCII字符串的RVA(例如KERNEL32.DLL)。
DWORD
Base
這個域包含了這個可執(zhí)行文件的導(dǎo)出符號所使用的序數(shù)值的起始值,。通常情況下這個值為1,,但并不總是這樣。當(dāng)通過序數(shù)查找導(dǎo)出符號時,,將序數(shù)值減去這個域的值就得到了這個導(dǎo)出符號在導(dǎo)出地址表(Export Address Table ,,EAT)中的索引。
DWORD
NumberOfFunctions
EAT中的元素數(shù),。注意EAT中的某些元素可能為0,,這表明沒有
代碼/數(shù)據(jù)使用那個序數(shù)值導(dǎo)出。
DWORD
NumberOfNames
導(dǎo)出名稱表(Export Names Table,,ENT)中的元素數(shù)。這個域的值總是小于或等于NumberOfFunctions域的值,。當(dāng)某些符號僅使用序數(shù)導(dǎo)出時,,它就小于那個域的值。如果導(dǎo)出序數(shù)之間有間隔,,它同樣也小于那個域的值,。這個域的值也是導(dǎo)出序數(shù)表的大小(見下文),。
DWORD
AddressOfFunctions
EAT的RVA,。EAT中的每個元素都是一個RVA。其中每個非0的RVA都對應(yīng)一個導(dǎo)出符號,。
DWORD
AddressOfNames
ENT的RVA,。ENT中的每個元素都是一個ASCII碼字符串的RVA。其中的每個ASCII碼字符串都對應(yīng)一個由名稱導(dǎo)出的符號,。這些字符串是按一定順序排列的,。這就使得加載器在查找導(dǎo)出符號時可以進(jìn)行二進(jìn)制搜索。名稱字符串的排序是按二進(jìn)制(與C++運行時庫函數(shù)strcmp類似),,而不是與位置相關(guān)的字母表順序,。
DWORD
AddressOfNameOrdinals
導(dǎo)出序號表的RVA。這個表是一個WORD類型的數(shù)組。它將ENT中的索引映射到導(dǎo)出地址表中相應(yīng)的元素上,。
導(dǎo)出目錄(Export Directory)指向三個數(shù)組和一個ASCII碼字符串表,。其中只有導(dǎo)出地址表是必需的,它是一個由指向?qū)С龊瘮?shù)的指針組成的數(shù)組,。導(dǎo)出序數(shù)是這個數(shù)組的索引(見下圖),。
讓我們通過例子來看一下導(dǎo)出表的工作原理。下圖顯示了KERNEL32.DLL導(dǎo)出表的部分內(nèi)容:
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
假設(shè)你調(diào)用GetProcAddress來獲取KERNEL32中的AddAtomA這個API的地址,。這時系統(tǒng)開始查找KERNEL32的IMAGE_EXPORT_DIRECTORY結(jié)構(gòu),。它從那里獲取了導(dǎo)出名稱表的起始地址,知道了在這個數(shù)組中有0x3A0個元素,,它通過二進(jìn)制搜索來查找字符串“AddAtomA”,。
假設(shè)加載器發(fā)現(xiàn)AddAtomA是這個數(shù)組中的第二個元素。然后它從導(dǎo)出序數(shù)表(Export Ordinal Table)中讀取相應(yīng)的第二個值,。這個值就是AddAtomA的導(dǎo)出序數(shù),。將這個導(dǎo)出序數(shù)作為EAT的索引(加上Base域的值),它最終獲取AddAtomA的相對虛擬地址(RVA)是0x82C2,。將此值與KERNEL32的加載地址相加就得到了AddAtomA的實際地址,。
導(dǎo)出轉(zhuǎn)發(fā)
導(dǎo)出表一個特別聰明的地方是它能將一個導(dǎo)出函數(shù)轉(zhuǎn)發(fā)(Forwarding)到其它DLL。例如在Windows NT,、Windows 2000和Windows XP中,,KERNEL32中的HeapAlloc函數(shù)被轉(zhuǎn)發(fā)到了NTDLL導(dǎo)出的RtlAllocHeap函數(shù)上。轉(zhuǎn)發(fā)是在鏈接時通過.DEF文件中的EXPORTS節(jié)中的一種特殊語法形式來實現(xiàn)的,。對于HeapAlloc這個例子,,KERNEL32的.DEF文件一定包含下面的內(nèi)容:
        EXPORTS
        
        HeapAlloc = NTDLL.RtlAllocHeap
怎樣才能區(qū)別轉(zhuǎn)發(fā)的函數(shù)與正常導(dǎo)出的函數(shù)呢?這需要一些技巧。通常EAT中包含的是導(dǎo)出符號的RVA。但是如果這個RVA位于導(dǎo)出表中(通過相應(yīng)的DataDirectory中的VirtualAddress域和Size域進(jìn)行判斷),,那么它就是轉(zhuǎn)發(fā)的,。
當(dāng)轉(zhuǎn)發(fā)一個符號時,它的RVA很明顯不能是當(dāng)前模塊中的代碼或數(shù)據(jù)的地址。實際上,它的RVA指向一個由DLL和轉(zhuǎn)發(fā)到的符號名稱組成的字符串。在前面的例子中,,這個字符串就是NTDLL.RtlAllocHeap。
導(dǎo)入表
與導(dǎo)出函數(shù)或變量相反的就是導(dǎo)入它們,。為了與前面保持一致,,我仍然使用“符號”這個術(shù)語來指代導(dǎo)入的函數(shù)和變量。
導(dǎo)入數(shù)據(jù)被保存在IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)中,。對應(yīng)著導(dǎo)入表的數(shù)據(jù)目錄項就指向由這個結(jié)構(gòu)組成的數(shù)組,。每個IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)都與一個導(dǎo)入的可執(zhí)行文件對應(yīng),。這個數(shù)組的最后一個元素的所有域都被設(shè)置為0。下表是這個結(jié)構(gòu)的內(nèi)容:
大小
描述
DWORD
OriginalFirstThunk
這個域的命名太不恰當(dāng),。它包含導(dǎo)入名稱表的RVA,。導(dǎo)入名稱表是一個IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組。這個域被設(shè)置為0表示IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)數(shù)組的結(jié)尾,。
DWORD
TimeDateStamp
如果可執(zhí)行文件并未綁定導(dǎo)入的DLL,,這個域的值為0。當(dāng)使用老的綁定類型進(jìn)行綁定(參考“綁定”一節(jié))時,,這個域包含日期/時間戳,。當(dāng)使用新的綁定類型進(jìn)行綁定時,這個域的值為-1,。
DWORD
ForwarderChain
這是首個轉(zhuǎn)發(fā)的函數(shù)的索引,。如果沒有轉(zhuǎn)發(fā)的函數(shù),這個域被設(shè)置為-1,。它僅用于老的綁定類型,,因為那種綁定類型不能很有效地處理轉(zhuǎn)發(fā)的函數(shù)。
DWORD
Name
導(dǎo)入的DLL名稱字符串(ASCII碼格式)的RVA,。
DWORD
FirstThunk
導(dǎo)入地址表的RVA,。IAT是一個IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組。
每個IMAGE_IMPORT_DESCRIPTOR結(jié)構(gòu)指向兩個數(shù)組,,這兩個數(shù)組實際上是一樣的,。它們有好幾種叫法,但最常用的名稱是導(dǎo)入地址表(Import Address Table,,IAT)和導(dǎo)入名稱表(Import Name Talbe,,INT)。下圖顯示的是可執(zhí)行文件從USER32.DLL中導(dǎo)入一些API時的情況,。
    這兩個數(shù)組的元素均為IMAGE_THUNK_DATA類型的結(jié)構(gòu),,這個結(jié)構(gòu)是一個與指針大小相同的共用體(或者稱為聯(lián)合),。每個IMAGE_THUNK_DATA結(jié)構(gòu)對應(yīng)著從可執(zhí)行文件中導(dǎo)入的一個函數(shù),。這兩個數(shù)組最后都以一個值為0的IMAGE_THUNK_DATA結(jié)構(gòu)作為結(jié)尾。這個共用體(實際是一個DWORD值)可以有如下幾種含義:
DWORD ForwarderString;// 轉(zhuǎn)發(fā)函數(shù)字符串的RVA(見上文)
DWORD Function;       // 導(dǎo)入函數(shù)的內(nèi)存地址
DWORD Ordinal;        // 導(dǎo)入函數(shù)的序數(shù)
DWORD AddressOfData; // IMAGE_IMPORT_BY_NAME和導(dǎo)入函數(shù)名稱的RVA(見下文)
IAT中的IMAGE_THUNK_DATA結(jié)構(gòu)的用途可以分為兩種,。在可執(zhí)行文件中,,它們或者是導(dǎo)入函數(shù)的序數(shù),或者是一個IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的RVA,。IMAGE_IMPORT_BY_NAME結(jié)構(gòu)只是一個WORD類型的值,,它后面跟著導(dǎo)入函數(shù)的名稱字符串。這個WORD類型的值是一個“提示(hint)”,,它提示加載器導(dǎo)入函數(shù)的序號可能是什么,。當(dāng)加載器加載可執(zhí)行文件時,它用導(dǎo)入函數(shù)的實際地址來覆蓋IAT中的每個元素。這一點是理解下文的關(guān)鍵,。我強烈建議你讀一讀本期雜志中Russell Osterlund的文章——揭開Windows加載器的神秘面紗,,這篇文章詳細(xì)講述了Windows加載器的行為。
在可執(zhí)行文件被加載之前,,是否存在一種方法能夠區(qū)分IMAGE_THUNK_DATA結(jié)構(gòu)中到底包含的是導(dǎo)入函數(shù)的序數(shù)呢,,還是IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的RVA呢?答案在IMAGE_THUNK_DATA結(jié)構(gòu)的最高位,。如果它為1,,那么低31位(在64位可執(zhí)行文件中是低63位)中是導(dǎo)入函數(shù)的序數(shù)。如果最高位為0,,那么IMAGE_THUNK_DATA結(jié)構(gòu)的值就是IMAGE_IMPORT_BY_NAME結(jié)構(gòu)的RVA,。
另一個數(shù)組INT,本質(zhì)上與IAT是一樣的,。它也是一個IMAGE_THUNK_DATA結(jié)構(gòu)數(shù)組,。關(guān)鍵的區(qū)別在于當(dāng)加載器將可執(zhí)行文件加載進(jìn)內(nèi)存時,它并不覆蓋INT,。為什么對于從DLL中導(dǎo)入的每組API都需要有兩個并列的數(shù)組呢,?答案在于一個稱為綁定(binding)的概念。當(dāng)在綁定過程(后面我會講到)中覆蓋可執(zhí)行文件的IAT時,,需要以某種方式保存原來的信息,。而作為這個信息的副本的INT,正是這個用途,。
INT對于可執(zhí)行文件的加載并不是必需的,。但是如果它不存在的話,那么這個可執(zhí)行文件就不能被綁定,。Microsoft鏈接器總是生成INT,,但是長期以來,Borland鏈接器(TLINK)都不生成它,。這樣,,由Borland鏈接器生成的可執(zhí)行文件就不能被綁定。
在早期的Microsoft鏈接器中,,導(dǎo)入節(jié)并不是專門針對于鏈接器的,。組成可執(zhí)行文件導(dǎo)入節(jié)的所有數(shù)據(jù)都來自導(dǎo)入庫。你可以對一個導(dǎo)入庫文件運行DUMPBIN或PEDUMP來看一下,。你會發(fā)現(xiàn)一些節(jié)名類似于.idata$3和.idata$4的節(jié),。鏈接器只是簡單地遵守它的規(guī)則來組合節(jié),所有的結(jié)構(gòu)和數(shù)組就神奇般地各就其位了,。幾年前Microsoft引進(jìn)了一種新的導(dǎo)入庫格式,,這種導(dǎo)入庫特別小,,以便讓鏈接器能在創(chuàng)建導(dǎo)入數(shù)據(jù)時更具主動性。
綁定
當(dāng)可執(zhí)行文件被綁定時(例如通過Bind程序),,其IAT中的IMAGE_THUNK_DATA結(jié)構(gòu)中是導(dǎo)入函數(shù)的實際地址,。也就是說,磁盤上的可執(zhí)行文件的IAT中存儲的就是其導(dǎo)入的DLL中的函數(shù)在內(nèi)存中的實際地址,。當(dāng)加載一個被綁定的可執(zhí)行文件時,,Windows加載器可以跳過查找每個導(dǎo)入函數(shù)并覆蓋IAT這一步。因為IAT中已經(jīng)是正確的地址了,。但是這只有正確對齊時才行,。我在2000年5月的Under the Hood專欄中講了一些測試標(biāo)準(zhǔn),你可以通過它們來確定綁定可執(zhí)行文件能夠?qū)虞d性能有多大提高,。
你也許會懷疑將可執(zhí)行文件綁定是否保險,。你可能會想,如果綁定了可執(zhí)行文件,,但它導(dǎo)入的DLL發(fā)生了變化,,這時怎么辦呢?當(dāng)這種情況發(fā)生時,,IAT中的地址已經(jīng)失效了,。加載器會檢查這種情況并隨機應(yīng)變。如果IAT中的地址已經(jīng)失效,,加載器會根據(jù)INT中的信息重新解析導(dǎo)入函數(shù)的地址,。
在安裝程序時對其進(jìn)行綁定應(yīng)該是最可能發(fā)生的情況了。Windows Installer中的BindImage這個動作可以替你做這件事,。同樣,,IMAGEHLP.DLL中也提供了BindImageEx這個API。不管用哪一種方法,,綁定都是個比較好的做法,。如果加載器確定綁定信息是有效的,那么可執(zhí)行文件就會被加載的更快,。如果綁定信息失效,,它也并不會比不綁定效果差。
對加載器來說,,使綁定生效的一個關(guān)鍵步驟是確定IAT中的綁定信息是否有效,。當(dāng)可執(zhí)行文件被綁定時,,有關(guān)它導(dǎo)入的DLL的信息也被放在可執(zhí)行文件中,。加載器檢查這個信息以快速確定綁定的有效性。在綁定的最初實現(xiàn)中并未添加這個信息,,因此可執(zhí)行文件可能按老的綁定方式進(jìn)行綁定,,或者按新的綁定方式進(jìn)行綁定,。我在這里講的是新的綁定方式。
確定綁定信息有效性的一個關(guān)鍵數(shù)據(jù)結(jié)構(gòu)是IMAGE_BOUND_IMPORT_DESCRIPTOR,。被綁定的可執(zhí)行文件中有一個此結(jié)構(gòu)的列表,。每個IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)表示一個綁定到的DLL的日期/時間戳。這個列表的RVA由數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT的元素給出,。IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)中的成員如下:
  • TimeDateStamp,,這是包含導(dǎo)入的DLL的日期/時間戳的一個DWORD類型的值。

  • OffsetModuleName,,這是包含導(dǎo)入的DLL的名稱字符串偏移地址的一個WORD類型                  的值,。這個域是相對于首個IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)的偏移(而不是RVA)。

  • NumberOfModuleForwarderRefs,,這是一個WORD類型的值,,它包含緊跟在這個結(jié)構(gòu)后面的IMAGE_BOUND_FORWARDER_REF結(jié)構(gòu)的數(shù)目。除了最后一個WORD類型的成員(NumberOfModuleForwarderRefs)是保留的外,,IMAGE_BOUND_FORWARDER_REF結(jié)構(gòu)與IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)一樣,。
一般情況下,每個導(dǎo)入的DLL對應(yīng)的IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)簡單地組成一個數(shù)組,。但是當(dāng)綁定的API轉(zhuǎn)發(fā)到了另一個DLL上時,,這個轉(zhuǎn)發(fā)到的DLL的有效性也需要檢查。在這種情況下,,IMAGE_BOUND_FORWARDER_REF結(jié)構(gòu)就與IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu)交叉在了一起,。下面舉一個例子來說明。
假設(shè)你鏈接到了KERNEL32.DLL中的HeapAlloc這個API上,,而它實際上被轉(zhuǎn)發(fā)到了NTDLL中的RtlAllocateHeap上,,然后你綁定這個可執(zhí)行文件。那么在這個可執(zhí)行文件中,,對應(yīng)于KERNEL32.DLL這個導(dǎo)入的DLL就有一個相應(yīng)的IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu),,同時它后面是一個對應(yīng)于NTDLL.DLL的IMAGE_BOUND_FORWARDER_REF結(jié)構(gòu)。緊跟在它們后面的可能是與你導(dǎo)入并綁定到的其它DLL對應(yīng)的IMAGE_BOUND_IMPORT_DESCRIPTOR結(jié)構(gòu),。
延遲加載數(shù)據(jù)
前面我已經(jīng)講過延遲加載(Delayload)一個DLL就是隱含導(dǎo)入與通過LoadLibrary和GetProcAddress顯式導(dǎo)入這兩種方式的混合?,F(xiàn)在讓我們來看一下延遲加載所需的數(shù)據(jù)結(jié)構(gòu)以及它的工作原理。
一定要記住延遲加載并不是操作系統(tǒng)的功能,。它完全是由鏈接器和運行時庫添加的附加代碼和數(shù)據(jù)來實現(xiàn)的,。正因為如此,WINNT.H中并沒有幾個地方涉及到延遲加載,。但是你會發(fā)現(xiàn)延遲加載數(shù)據(jù)和正常導(dǎo)入數(shù)據(jù)二者的定義是平行的,。
DataDirectory中的IMAGE_DIRECTORY_ENTRY_DELAY_IMPORT元素指向延遲加載數(shù)據(jù)。這個元素中實際是一個ImgDelayDescr結(jié)構(gòu)數(shù)組的RVA,,這個結(jié)構(gòu)被定義在Visual C++的DelayImp.H文件中,。下表是這個結(jié)構(gòu)的內(nèi)容,。對應(yīng)于每一個導(dǎo)入的DLL都有一個相應(yīng)的ImgDelayDescr結(jié)構(gòu)。
大小
描述
DWORD
grAttrs
此結(jié)構(gòu)的屬性,。當(dāng)前惟一定義的標(biāo)志是dlattrRva(值為1),。這個標(biāo)志表明此結(jié)構(gòu)中的地址域是RVA,而不是虛擬地址,。設(shè)置這個標(biāo)志表明延遲加載描述符是VC7.0或其后續(xù)版本,。
RVA
rvaDLLName
導(dǎo)入的DLL的名稱字符串的RVA。這個字符串被傳遞給LoadLibrary函數(shù),。
RVA
rvaHmod
一塊HMODULE大小的內(nèi)存的RVA,。當(dāng)延遲加載的DLL被加載進(jìn)內(nèi)存時,它的HMODULE被存儲在這個位置,。
RVA
rvaIAT
此DLL的導(dǎo)入地址表的RVA,。它的格式與正常的IAT相同。
RVA
rvaINT
此DLL的導(dǎo)入名稱表的RVA,。它的格式與正常的INT相同,。
RVA
rvaBoundIAT
可選的綁定IAT的RVA。它是此DLL的導(dǎo)入地址表的一個綁定副本的RVA,。它的格式與正常的IAT相同,。當(dāng)前這個IAT副本并未綁定,但這個功能可能被添加到將來的BIND程序中,。
RVA
rvaUnloadIAT
原始的IAT的可選副本的RVA,。它是此DLL的導(dǎo)入地址表的一個未綁定的副本的RVA。它的格式與正常的IAT相同,。當(dāng)前總是設(shè)置為0,。
DWORD
dwTimeStamp
延遲加載導(dǎo)入的DLL的日期/時間戳。通常設(shè)置為0,。
我們從ImgDelayDescr結(jié)構(gòu)中可以獲取的主要內(nèi)容就是它包含了DLL的IAT和INT的地址,。這些表與正常情況下的表是一樣的,只不過它們是由運行時庫代碼進(jìn)行讀寫而不是由操作系統(tǒng),。當(dāng)你調(diào)用延遲加載的DLL中的函數(shù)時,,運行時庫代碼就調(diào)用LoadLibrary加載相應(yīng)的DLL(如果需要的話),然后調(diào)用GetProcAddress來獲取函數(shù)地址,,最后將獲取的地址存儲在延遲加載IAT中,,以便將來可以直接調(diào)用這個函數(shù)。
延遲加載所使用的數(shù)據(jù)結(jié)構(gòu)在設(shè)計時有一個失誤的地方需要解釋一下,。在Visual C++ 6.0中——這是它最初的形式,,ImgDelayDescr結(jié)構(gòu)中的所有包含地址的域使用的都是虛擬地址,而不是RVA。也就是說,,它們包含了延遲加載數(shù)據(jù)所在位置的實際地址。這些域都是DWORD類型的,,也就是x86上一個指針的大小,。
現(xiàn)在要全面支持IA-64了。突然,,4字節(jié)已經(jīng)不夠保存一個完整的地址了,。哎呀!在這個時候,,Microsoft做了一件正確的事,,把包含地址的域都改為包含RVA了。如前面所示,,我使用的是已經(jīng)修訂過的結(jié)構(gòu)定義和名稱,。
還有一個問題就是確定ImgDelayDescr使用的是RVA還是虛擬地址。這個結(jié)構(gòu)中有一個域包含了相關(guān)的標(biāo)志,。當(dāng)grAttrs域為1時,,這個結(jié)構(gòu)中的成員中包含的是RVA。從Visual Studio .NET和64位編譯器開始,,這是惟一選項,。如果grAttrs不是1,ImgDelayDescr結(jié)構(gòu)中的域包含的都是虛擬地址,。
資源節(jié)
在PE文件的所有節(jié)中,,在資源節(jié)中定位數(shù)據(jù)是最復(fù)雜的。在這里我只講述一些獲取諸如圖標(biāo),、位圖以及對話框之類的資源的原始數(shù)據(jù)所需的一些數(shù)據(jù)結(jié)構(gòu),。我不涉及它們的實際格式,那已經(jīng)超出了本文的范圍,。
資源可以在一個叫做.rsrc的節(jié)中找到,。DataDirectory中索引為IMAGE_DIRECTORY_ENTRY_RESOURCE的元素包含了資源的RVA和大小。由于多方面的原因,,資源被組織得與文件系統(tǒng)類似——有目錄和葉結(jié)點,。
DataDirectory中的資源指針指向了一個IMAGE_RESOURCE_DIRECTORY類型的結(jié)構(gòu)。這個結(jié)構(gòu)中包含了目前尚未使用的Characteristics域,、TimeDateStamp域以及版本號域(MajorVersionMinorVersion),。這個結(jié)構(gòu)中真正有用的域是NumberOfNamedEntries和NumberOfIdEntries。
每個IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)后面是一個IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)數(shù)組,。另外,,IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)中的NumberOfNamedEntries和NumberOfIdEntries這兩個域保存的就是這個數(shù)組中IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)的數(shù)目。(如果你感覺這些數(shù)據(jù)結(jié)構(gòu)的名稱讓你看得頭疼,,說句實在話,,我將它們寫下來也挺難受的?。?/span>
每個目錄項(即IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu))或者指向另一個資源目錄,或者指向具體的資源數(shù)據(jù),。當(dāng)它指向另一個資源目錄時,,這個結(jié)構(gòu)中的第二個DWORD的最高位為1,其余的31位是那個資源目錄的偏移,。這個偏移是相對于資源節(jié)開頭來說的,,而不是RVA。
當(dāng)它指向?qū)嶋H的某種資源時,,第二個DWORD的最高位為0,,其余的31位是具體資源(例如對話框)的偏移。同上面一樣,,這個偏移同樣是相對于資源節(jié)開頭來說的,,而不是RVA。
每個目錄項可以通過名稱或者ID值來標(biāo)識,。它們就是你在.RC文件中為具體資源指定的名稱或ID值,。當(dāng)目錄項的第一個DWORD的最高位為1時,其余的31位是資源名稱(字符串)的偏移,;如果最高位為0,,那么其低16位是資源標(biāo)識(ID)的值。
理論已經(jīng)足夠了,!現(xiàn)在讓我們看一個實際的例子,。下面是PEDUMP輸出的ADVAPI32.DLL的資源節(jié)的部分內(nèi)容:
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”開頭的每一行對應(yīng)于一個IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)?!癛esDir“后面的括號中是資源目錄的名稱,。在這個例子中,資源目錄的名稱分別為0,、MOFDATA,、MOFRESOURCENAME、STRING,、C36,、RCDATA和66。名稱后面是以名稱標(biāo)識的和以ID標(biāo)識的資源目錄的總個數(shù)(后面的括號中是它們分別的個數(shù)),。在這個例子中,,頂級目錄一共有3個直接的子目錄,所有其它目錄都只有一個下級子目錄,。
頂級目錄類似于文件系統(tǒng)中的根目錄,。根目錄下的每個子目錄項(也就是第二級目錄)代表資源的類型(字符串表、對話框、菜單等等),。它們下面還有第三級子目錄,。
對于某種具體的資源類型來說,一般有三級目錄,。例如如果有五個對話框,,那么第二級的DIALOG目錄下面將會有五個子目錄項。這五個子目錄項本身也都是目錄,。在這五個目錄下面都只有一項內(nèi)容,,它就是具體資源的原始數(shù)據(jù)的偏移地址,。很簡單,,不是嗎?
如果你更喜歡通過讀源代碼來學(xué)習(xí)的話,,你可以仔細(xì)看一下PEDUMP中轉(zhuǎn)儲資源的那部分代碼(PEDUMP的源代碼可以從2002年2月本文的第一部分中下載),。除了顯示所有的資源目錄以及它們的元素個數(shù)外,PEDUMP還可以顯示幾種常見的資源類型,,例如對話框等,。
基址重定位
在可執(zhí)行文件中的許多地方,你都會發(fā)現(xiàn)內(nèi)存地址的蹤跡,。當(dāng)鏈接器在生成可執(zhí)行文件時,,它假定這個可執(zhí)行文件會被加載到內(nèi)存中的某一個地址處(即首選地址)。只有在可執(zhí)行文件被加載到其首選地址時,,所有這些內(nèi)存地址才是正確的,。這個首選地址由IMAGE_FILE_HEADER結(jié)構(gòu)中的ImageBase域給出。
如果加載器由于某種原因需要把可執(zhí)行文件加載到其它地址處時,,所有這些地址都變成不正確的了,。這將會額外增加加載器的工作量。在2000年5月的Under The Hood專欄(前面已經(jīng)提到)中我已經(jīng)講過當(dāng)幾個DLL首選加載地址相同時會導(dǎo)致性能損失,,以及如何使用REBASE工具來解決這個問題,。
基址重定位(Base Relocations)信息告訴加載器可執(zhí)行文件不能被加載到其首選地址時需要進(jìn)行修改的每一個位置。對于加載器來說,,幸運的是它并不需要知道地址使用的細(xì)節(jié)問題,。它只知道有一個地址列表,其中的每一個地址都需要以同樣的方式進(jìn)行修改,。
讓我們來看一個x86平臺上的可執(zhí)行文件的例子,。假設(shè)有以下指令,它將一個全局變量(地址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處,,這條指令當(dāng)然可以正確執(zhí)行,。但是現(xiàn)在我們假設(shè)它被加載到了0x00500000處。如果真是這樣,,那么這條指令的最后的四個字節(jié)需要被改成0x0050D434,。
那么加載器是如何做的呢?它比較首選加載地址與實際加載地址,,然后計算出△(delta,,音譯為德耳塔,數(shù)學(xué)中的常用符號,,表示差值的意思),。在這個例子中,△為0x00100000,。這個△被加到變量原來的地址值(大小為DWORD)上,,形成新的地址。在前面的例子中,,關(guān)于地址0x00401022處,,即指令中的DWORD值處,將會有一個相應(yīng)的重定位信息,。
簡而言之,,基址重定位信息只是可執(zhí)行文件中的一個地址列表,當(dāng)加載進(jìn)內(nèi)存時,,這些地址中的值都要再加上△,。為了提高系統(tǒng)性能,可執(zhí)行文件的頁面只有在需要時才會被加載進(jìn)內(nèi)存(可執(zhí)行文件的加載與內(nèi)存映射文件類似),,基址重定位信息的格式就反映了這個特性,。基址重定位信息所在的節(jié)通常被稱為.reloc節(jié),,但是查找它的正確方法是通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_BASERELOC的那個元素,。
基址重定位信息是一些非常簡單的IMAGE_BASE_RELOCATION結(jié)構(gòu)。此結(jié)構(gòu)中的VirtualAddress域包含了需要進(jìn)行重定位的內(nèi)存范圍的起始RVA,。SizeOfBlock域給出了重定位信息的大小,,其中包括IMAGE_BASE_RELOCATION自身的大小。
緊跟著IMAGE_BASE_RELOCATION結(jié)構(gòu)后面是一組可變數(shù)目的WORD值,。這些WORD值的數(shù)目可以從IMAGE_BASE_RELOCATION結(jié)構(gòu)的SizeOfBlock域推出,。其中每個WORD值由兩部分組成,。高4位指明了重定位的類型,由WINNT.H中的一系列IMAGE_REL_BASED_xxx值給出,。低12位是相對于IMAGE_BASE_RELOCATION結(jié)構(gòu)的VirtualAddress域的偏移,,這是應(yīng)該進(jìn)行重定位的地方。
在前面那個關(guān)于基址重定位的例子中,,我把情況簡化了,。實際上有多種類型的重定位方式。對于x86平臺上的可執(zhí)行文件來說,,所有的重定位類型都是IMAGE_REL_BASED_HIGHLOW,。你經(jīng)常會在一組重定位信息之后看到類型為IMAGE_REL_BASED_ABSOLUTE的重定位信息。它們實際上并沒有什么作用,,只是為了填充空間以便下一個IMAGE_BASE_RELOCATION結(jié)構(gòu)能夠按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文件是最先被加載到進(jìn)程的地址空間中的,,因此可以絕對保證它能被加載到其首選地址上,。DLL就沒有這么幸運了,因此DLL中總是存在基址重定位信息,,除非你使用/FIXED鏈接器選項明確忽略它們,。在Visual Studio .NET中,鏈接器在生成調(diào)試版和發(fā)行版的EXE文件時都不產(chǎn)生基址重定位信息,。
調(diào)試目錄
當(dāng)創(chuàng)建可執(zhí)行文件并生成相應(yīng)的調(diào)試信息時,,通常文件中會包含這種信息格式的細(xì)節(jié)以及它的位置。操作系統(tǒng)運行可執(zhí)行文件時并不需要調(diào)試信息,,但它對于開發(fā)工具非常有用,。一個EXE文件可以包含多種格式的調(diào)試信息,調(diào)試目錄(Debug Directory)結(jié)構(gòu)指出哪種格式可用,。
可以通過數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_DEBUG的元素找到調(diào)試目錄,。它是由IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)組成的數(shù)組,其中每一個結(jié)構(gòu)對應(yīng)一種類型的調(diào)試信息,,如下表所示,。調(diào)試目錄中元素的數(shù)目可以使用數(shù)據(jù)目錄中的Size域計算得出,。
大小
描述
DWORD
Characteristics
未用,設(shè)置為0,。
DWORD
TimeDateStamp
調(diào)試信息的日期/時間戳,。
WORD
MajorVersion
調(diào)試信息的主版本號,未用,。
WORD
MinorVersion
調(diào)試信息的次版本號,,未用。
DWORD
Type
調(diào)試信息的類型,。以下是經(jīng)常遇到的類型:
IMAGE_DEBUG_TYPE_COFF
IMAGE_DEBUG_TYPE_CODEVIEW      // 包含PDB文件
IMAGE_DEBUG_TYPE_FPO           // 幀指針省略
IMAGE_DEBUG_TYPE_MISC          // IMAGE_DEBUG_MISC
IMAGE_DEBUG_TYPE_OMAP_TO_SRC
IMAGE_DEBUG_TYPE_OMAP_FROM_SRC
IMAGE_DEBUG_TYPE_BORLAND       // Borland格式
DWORD
SizeOfData
文件中調(diào)試數(shù)據(jù)的大小,。不包括外部調(diào)試文件(例如.PDB文件)的大小。
DWORD
AddressOfRawData
當(dāng)映射進(jìn)內(nèi)存時調(diào)試數(shù)據(jù)的RVA,。如果調(diào)試信息不被映射,,它被設(shè)置為0。
DWORD
PointerToRawData
調(diào)試數(shù)據(jù)的文件偏移(不是RVA),。
到目前為止,,最流行的調(diào)試信息格式是PDB文件。PDB文件實質(zhì)上是CodeView格式調(diào)試信息的發(fā)展,。一個類型為IMAGE_DEBUG_TYPE_CODEVIEW的調(diào)試目錄標(biāo)志著PDB信息的存在,。如果你檢查由這個元素指向的數(shù)據(jù),會發(fā)現(xiàn)一個短的CodeView格式的頭部,。這個調(diào)試數(shù)據(jù)主要是一個外部PDB文件的路徑,。在Visual Studio 6.0中,調(diào)試頭開始處是一個NB10簽名,。在Visual Studio .NET中,,這個頭開始處是RSDS。
在Visual Studio 6.0中,,可以使用/DEBUGTYPE:COFF鏈接器選項來生成COFF調(diào)試信息,。Visual Studio .NET將這項功能移除了。對于經(jīng)過優(yōu)化的x86代碼,,由于函數(shù)可能沒有正常的棧幀,,所有使用幀指針省略(Frame Pointer Omission,F(xiàn)PO)調(diào)試信息,。FPO數(shù)據(jù)允許調(diào)試器定位局部變量和參數(shù),。
有兩種OMAP調(diào)試信息僅用于Microsoft的程序。Microsoft內(nèi)部使用一種工具對可執(zhí)行文件中的代碼進(jìn)行重新排列以減少分頁,。(它所做的不僅僅是Working Set Tuner所能做到的,。)OMAP信息讓工具可以在調(diào)試信息中的原始地址與重排后的代碼中的新地址之間進(jìn)行轉(zhuǎn)換。
順便說一下,,DBG文件也包含了一個類似于我上面講的調(diào)試目錄,。DBG文件流行于Windows NT 4.0時代,,它們主要包含COFF調(diào)試信息,但是Windows XP偏愛PDB文件而將它們淘汰了,。
.NET頭部
對于開發(fā)工具生成的用于Microsoft .NET環(huán)境下的可執(zhí)行文件來說,,它們首先是PE文件。但是在大多數(shù)情況下.NET文件中正常的代碼和數(shù)據(jù)是微不足道的,。.NET可執(zhí)行文件的主要目的是將.NET特定的信息,,例如元數(shù)據(jù)和中間語言(IL),加載進(jìn)內(nèi)存,。另外.NET可執(zhí)行文件鏈接到了MSCOREE.DLL文件上,。這個DLL是.NET進(jìn)程的起點。當(dāng)加載.NET可執(zhí)行文件時,,它的入口點通常是一個小的占位程序,。這個占位程序只是跳轉(zhuǎn)到MSCOREE.DLL的一個導(dǎo)出函數(shù)(_CorExeMain或_CorDllMain)上。從那里開始,,MSCOREE獲取控制權(quán),,開始使用可執(zhí)行文件中的元數(shù)據(jù)和IL。這類似于(.NET版之前的)Visual Basic中的應(yīng)用程序使用MSVBVM60.DLL所采用的方式,。.NET信息的起點是IMAGE_COR20_HEADER結(jié)構(gòu),,它當(dāng)前被定義在.NET Framework SDK中的CorHDR.H文件以及最新的WINNT.H文件中。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR的項指向IMAGE_COR20_HEADER結(jié)構(gòu),。下表列出了IMAGE_COR20_HEADER結(jié)構(gòu)中的域,。關(guān)于IMAGE_COR20_HEADER指向的元數(shù)據(jù),、方法IL以及其它內(nèi)容將在后續(xù)文件中詳細(xì)講述,。

類型
描述
DWORD
cb
頭部的大小(以字節(jié)計),。
WORD
MajorRuntimeVersion
運行這個程序所需的運行時組件的最小版本號,。對于第一個發(fā)行的.NET Framework而言,此值為2,。
WORD
MinorRuntimeVersion
次版本號,,當(dāng)前為0。
IMAGE_DATA_DIRECTORY
MetaData
元數(shù)據(jù)表的RVA,。
DWORD
Flags
包含這個映像屬性的標(biāo)志,。當(dāng)前定義了以下值:
COMIMAGE_FLAGS_ILONLY
// 映像僅包含IL代碼,并不需要運// 行于特定CPU上
COMIMAGE_FLAGS_32BITREQUIRED // 僅運行于32位處理器上
COMIMAGE_FLAGS_IL_LIBRARY
STRONGNAMESIGNED
// 映像已經(jīng)用散列數(shù)據(jù)簽名
COMIMAGE_FLAGS_TRACKDEBUGDATA
// 讓JIT或運行時組件為方法保持// 調(diào)試信息
DWORD
EntryPointToken
映像入口點的MethodDef的記號,。.NET運行時調(diào)用這個方法開始托管執(zhí)行,。
IMAGE_DATA_DIRECTORY
Resources
.NET資源的RVA和大小。
IMAGE_DATA_DIRECTORY
StrongNameSignature
強名稱散列數(shù)據(jù)的RVA,。
IMAGE_DATA_DIRECTORY
CodeManagerTable
代碼管理器表的RVA,。代碼管理器包含獲取正在運行的程序的狀態(tài)(例如堆棧跟蹤和跟蹤GC引用)所需的代碼,。
IMAGE_DATA_DIRECTORY
VTableFixups
需要被修正的函數(shù)指針組成的數(shù)組。用于支持非托管的C++虛表,。
IMAGE_DATA_DIRECTORY
ExportAddressTableJumps
由對應(yīng)于導(dǎo)出符號的JMP形實轉(zhuǎn)換塊被寫入的位置(RVA)組成的數(shù)組的RVA,。這些形實轉(zhuǎn)換塊允許托管方法被導(dǎo)出,這樣非托管代碼可以調(diào)用它們,。
IMAGE_DATA_DIRECTORY
ManagedNativeHeader
在內(nèi)存中供.NET運行時組件內(nèi)部使用,。在可執(zhí)行文件中被設(shè)置為0。
TLS初始化
當(dāng)使用__declspec(thread)定義線程局部變量時,,編譯器將它們放入一個名為.tls的節(jié)中,。當(dāng)系統(tǒng)創(chuàng)建新線程時,它從進(jìn)程堆中分配內(nèi)存來保存用于新線程的線程局部變量,。這部分內(nèi)存使用.tls節(jié)中的值進(jìn)行初始化,。系統(tǒng)將分配的內(nèi)存的地址保存在TLS數(shù)組中,F(xiàn)S:[2Ch]指向這個數(shù)組(在x86平臺上),。
如果數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_TLS的元素不為0,,那就表示可執(zhí)行文件中存在線程局部存儲(TLS)。而這個元素指向一個IMAGE_TLS_DIRECTORY結(jié)構(gòu),,如下表所示,。
大小
描述
DWORD
StartAddressOfRawData
用于在內(nèi)存中初始化新線程的TLS數(shù)據(jù)的一段內(nèi)存的起始地址。
DWORD
EndAddressOfRawData
用于在內(nèi)存中初始化新線程的TLS數(shù)據(jù)的一段內(nèi)存的結(jié)束地址,。
DWORD
AddressOfIndex
當(dāng)可執(zhí)行文件被加載進(jìn)內(nèi)存時,,如果它包含.tls節(jié),加載器調(diào)用TlsAlloc給它分配一個TLS句柄,,并將分配的句柄保存在這個域指定的位置處,。運行時庫使用這個句柄定位線程局部數(shù)據(jù)。
DWORD
AddressOfCallBacks
由PIMAGE_TLS_CALLBACK類型的函數(shù)指針組成的數(shù)組的地址,。當(dāng)創(chuàng)建或撤銷線程時,,這個列表中的每個函數(shù)都會被調(diào)用。最后一個元素的值為0,,它標(biāo)志著表的結(jié)尾,。一般由Visual C++生成的可執(zhí)行文件中這個表是空的。
DWORD
SizeOfZeroFill
已初始化數(shù)據(jù)中除了由StartAddressOfRawDataEndAddressOfRawData域組成的已初始化數(shù)據(jù)界限之外的大?。ㄒ宰止?jié)計),。所有超出這個范圍的用于單個線程的數(shù)據(jù)都被初始化為0。
DWORD
Characteristics
保留,,當(dāng)前被設(shè)置為0,。
注意到IMAGE_TLS_DIRECTORY中的地址都是虛擬地址而不是RVA這一點很重要。因此如果可執(zhí)行文件不能被加載到其首選加載地址時,,它們都要進(jìn)行基址重定位,。同時,,IMAGE_TLS_DIRECTORY結(jié)構(gòu)本身并不在.tls節(jié)中,它位于.rdata節(jié)中,。
程序異常數(shù)據(jù)
      一些平臺(包括IA-64)并不使用x86平臺上的基于幀的異常處理,,它們使用的是基于表的異常處理。在這種異常處理中有一個表,,它包含了可能會被異常展開(unwinding)影響到的每一個函數(shù)的信息,。這些信息主要包括每個函數(shù)的開始地址、結(jié)束地址以及在哪里并如何處理異常,。當(dāng)發(fā)生異常時,,系統(tǒng)搜索整個表來尋找處理它的相應(yīng)項并處理。異常表是一個由IMAGE_RUNTIME_FUNCTION_ENTRY結(jié)構(gòu)組成的數(shù)組,。數(shù)據(jù)目錄中索引為IMAGE_DIRECTORY_ENTRY_EXCEPTION的元素引向此數(shù)組,。這個結(jié)構(gòu)的格式因平臺而異。對于IA-64平臺,,它的結(jié)構(gòu)如下:
DWORD BeginAddress;
DWORD EndAddress;
DWORD UnwindInfoAddress;
UnwindInfoAddress數(shù)據(jù)的結(jié)構(gòu)并未在WINNT.H文件中給出,。但是它的具體格式可以在Intel的"IA-64 Software Conventions and Runtime Architecture Guide"一書第11章中找到。
PEDUMP程序
      現(xiàn)在我的PEDUMP程序與1994年時的相比已經(jīng)有了很大改進(jìn),。它可以顯示本文中講的所有結(jié)構(gòu),,其中包括:
  • IMAGE_NT_HEADERS
  • 導(dǎo)入表/導(dǎo)出表
  • 資源
  • 基址重定位
  • 調(diào)試目錄
  • 延遲導(dǎo)入表
  • 綁定導(dǎo)入描述符
  • IA-64異常處理表
  • TLS初始化數(shù)據(jù)
  • .NET運行時頭
除了可以轉(zhuǎn)儲可執(zhí)行文件外,PEDUMP還可以轉(zhuǎn)儲COFF格式的OBJ文件,、COFF導(dǎo)入庫(新格式以及老格式),、COFF符號表和DBG文件。
PEDUMP是一個命令行程序,。對前面提到的各種文件運行PEDUMP時,,如果不加任何選項,它默認(rèn)輸出的是最有用的數(shù)據(jù)結(jié)構(gòu)信息,。有好幾個命令行選項可以用來添加其它的輸出信息:
名稱
描述
/A
轉(zhuǎn)儲所有內(nèi)容
/B
顯示基址重定位信息
/H
包括每個節(jié)中原始數(shù)據(jù)的十六進(jìn)制形式
/I
包括導(dǎo)入地址表形實轉(zhuǎn)換塊的地址
/L
包括行號信息
/P
包括PDATA(運行時函數(shù))
/R
包括詳細(xì)的資源信息(字符串表和對話框)
/S
顯示符號表
關(guān)于PEDUMP的源代碼有幾個地方值得注意,。首先它可以按32位或64位可執(zhí)行文件編譯和運行。如果你手邊有Itanium機器可以試一下,。另外,無論PEDUMP以何種方式編譯,,它都可以同時轉(zhuǎn)儲32位和64位文件,。換句話說,32位版的PEDUMP可以轉(zhuǎn)儲32位和64位文件,,64位版的PEDUMP也可以轉(zhuǎn)儲32位和64位文件,。
在考慮使PEDUMP可以同時處理32位和64位文件時,我想避免為32位結(jié)構(gòu)和64位結(jié)構(gòu)分別寫一個函數(shù),。因此我使用了C++模板,。
在好幾個文件(特別是EXEDUMP.CPP)中,,你都會發(fā)現(xiàn)各種模板函數(shù)。大多數(shù)情況下,,模板函數(shù)的參數(shù)最終會被擴展為IMAGE_NT_HEADERS32結(jié)構(gòu)或 IMAGE_NT_HEADERS64結(jié)構(gòu),。當(dāng)調(diào)用這些函數(shù)時,由代碼自身確定是32位還是64位文件并用相應(yīng)參數(shù)類型去調(diào)用相應(yīng)的函數(shù),,引起相應(yīng)的模板展開,。
伴隨PEDUMP源代碼的還有一個Visual C++ 6.0工程文件。工程配置除了傳統(tǒng)的x86 debug和 release外,,還有相應(yīng)的64位配置,。要想使它正常工作,你需要把64位工具(當(dāng)前在Platform SDK中)的路徑添加到Tools | Options | Directories 選項卡最上面的Executable files路徑中,,同時還要設(shè)置相應(yīng)的64位Include目錄和Lib目錄的路徑,。在我的機器上這個工程文件可以正常工作,但是在你的機器上可能需要進(jìn)行少量修改才行,。
為了使PEDUMP可以處理的內(nèi)容盡可能全面,,這就需要使用最新的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位版的程序時對工程目錄進(jìn)行一些修改,。
結(jié)束語
      可移植可執(zhí)行文件格式是一種結(jié)構(gòu)非常好且相對簡單的可執(zhí)行文件格式,。特別好的一點是PE文件可以被直接映射進(jìn)內(nèi)存,這樣它在磁盤上的數(shù)據(jù)結(jié)構(gòu)與運行時Windows使用的結(jié)構(gòu)一致,。我同時非常驚奇于PE格式是如何經(jīng)受住10多年來的各種變化,,甚至包括移植到64位Windows以及.NET平臺上,對它的影響,。(PE格式的設(shè)計者竟然如此深謀遠(yuǎn)慮?。?/span>
盡管我講了PE文件許多方面的內(nèi)容,但是仍然還有一些主題我沒有涉及到,,其中包括一些標(biāo)志,、屬性以及數(shù)據(jù)結(jié)構(gòu)。我認(rèn)為它們并不常用,,因此也就沒有在這里講,。但是我希望我在這里對PE文件的講解能使你更容易理解Microsoft的PE規(guī)范。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點,。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點擊一鍵舉報,。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多