深入剖析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的工具。
導(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),,如下表所示:
導(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)容:
每個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)中的成員如下:
一般情況下,每個導(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)。
我們從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域以及版本號域(MajorVersion和MinorVersion),。這個結(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域計算得出,。
到目前為止,,最流行的調(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ì)講述,。
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),,如下表所示,。
注意到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),,其中包括:
除了可以轉(zhuǎn)儲可執(zhí)行文件外,PEDUMP還可以轉(zhuǎn)儲COFF格式的OBJ文件,、COFF導(dǎo)入庫(新格式以及老格式),、COFF符號表和DBG文件。 PEDUMP是一個命令行程序,。對前面提到的各種文件運行PEDUMP時,,如果不加任何選項,它默認(rèn)輸出的是最有用的數(shù)據(jù)結(jié)構(gòu)信息,。有好幾個命令行選項可以用來添加其它的輸出信息:
關(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ī)范。 |
|