很久以前,我開始為Microsoft Systems Journal(現(xiàn)在的MSDN(R) Magazine)寫文章,其中
有一篇名為“探索PE文件內(nèi)幕——Win32可移植可執(zhí)行文件格式之旅”的文章很受歡迎,大大超
出了我的意料。直到現(xiàn)在,我還聽說有人(甚至在Microsoft)仍然在使用那篇文章,它依舊被
收錄在MSDN Library中。不幸的是,文章的最大問題是它們是靜止的。但是Win32(R)的世界在這些
年已經(jīng)發(fā)生了很大的變化,因此那篇文章已經(jīng)嚴(yán)重過時了。我要從本月開始用兩部分系列的文章
來補救這種情況。
你可能想知道為什么要關(guān)注可執(zhí)行文件的格式。答案永遠是:操作系統(tǒng)的可執(zhí)行文件格式和
數(shù)據(jù)結(jié)構(gòu)展現(xiàn)了操作系統(tǒng)內(nèi)部許多信息。通過理解 EXE和 DLL 的內(nèi)部情況,你會發(fā)現(xiàn)你已經(jīng)變成
你周圍一個更優(yōu)秀的程序員。
當(dāng)然,通過閱讀 Microsoft的 PECOFF 規(guī)范你可以獲得許多我將要告訴你的內(nèi)容。但是與大
多數(shù)規(guī)范一樣,它更注重完整性而不是可讀性。在本文中,我把精力集中于解釋整個故事中最重
要的部分,同時填補那些并不適合出現(xiàn)在官方規(guī)范中的怎么樣(How)以及為什么(Why)的問題。
另外,在本文中我還會講到一些非常有用的內(nèi)容,它們并未出現(xiàn)在任何 Microsoft官方文檔中。
讓我先舉一些例子來說明自從1994年我寫那篇文章以來有關(guān)可執(zhí)行文件方面都發(fā)生了哪些
變化。由于16位 Windows(R)已經(jīng)成為歷史,因此沒有必要再與Win16的 NE(New Executable)格
式相比較了。另一個已經(jīng)脫離人們視野的是 Win32s(R)。在 Windows 3.1 上運行Win32 程序非常不
穩(wěn)定是最令人討厭的事。
回到當(dāng)時,Windows 95(當(dāng)時代號為“Chicago”)甚至還未發(fā)行。Windows NT(R)還是3.5
版。Microsoft 鏈接器還未進行非常有效地優(yōu)化。值得一提的是當(dāng)時已經(jīng)在MIPS和 DEC Alpha
上實現(xiàn)了 Windows NT。
自從那篇文章以來都出現(xiàn)了什么新內(nèi)容呢?64位Windows引進了它自己的變種的PE文件格
式。Windows CE 添加了許多的新型處理器。諸如 DLL延遲加載、節(jié)合并以及綁定之類的優(yōu)化已
經(jīng)鋪天蓋地。有許多新東西要加入到這個故事中。
讓我們不要忘了 Microsoft(R) .NET。該把它放在什么位置呢?對于操作系統(tǒng)來說,.NET 可
執(zhí)行文件只不過是普通的Win32 可執(zhí)行文件。但是.NET 運行時能夠識別出這些可執(zhí)行文件中的
數(shù)據(jù)并把它作為元數(shù)據(jù)(metadata)和中間語言(Intermediate Language,IL),它們對.NET
來說非常重要。在本文中,我要敲開.NET 元數(shù)據(jù)結(jié)構(gòu)的大門,但把對它全部光彩的徹底挖掘留
給下一篇文章。
如果Win32 世界中的所有這些加加減減還不足以成為我重新寫那篇文章的理由的話,那么
我只有列出原來那篇文章中的一些令我害怕的錯誤了。例如我對線程局部存儲(TLS)支持情況
的描述是錯誤的。同樣,通篇我對日期/時間戳這個DWORD 的描述僅在太平洋時區(qū)才是精確的!
另外,有許多內(nèi)容在當(dāng)時是正確的,但現(xiàn)在已經(jīng)不正確了。我說過.rdata 節(jié)并沒有太大的
作用。今天,誠然是這樣。我也說過.idata 節(jié)是可讀/可寫的節(jié),但現(xiàn)在卻有許多試圖攔截 API
的人發(fā)現(xiàn)它在很多情況下都是不正確的。
伴隨著在這篇文章中完全更新 PE文件格式的故事,我也對用于顯示 PE文件內(nèi)容的PEDUMP
程序進行了徹底修改。PEDUMP 現(xiàn)在可以在 x86和 IA-64 平臺上編譯和運行,并且能夠轉(zhuǎn)儲32 位
和 64位PE 文件。最重要的是,PEDUMP的源代碼可以從本文開頭的鏈接處下載。這樣,你就有
了一個用這里講的概念和數(shù)據(jù)結(jié)構(gòu)實際工作的例子。
PE 文件格式概覽
Microsoft引進了PE 文件格式,更經(jīng)常被稱為PE 格式,作為最初的 Win32規(guī)范的一部分。
然而PE 文件源自VAX/VMS上早期的通用目標(biāo)文件格式(Common Object File Format,COFF)。
這是由于許多最初的Windows NT 開發(fā)團隊的成員都來自數(shù)字設(shè)備公司(Digital Equipment
Corporation,DEC)。這些開發(fā)者很自然就使用現(xiàn)有的代碼以便快速開始新的Windows NT 平臺。
之所以選擇術(shù)語“可移植可執(zhí)行”是打算要在所有支持的 CPU 上的所有版本的 Windows 上
使用相同的可執(zhí)行文件格式。從大的方面來說,這個目標(biāo)已經(jīng)實現(xiàn),因為Windows NT及其后繼
操作系統(tǒng)、Windows 95 及其后繼操作系統(tǒng)以及Windows CE 都使用相同的可執(zhí)行文件格式。
Microsoft編譯器生成的OBJ 文件也使用 COFF 格式。從COFF 格式的一些域使用的竟然是
八進制編碼你就能知道它是多么老。COFF格式的OBJ 文件中有許多數(shù)據(jù)結(jié)構(gòu)和枚舉類型與PE 文
件相同,后面我會提到。
64 位Windows 需要做的只是修改PE 格式的少數(shù)幾個域。這種新的格式被稱為PE32+。它并
沒有增加任何新域,僅從PE 格式中刪除了一個域。其余的改變就是簡單地把某些域從32 位擴展
到 64位。在大部分情況下,你都能寫出同時適用于32 位和64 位PE 文件的代碼。Windows 頭文
件有這種魔力可以使這些區(qū)別對于大多數(shù)基于C++的代碼都不可見。
EXE文件與 DLL 文件的區(qū)別完全是語義上的。它們使用的是相同的PE 格式。惟一的不同在
于一個位,這個位用來指示文件應(yīng)該作為 EXE 還是DLL。甚至DLL文件的擴展名也完全也是人為
的。你可以給 DLL 一個完全不同的擴展名,例如.OCX 控件和控制面板小程序(.CPL)都是DLL。
PE 文件一個非常好的地方就是它的數(shù)據(jù)結(jié)構(gòu)在磁盤上與在內(nèi)存中一樣。加載一個可執(zhí)行文
件到內(nèi)存(例如通過調(diào)用LoadLibrary函數(shù))主要就是把PE 文件中的某個部分映射到地址空間
中。因此像IMAGE_NT_HEADERS(后面我會講到)這樣的數(shù)據(jù)結(jié)構(gòu)在磁盤上和在內(nèi)存中是一樣的。
如果你知道如何在一個 PE文件中找到某些內(nèi)容,你幾乎可以確定當(dāng)文件被加載進內(nèi)存時可以找
到同樣的信息。
注意到PE 文件并不是作為單一的內(nèi)存映射文件被映射進內(nèi)存的這一點非常重要。相反,
Windows 加載器查看PE 文件并確定文件中的哪些部分需要被映射。當(dāng)映射進內(nèi)存時,文件中的
高偏移相對于內(nèi)存中的高地址。某項內(nèi)容在磁盤文件中的偏移可能與它被加載進內(nèi)存之后的偏移
不同,但是將磁盤文件中的偏移轉(zhuǎn)換成內(nèi)存偏移需要的所有信息都存在