Windows NT 3.1引入了一種名為PE文件格式的新可執(zhí)行文件格式。PE文件格式的規(guī)范包含在了MSDN的CD中(Specs and Strategy, Specifications, Windows NT File Format Specifications),但是它非常之晦澀。
然而這一的文檔并未提供足夠的信息,所以開發(fā)者們無(wú)法很好地弄懂PE格式。本文旨在解決這一問題,它會(huì)對(duì)整個(gè)的PE文件格式作一個(gè)十分徹底的解釋,另外,本文中還帶有對(duì)所有必需結(jié)構(gòu)的描述以及示范如何使用這些信息的源碼示例。
為了獲得PE文件中所包含的重要信息,我編寫了一個(gè)名為PEFILE.DLL的動(dòng)態(tài)鏈接庫(kù),本文中所有出現(xiàn)的源碼示例亦均摘自于此。這個(gè)DLL和它的源代碼都作為PEFile示例程序的一部分包含在了CD中(譯注:示例程序請(qǐng)?jiān)贛SDN中尋找,本站恕不提供),你可以在你自己的應(yīng)用程序中使用這個(gè)DLL;同樣,你亦可以依你所愿地使用并構(gòu)建它的源碼。在本文末尾,你會(huì)找到PEFILE.DLL的函數(shù)導(dǎo)出列表和一個(gè)如何使用它們的說明。我覺得你會(huì)發(fā)現(xiàn)這些函數(shù)會(huì)讓你從容應(yīng)付PE文件格式的。
介紹
Windows操作系統(tǒng)家族最近增加的Windows NT為開發(fā)環(huán)境和應(yīng)用程序本身帶來了很大的改變,這之中一個(gè)最為重大的當(dāng)屬PE文件格式了。新的PE文件格式主要來自于UNIX操作系統(tǒng)所通用的COFF規(guī)范,同時(shí)為了保證與舊版本MS-DOS及Windows操作系統(tǒng)的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ頭部。
在本文之中,PE文件格式是以自頂而下的順序解釋的。在你從頭開始研究文件內(nèi)容的過程之中,本文會(huì)詳細(xì)討論P(yáng)E文件的每一個(gè)組成部分。
許多單獨(dú)的文件成分定義都來自于Microsoft Win32 SDK開發(fā)包中的WINNT.H文件,在這個(gè)文件中你會(huì)發(fā)現(xiàn)用來描述文件頭部和數(shù)據(jù)目錄等各種成分的結(jié)構(gòu)類型定義。但是,在WINNT.H中缺少對(duì)PE文件結(jié)構(gòu)足夠的定義,在這種情況下,我定義了自己的結(jié)構(gòu)來存取文件數(shù)據(jù)。你會(huì)在PEFILE.DLL工程的PEFILE.H中找到這些結(jié)構(gòu)的定義,整套的PEFILE.H開發(fā)文件包含在PEFile示例程序之中。
本文配套的示例程序除了PEFILE.DLL示例代碼之外,還有一個(gè)單獨(dú)的Win32示例應(yīng)用程序,名為EXEVIEW.EXE。創(chuàng)建這一示例目的有二:首先,我需要測(cè)試PEFILE.DLL的函數(shù),并且某些情況要求我同時(shí)查看多個(gè)文件;其次,很多解決PE文件格式的工作和直接觀看數(shù)據(jù)有關(guān)。例如,要弄懂導(dǎo)入地址名稱表是如何構(gòu)成的,我就得同時(shí)查看.idata段頭部、導(dǎo)入映像數(shù)據(jù)目錄、可選頭部以及當(dāng)前的.idata段實(shí)體,而EXEVIEW.EXE就是查看這些信息的最佳示例。
閑話少敘,讓我們開始吧。
PE文件結(jié)構(gòu)
PE文件格式被組織為一個(gè)線性的數(shù)據(jù)流,它由一個(gè)MS-DOS頭部開始,接著是一個(gè)是模式的程序殘余以及一個(gè)PE文件標(biāo)志,這之后緊接著PE文件頭和可選頭部。這些之后是所有的段頭部,段頭部之后跟隨著所有的段實(shí)體。文件的結(jié)束處是一些其它的區(qū)域,其中是一些混雜的信息,包括重分配信息、符號(hào)表信息、行號(hào)信息以及字串表數(shù)據(jù)。我將所有這些成分列于圖1。

圖1.PE文件映像結(jié)構(gòu)
從MS-DOS文件頭結(jié)構(gòu)開始,我將按照PE文件格式各成分的出現(xiàn)順序依次對(duì)其進(jìn)行討論,并且討論的大部分是以示例代碼為基礎(chǔ)來示范如何獲得文件的信息的。所有的源碼均摘自PEFILE.DLL模塊的PEFILE.C文件。這些示例都利用了Windows NT最酷的特色之一——內(nèi)存映射文件,這一特色允許用戶使用一個(gè)簡(jiǎn)單的指針來存取文件中所包含的數(shù)據(jù),因此所有的示例都使用了內(nèi)存映射文件來存取PE文件中的數(shù)據(jù)。
注意:請(qǐng)查閱本文末尾關(guān)于如何使用PEFILE.DLL的那一段。
MS-DOS頭部/實(shí)模式頭部
如上所述,PE文件格式的第一個(gè)組成部分是MS-DOS頭部。在PE文件格式中,它并非一個(gè)新概念,因?yàn)樗cMS-DOS 2.0以來就已有的MS-DOS頭部是完全一樣的。保留這個(gè)相同結(jié)構(gòu)的最主要原因是,當(dāng)你嘗試在Windows 3.1以下或MS-DOS 2.0以上的系統(tǒng)下裝載一個(gè)文件的時(shí)候,操作系統(tǒng)能夠讀取這個(gè)文件并明白它是和當(dāng)前系統(tǒng)不相兼容的。換句話說,當(dāng)你在MS-DOS 6.0下運(yùn)行一個(gè)Windows NT可執(zhí)行文件時(shí),你會(huì)得到這樣一條消息:“This program cannot be run in DOS mode.”如果MS-DOS頭部不是作為PE文件格式的第一部分的話,操作系統(tǒng)裝載文件的時(shí)候就會(huì)失敗,并提供一些完全沒用的信息,例如:“The name specified is not recognized as an internal or external command, operable program or batch file.”
MS-DOS頭部占據(jù)了PE文件的頭64個(gè)字節(jié),描述它內(nèi)容的結(jié)構(gòu)如下:
實(shí)模式殘余程序
實(shí)模式殘余程序是一個(gè)在裝載時(shí)能夠被MS-DOS運(yùn)行的實(shí)際程序。對(duì)于一個(gè)MS-DOS的可執(zhí)行映像文件,應(yīng)用程序就是從這里執(zhí)行的。對(duì)于Windows、OS/2、Windows NT這些操作系統(tǒng)來說,MS-DOS殘余程序就代替了主程序的位置被放在這里。這種殘余程序通常什么也不做,而只是輸出一行文本,例如:“This program requires Microsoft Windows v3.1 or greater.”當(dāng)然,用戶可以在此放入任何的殘余程序,這就意味著你可能經(jīng)??吹较襁@樣的東西:“You can‘‘t run a Windows NT application on OS/2, it‘‘s simply not possible.”
當(dāng)為Windows 3.1構(gòu)建一個(gè)應(yīng)用程序的時(shí)候,鏈接器將向你的可執(zhí)行文件中鏈接一個(gè)名為WINSTUB.EXE的默認(rèn)殘余程序。你可以用一個(gè)基于MS-DOS的有效程序取代WINSTUB,并且用STUB模塊定義語(yǔ)句指示鏈接器,這樣就能夠取代鏈接器的默認(rèn)行為。為Windows NT開發(fā)的應(yīng)用程序可以通過使用-STUB:鏈接器選項(xiàng)來實(shí)現(xiàn)。
PE文件頭部與標(biāo)志
PE文件頭部是由MS-DOS頭部的e_lfanew域定位的,這個(gè)域只是給出了文件的偏移量,所以要確定PE頭部的實(shí)際內(nèi)存映射地址,就需要添加文件的內(nèi)存映射基地址。例如,以下的宏是包含在PEFILE.H源文件之中的:
請(qǐng)注意這個(gè)宏所獲得的是PE文件標(biāo)志,而并非PE文件頭部的偏移量。那是由于自Windows與OS/2的可執(zhí)行文件開始,.EXE文件都被賦予了目標(biāo)操作系統(tǒng)的標(biāo)志。對(duì)于Windows NT的PE文件格式而言,這一標(biāo)志在PE文件頭部結(jié)構(gòu)之前。在Windows和OS/2的某些版本中,這一標(biāo)志是文件頭的第一個(gè)字。同樣,對(duì)于PE文件格式,Windows NT使用了一個(gè)DWORD值。
以上的宏返回了文件標(biāo)志的偏移量,而不管它是哪種類型的可執(zhí)行文件。所以,文件頭部是在DWORD標(biāo)志之后,還是在WORD標(biāo)志處,是由這個(gè)標(biāo)志是否Windows NT文件標(biāo)志所決定的。要解決這個(gè)問題,我編寫了ImageFileType函數(shù)(如下),它返回了映像文件的類型:
現(xiàn)在把我們的注意力轉(zhuǎn)向Windows NT PE文件格式,我們會(huì)發(fā)現(xiàn)只要我們得到了文件標(biāo)志的位置,PE文件之后就會(huì)有4個(gè)字節(jié)相跟隨。下一個(gè)宏標(biāo)識(shí)了PE文件的頭部:
既然我們知道了PE文件頭的位置,那么就可以檢查頭部的數(shù)據(jù)了。我們只需要把這個(gè)位置賦值給一個(gè)結(jié)構(gòu),如下:
PE文件中的信息基本上是一些高級(jí)信息,這些信息是被操作系統(tǒng)或者應(yīng)用程序用來決定如何處理這個(gè)文件的。第一個(gè)域是用來表示這個(gè)可執(zhí)行文件被構(gòu)建的目標(biāo)機(jī)器種類,例如DEC(R) Alpha、MIPS R4000、Intel(R) x86或一些其它處理器。系統(tǒng)使用這一信息來在讀取這個(gè)文件的其它數(shù)據(jù)之前決定如何處理它。
Characteristics域表示了文件的一些特征。比如對(duì)于一個(gè)可執(zhí)行文件而言,分離調(diào)試文件是如何操作的。調(diào)試器通常使用的方法是將調(diào)試信息從PE文件中分離,并保存到一個(gè)調(diào)試文件(.DBG)中。要這么做的話,調(diào)試器需要了解是否要在一個(gè)單獨(dú)的文件中尋找調(diào)試信息,以及這個(gè)文件是否已經(jīng)將調(diào)試信息分離了。我們可以通過深入可執(zhí)行文件并尋找調(diào)試信息的方法來完成這一工作。要使調(diào)試器不在文件中查找的話,就需要用到IMAGE_FILE_DEBUG_STRIPPED這個(gè)特征,它表示文件的調(diào)試信息是否已經(jīng)被分離了。這樣一來,調(diào)試器可以通過快速查看PE文件的頭部的方法來決定文件中是否存在著調(diào)試信息。
WINNT.H定義了若干其它表示文件頭信息的標(biāo)記,就和以上的例子差不多。我把研究這些標(biāo)記的事情留給讀者作為練習(xí),由你們來看看它們是不是很有趣,這些標(biāo)記位于WINNT.H中的IMAGE_FILE_HEADER結(jié)構(gòu)之后。
PE文件頭結(jié)構(gòu)中另一個(gè)有用的入口是NumberOfSections域,它表示如果你要方便地提取文件信息的話,就需要了解多少個(gè)段——更明確一點(diǎn)來說,有多少個(gè)段頭部和多少個(gè)段實(shí)體。每一個(gè)段頭部和段實(shí)體都在文件中連續(xù)地排列著,所以要決定段頭部和段實(shí)體在哪里結(jié)束的話,段的數(shù)目是必需的。以下的函數(shù)從PE文件頭中提取了段的數(shù)目:
PE可選頭部
PE可執(zhí)行文件中接下來的224個(gè)字節(jié)組成了PE可選頭部。雖然它的名字是“可選頭部”,但是請(qǐng)確信:這個(gè)頭部并非“可選”,而是“必需”的。OPTHDROFFSET宏可以獲得指向可選頭部的指針:
標(biāo)準(zhǔn)域
首先,請(qǐng)注意這個(gè)結(jié)構(gòu)被劃分為“標(biāo)準(zhǔn)域”和“NT附加域”。所謂標(biāo)準(zhǔn)域,就是和UNIX可執(zhí)行文件的COFF格式所公共的部分。雖然標(biāo)準(zhǔn)域保留了COFF中定義的名字,但是Windows NT仍然將它們用作了不同的目的——盡管換個(gè)名字更好一些。
·Magic。我不知道這個(gè)域是干什么的,對(duì)于示例程序EXEVIEW.EXE示例程序而言,這個(gè)值是0x010B或267(譯注:0x010B為.EXE,0x0107為ROM映像,這個(gè)信息我是從eXeScope上得來的)。
·MajorLinkerVersion、MinorLinkerVersion。表示鏈接此映像的鏈接器版本。隨Window NT build 438配套的Windows NT SDK包含的鏈接器版本是2.39(十六進(jìn)制為2.27)。
·SizeOfCode??蓤?zhí)行代碼尺寸。
·SizeOfInitializedData。已初始化的數(shù)據(jù)尺寸。
·SizeOfUninitializedData。未初始化的數(shù)據(jù)尺寸。
·AddressOfEntryPoint。在標(biāo)準(zhǔn)域中,AddressOfEntryPoint域是對(duì)PE文件格式來說最為有趣的了。這個(gè)域表示應(yīng)用程序入口點(diǎn)的位置。并且,對(duì)于系統(tǒng)黑客來說,這個(gè)位置就是導(dǎo)入地址表(IAT)的末尾。以下的函數(shù)示范了如何從可選頭部獲得Windows NT可執(zhí)行映像的入口點(diǎn)。
·BaseOfData。已載入映像的未初始化數(shù)據(jù)(“.bss”段)的相對(duì)偏移量。
Windows NT附加域
添加到Windows NT PE文件格式中的附加域?yàn)閃indows NT特定的進(jìn)程行為提供了裝載器的支持,以下為這些域的概述。
·ImageBase。進(jìn)程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK鏈接器將這個(gè)值默認(rèn)設(shè)為0x00400000,但是你可以使用-BASE:linker開關(guān)改變這個(gè)值。
·SectionAlignment。從ImageBase開始,每個(gè)段都被相繼的裝入進(jìn)程的地址空間中。SectionAlignment則規(guī)定了裝載時(shí)段能夠占據(jù)的最小空間數(shù)量——就是說,段是關(guān)于SectionAlignment對(duì)齊的。
Windows NT虛擬內(nèi)存管理器規(guī)定,段對(duì)齊不能少于頁(yè)尺寸(當(dāng)前的x86平臺(tái)是4096字節(jié)),并且必須是成倍的頁(yè)尺寸。4096字節(jié)是x86鏈接器的默認(rèn)值,但是它可以通過-ALIGN: linker開關(guān)來設(shè)置。
·FileAlignment。映像文件首先裝載的最小的信息塊間隔。例如,鏈接器將一個(gè)段實(shí)體(段的原始數(shù)據(jù))加零擴(kuò)展為文件中最接近的FileAlignment邊界。早先提及的2.39版鏈接器將映像文件以0x200字節(jié)的邊界對(duì)齊,這個(gè)值可以被強(qiáng)制改為512到65535這么多。
·MajorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的主版本號(hào);通常對(duì)Windows NT 1.0而言,這個(gè)值被設(shè)為1。
·MinorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的次版本號(hào);通常對(duì)Windows NT 1.0而言,這個(gè)值被設(shè)為0。
·MajorImageVersion。用來表示應(yīng)用程序的主版本號(hào);對(duì)于Microsoft Excel 4.0而言,這個(gè)值是4。
·MinorImageVersion。用來表示應(yīng)用程序的次版本號(hào);對(duì)于Microsoft Excel 4.0而言,這個(gè)值是0。
·MajorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的主版本號(hào);通常對(duì)于Windows NT 3.10而言,這個(gè)值被設(shè)為3。
·MinorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的次版本號(hào);通常對(duì)于Windows NT 3.10而言,這個(gè)值被設(shè)為10。
·Reserved1。未知目的,通常不被系統(tǒng)使用,并被鏈接器設(shè)為0。
·SizeOfImage。表示載入的可執(zhí)行映像的地址空間中要保留的地址空間大小,這個(gè)數(shù)字很大程度上受SectionAlignment的影響。例如,考慮一個(gè)擁有固定頁(yè)尺寸4096字節(jié)的系統(tǒng),如果你有一個(gè)11個(gè)段的可執(zhí)行文件,它的每個(gè)段都少于4096字節(jié),并且關(guān)于65536字節(jié)邊界對(duì)齊,那么SizeOfImage域?qū)?huì)被設(shè)為11 * 65536 = 720896(176頁(yè))。而如果一個(gè)相同的文件關(guān)于4096字節(jié)對(duì)齊的話,那么SizeOfImage域的結(jié)果將是11 * 4096 = 45056(11頁(yè))。這只是個(gè)簡(jiǎn)單的例子,它說明每個(gè)段需要少于一個(gè)頁(yè)面的內(nèi)存。在現(xiàn)實(shí)中,鏈接器通過個(gè)別地計(jì)算每個(gè)段的方法來決定SizeOfImage確切的值。它首先決定每個(gè)段需要多少字節(jié),并且最后將頁(yè)面總數(shù)向上取整至最接近的SectionAlignment邊界,然后總數(shù)就是每個(gè)段個(gè)別需求之和了。
·SizeOfHeaders。這個(gè)域表示文件中有多少空間用來保存所有的文件頭部,包括MS-DOS頭部、PE文件頭部、PE可選頭部以及PE段頭部。文件中所有的段實(shí)體就開始于這個(gè)位置。
·CheckSum。校驗(yàn)和是用來在裝載時(shí)驗(yàn)證可執(zhí)行文件的,它是由鏈接器設(shè)置并檢驗(yàn)的。由于創(chuàng)建這些校驗(yàn)和的算法是私有信息,所以在此不進(jìn)行討論。
·Subsystem。用于標(biāo)識(shí)該可執(zhí)行文件目標(biāo)子系統(tǒng)的域。每個(gè)可能的子系統(tǒng)取值列于WINNT.H的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)之后。
·DllCharacteristics。用來表示一個(gè)DLL映像是否為進(jìn)程和線程的初始化及終止包含入口點(diǎn)的標(biāo)記。
·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit。這些域控制要保留的地址空間數(shù)量,并且負(fù)責(zé)棧和默認(rèn)堆的申請(qǐng)。在默認(rèn)情況下,棧和堆都擁有1個(gè)頁(yè)面的申請(qǐng)值以及16個(gè)頁(yè)面的保留值。這些值可以使用鏈接器開關(guān)-STACKSIZE:與-HEAPSIZE:來設(shè)置。
·LoaderFlags。告知裝載器是否在裝載時(shí)中止和調(diào)試,或者默認(rèn)地正常運(yùn)行。
·NumberOfRvaAndSizes。這個(gè)域標(biāo)識(shí)了接下來的DataDirectory數(shù)組。請(qǐng)注意它被用來標(biāo)識(shí)這個(gè)數(shù)組,而不是數(shù)組中的各個(gè)入口數(shù)字,這一點(diǎn)非常重要。
·DataDirectory。數(shù)據(jù)目錄表示文件中其它可執(zhí)行信息重要組成部分的位置。它事實(shí)上就是一個(gè)IMAGE_DATA_DIRECTORY結(jié)構(gòu)的數(shù)組,位于可選頭部結(jié)構(gòu)的末尾。當(dāng)前的PE文件格式定義了16種可能的數(shù)據(jù)目錄,這之中的11種現(xiàn)在在使用中。
數(shù)據(jù)目錄
WINNT.H之中所定義的數(shù)據(jù)目錄為:
所以要獲得一個(gè)數(shù)據(jù)目錄的話,那么首先你需要了解段的概念。我在下面會(huì)對(duì)其進(jìn)行描述,這個(gè)討論之后還有一個(gè)有關(guān)如何定位數(shù)據(jù)目錄的示例。
PE文件段
PE文件規(guī)范由目前為止定義的那些頭部以及一個(gè)名為“段”的一般對(duì)象組成。段包含了文件的內(nèi)容,包括代碼、數(shù)據(jù)、資源以及其它可執(zhí)行信息,每個(gè)段都有一個(gè)頭部和一個(gè)實(shí)體(原始數(shù)據(jù))。我將在下面描述段頭部的有關(guān)信息,但是段實(shí)體則缺少一個(gè)嚴(yán)格的文件結(jié)構(gòu)。因此,它們幾乎可以被鏈接器按任何的方法組織,只要它的頭部填充了足夠能夠解釋數(shù)據(jù)的信息。
段頭部
PE文件格式中,所有的段頭部位于可選頭部之后。每個(gè)段頭部為40個(gè)字節(jié)長(zhǎng),并且沒有任何的填充信息。段頭部被定義為以下的結(jié)構(gòu):
段頭部的域
·Name。每個(gè)段都有一個(gè)8字符長(zhǎng)的名稱域,并且第一個(gè)字符必須是一個(gè)句點(diǎn)。
·PhysicalAddress或VirtualSize。第二個(gè)域是一個(gè)union域,現(xiàn)在已不使用了。
·VirtualAddress。這個(gè)域標(biāo)識(shí)了進(jìn)程地址空間中要裝載這個(gè)段的虛擬地址。實(shí)際的地址由將這個(gè)域的值加上可選頭部結(jié)構(gòu)中的ImageBase虛擬地址得到。切記,如果這個(gè)映像文件是一個(gè)DLL,那么這個(gè)DLL就不一定會(huì)裝載到ImageBase要求的位置。所以一旦這個(gè)文件被裝載進(jìn)入了一個(gè)進(jìn)程,實(shí)際的ImageBase值應(yīng)該通過使用GetModuleHandle來檢驗(yàn)。
·SizeOfRawData。這個(gè)域表示了相對(duì)FileAlignment的段實(shí)體尺寸。文件中實(shí)際的段實(shí)體尺寸將少于或等于FileAlignment的整倍數(shù)。一旦映像被裝載進(jìn)入了一個(gè)進(jìn)程的地址空間,段實(shí)體的尺寸將會(huì)變得少于或等于FileAlignment的整倍數(shù)。
·PointerToRawData。這是一個(gè)文件中段實(shí)體位置的偏移量。
·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。這些域在PE格式中不使用。
·Characteristics。定義了段的特征。這些值可以在WINNT.H及本光盤(譯注:MSDN的光盤)的PE格式規(guī)范中找到。
值 定義
0x00000020 代碼段
0x00000040 已初始化數(shù)據(jù)段
0x00000080 未初始化數(shù)據(jù)段
0x04000000 該段數(shù)據(jù)不能被緩存
0x08000000 該段不能被分頁(yè)
0x10000000 共享段
0x20000000 可執(zhí)行段
0x40000000 可讀段
0x80000000 可寫段
定位數(shù)據(jù)目錄
數(shù)據(jù)目錄存在于它們相應(yīng)的數(shù)據(jù)段中。典型地來說,數(shù)據(jù)目錄是段實(shí)體中的第一個(gè)結(jié)構(gòu),但不是必需的。由于這個(gè)緣故,如果你需要定位一個(gè)指定的數(shù)據(jù)目錄的話,就需要從段頭部和可選頭部中獲得信息。
為了讓這個(gè)過程簡(jiǎn)單一點(diǎn),我編寫了以下的函數(shù)來定位任何一個(gè)在WINNT.H之中定義的數(shù)據(jù)目錄。
預(yù)定義段
一個(gè)Windows NT的應(yīng)用程序典型地?fù)碛?個(gè)預(yù)定義段,它們是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些應(yīng)用程序不需要所有的這些段,同樣還有一些應(yīng)用程序?yàn)榱俗约禾厥獾男枰x了更多的段。這種做法與MS-DOS和Windows 3.1中的代碼段和數(shù)據(jù)段相似。事實(shí)上,應(yīng)用程序定義一個(gè)獨(dú)特的段的方法是使用標(biāo)準(zhǔn)編譯器來指示對(duì)代碼段和數(shù)據(jù)段的命名,或者使用名稱段編譯器選項(xiàng)-NT——就和Windows 3.1中應(yīng)用程序定義獨(dú)特的代碼段和數(shù)據(jù)段一樣。
以下是一個(gè)關(guān)于Windows NT PE文件之中一些有趣的公共段的討論。
可執(zhí)行代碼段,.text
Windows 3.1和Windows NT之間的一個(gè)區(qū)別就是Windows NT默認(rèn)的做法是將所有的代碼段(正如它們?cè)赪indows 3.1中所提到的那樣)組成了一個(gè)單獨(dú)的段,名為“.text”。既然Windows NT使用了基于頁(yè)面的虛擬內(nèi)存管理系統(tǒng),那么將分開的代碼放入不同的段之中的做法就不太明智了。因此,擁有一個(gè)大的代碼段對(duì)于操作系統(tǒng)和應(yīng)用程序開發(fā)者來說,都是十分方便的。
.text段也包含了早先提到過的入口點(diǎn)。IAT亦存在于.text段之中的模塊入口點(diǎn)之前。(IAT在.text段之中的存在非常有意義,因?yàn)檫@個(gè)表事實(shí)上是一系列的跳轉(zhuǎn)指令,并且它們的跳轉(zhuǎn)目標(biāo)位置是已固定的地址。)當(dāng)Windows NT的可執(zhí)行映像裝載入進(jìn)程的地址空間時(shí),IAT就和每一個(gè)導(dǎo)入函數(shù)的物理地址一同確定了。要在.text段之中查找IAT,裝載器只用將模塊的入口點(diǎn)定位,而IAT恰恰出現(xiàn)于入口點(diǎn)之前。既然每個(gè)入口擁有相同的尺寸,那么向后退查找這個(gè)表的起始位置就很容易了。
數(shù)據(jù)段,.bss、.rdata、.data
.bss段表示應(yīng)用程序的未初始化數(shù)據(jù),包括所有函數(shù)或源模塊中聲明為static的變量。
.rdata段表示只讀的數(shù)據(jù),比如字符串文字量、常量和調(diào)試目錄信息。
所有其它變量(除了出現(xiàn)在棧上的自動(dòng)變量)存儲(chǔ)在.data段之中?;旧希@些是應(yīng)用程序或模塊的全局變量。
資源段,.rsrc
.rsrc段包含了模塊的資源信息。它起始于一個(gè)資源目錄結(jié)構(gòu),這個(gè)結(jié)構(gòu)就像其它大多數(shù)結(jié)構(gòu)一樣,但是它的數(shù)據(jù)被更進(jìn)一步地組織在了一棵資源樹之中。以下的IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)形成了這棵樹的根和各個(gè)結(jié)點(diǎn)。
一個(gè)目錄入口由兩個(gè)域組成,正如下面IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)所描述的那樣:
葉子結(jié)點(diǎn)是資源樹之中最底層的結(jié)點(diǎn),它們定義了當(dāng)前資源數(shù)據(jù)的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu)被用于描述每個(gè)葉子結(jié)點(diǎn):
要更清楚地了解這些內(nèi)容,請(qǐng)參考圖2。

圖2.一個(gè)簡(jiǎn)單的資源樹結(jié)構(gòu)
圖2描述了一個(gè)非常簡(jiǎn)單的資源樹,它包含了僅僅兩個(gè)資源對(duì)象:一個(gè)菜單和一個(gè)字串表。更深一層地來說,它們各自都有一個(gè)子項(xiàng)。然而,你仍然可以看到資源樹有多么復(fù)雜——即使它像這個(gè)一樣只有一點(diǎn)點(diǎn)資源。
在樹的根部,第一個(gè)目錄有一個(gè)文件中包含的所有資源種類的入口,而不管資源種類有多少。在圖2中,有兩個(gè)由樹根標(biāo)識(shí)的入口,一個(gè)是菜單的,另一個(gè)是字串表的。如果文件中擁有一個(gè)或多個(gè)對(duì)話框資源,那么根結(jié)點(diǎn)會(huì)再擁有一個(gè)入口,因此,就有了對(duì)話框資源的另一個(gè)分支。
WINUSER.H中標(biāo)識(shí)了基本的資源種類,我將它們列到了下面:
每個(gè)根目錄的入口都指向了樹中第二層級(jí)的一個(gè)兄弟結(jié)點(diǎn),這些結(jié)點(diǎn)也是目錄,并且每個(gè)都擁有它們自己的入口。在這一層級(jí),目錄被用來以給定的種類標(biāo)識(shí)每一個(gè)資源種類。如果你的應(yīng)用程序中有多個(gè)菜單,那么樹中的第二層級(jí)會(huì)為每個(gè)菜單都準(zhǔn)備一個(gè)入口。
你可能意識(shí)到了,資源可以由名稱或整數(shù)標(biāo)識(shí)。在這一層級(jí),它們是通過目錄結(jié)構(gòu)的Name域來分辨的。如果如果Name域最重要的位被設(shè)置了,那么其它的31個(gè)位就會(huì)被用作一個(gè)到IMAGE_RESOURCE_DIR_STRING_U結(jié)構(gòu)的偏移量。
另一方面,如果Name域最重要的位被清空,那么它的低31位就被用于表示資源的整數(shù)ID。圖2示范的就是菜單資源作為一個(gè)命名的資源,以及字串表作為一個(gè)ID資源。
如果有兩個(gè)菜單資源,一個(gè)由名稱標(biāo)識(shí),另一個(gè)由資源標(biāo)識(shí),那么它們二者就會(huì)在菜單資源目錄之后擁有兩個(gè)入口。有名稱的資源入口在第一位,之后是由整數(shù)標(biāo)識(shí)的資源。目錄域NumberOfNamedEntries和NumberOfIdEntries將各自包含值1,表示當(dāng)前的1個(gè)入口。
在第二層級(jí)的下面,資源樹就不再更深一步地?cái)U(kuò)展分支了。第一層級(jí)分支至表示每個(gè)資源種類的目錄中,第二層級(jí)分支至由標(biāo)識(shí)符表示的每個(gè)資源的目錄中,第三層級(jí)是被個(gè)別標(biāo)識(shí)的資源與它們各自的語(yǔ)言ID之間一對(duì)一的映射。要表示一個(gè)資源的語(yǔ)言ID,目錄入口結(jié)構(gòu)的Name域就被用來表示資源的主語(yǔ)言ID和子語(yǔ)言ID了。Windows NT的Win32 SDK開發(fā)包中列出了默認(rèn)的值資源,例如對(duì)于0x0409這個(gè)值來說,0x09表示主語(yǔ)言LANG_ENGLISH,0x04則被定義為子語(yǔ)言的SUBLANG_ENGLISH_CAN。所有的語(yǔ)言ID值都定義于Windows NT Win32 SDK開發(fā)包的文件WINNT.H中。
既然語(yǔ)言ID結(jié)點(diǎn)是樹中最后的目錄結(jié)點(diǎn),那么入口結(jié)構(gòu)的OffsetToData域就是到一個(gè)葉子結(jié)點(diǎn)(即前面提到過的IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu))的偏移量。
再回過頭來參考圖2,你會(huì)發(fā)現(xiàn)每個(gè)語(yǔ)言目錄入口都對(duì)應(yīng)著一個(gè)數(shù)據(jù)入口。這個(gè)結(jié)點(diǎn)僅僅表示了資源數(shù)據(jù)的尺寸以及資源數(shù)據(jù)的相對(duì)虛擬地址。
在資源數(shù)據(jù)段(.rsrc)之中擁有這么多結(jié)構(gòu)有一個(gè)好處,就是你可以不存取資源本身而直接可以從這個(gè)段收集很多信息。例如,你可以獲得有多少種資源、哪些資源(如果有的話)使用了特別的語(yǔ)言ID、特定的資源是否存在以及單獨(dú)種類資源的尺寸。為了示范如何利用這一信息,以下的函數(shù)說明了如何決定一個(gè)文件中包含的不同種類的資源:
導(dǎo)出數(shù)據(jù)段,.edata
.edata段包含了應(yīng)用程序或DLL的導(dǎo)出數(shù)據(jù)。在這個(gè)段出現(xiàn)的時(shí)候,它會(huì)包含一個(gè)到達(dá)導(dǎo)出信息的導(dǎo)出目錄。
AddressOfFunctions域是一個(gè)到導(dǎo)出函數(shù)入口列表的偏移量。AddressOfNames域是到一個(gè)導(dǎo)出函數(shù)名稱列表起始處偏移量的地址,這個(gè)列表是由null分隔的。AddressOfNameOrdinals是一個(gè)到相同導(dǎo)出函數(shù)順序值(每個(gè)值2字節(jié)長(zhǎng))列表的偏移量。
三個(gè)AddressOf...域是當(dāng)模塊裝載時(shí)進(jìn)程地址空間中的相對(duì)虛擬地址。一旦模塊被裝載,那么要獲得進(jìn)程地質(zhì)空間中的確切地址的話,就應(yīng)該在相對(duì)虛擬地址上加上模塊的基地址??墒牵谖募谎b載前,仍然可以決定這一地址:只要從給定的域地址中減去段頭部的虛擬地址(VirtualAddress),再加上段實(shí)體的偏移量(PointerToRawData),這個(gè)結(jié)果就是映像文件中的偏移量了。以下的例子解說了這一技術(shù):
導(dǎo)入數(shù)據(jù)段,.idata
.idata段是導(dǎo)入數(shù)據(jù),包括導(dǎo)入庫(kù)和導(dǎo)入地址名稱表。雖然定義了IMAGE_DIRECTORY_ENTRY_IMPORT,但是WINNT.H之中并無(wú)相應(yīng)的導(dǎo)入目錄結(jié)構(gòu)。作為代替,其中有若干其它的結(jié)構(gòu),名為IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA與IMAGE_IMPORT_DESCRIPTOR。在我個(gè)人看來,我實(shí)在不知道這些結(jié)構(gòu)是如何和.idata段發(fā)生關(guān)聯(lián)的,所以我花了若干個(gè)小時(shí)來破譯.idata段實(shí)體并且得到了一個(gè)更簡(jiǎn)單的結(jié)構(gòu),我名之為IMAGE_IMPORT_MODULE_DIRECTORY。
IMAGE_IMPORT_MODULE_DIRECTORY結(jié)構(gòu)中的一個(gè)域dwRVAModuleName是一個(gè)相對(duì)虛擬地址,它指向模塊的名稱。結(jié)構(gòu)中還有兩個(gè)dwUseless參數(shù),它們是為了保持段的對(duì)齊。PE文件格式規(guī)范提到了一些東西,關(guān)于導(dǎo)入標(biāo)記、時(shí)間/日期標(biāo)志以及主/次版本,但是在我的實(shí)驗(yàn)中,這兩個(gè)域自始而終都是空的,所以我仍然認(rèn)為它們沒有什么用處。
基于這個(gè)結(jié)構(gòu)的定義,你便可以獲得可執(zhí)行文件中導(dǎo)入的所有模塊和函數(shù)名稱了。以下的函數(shù)示范了如何獲得特定的PE文件中的所有導(dǎo)入函數(shù)名稱:
這個(gè)結(jié)構(gòu)中的第一個(gè)域dwRVAFunctionNameList是一個(gè)相對(duì)虛擬地址,這個(gè)地址指向一個(gè)相對(duì)虛擬地址的列表,這些地址是文件中的一些文件名。如下面的數(shù)據(jù)所示,所有導(dǎo)入模塊的模塊和函數(shù)名稱都列于.idata段數(shù)據(jù)中了:
這樣的式樣會(huì)在.idata段中重復(fù)出現(xiàn)。第一個(gè)模塊是COMDLG32.DLL,第二個(gè)是GDI32.DLL。請(qǐng)注意第一個(gè)模塊只導(dǎo)出了一個(gè)函數(shù),而第二個(gè)模塊導(dǎo)出了很多函數(shù)。在這兩種情況下,函數(shù)和模塊的排列的方法是首先出現(xiàn)一個(gè)函數(shù)名,之后是模塊名,然后是其它的函數(shù)名(如果有的話)。
以下的函數(shù)示范了如何獲得指定模塊的所有函數(shù)名。
最后一個(gè)域dwRVAFunctionAddressList是一個(gè)相對(duì)虛擬地址,它指向一個(gè)虛擬地址表。在文件裝載的時(shí)候,這個(gè)虛擬地址表會(huì)被裝載器置于段數(shù)據(jù)之中。但是在文件裝載前,這些虛擬地址會(huì)被一些嚴(yán)密符合函數(shù)名稱列表的虛擬地址替換。所以在文件裝載之前,有兩個(gè)同樣的虛擬地址列表,它們指向?qū)牒瘮?shù)列表。
調(diào)試信息段,.debug
調(diào)試信息位于.debug段之中,同時(shí)PE文件格式也支持單獨(dú)的調(diào)試文件(通常由.DBG擴(kuò)展名標(biāo)識(shí))作為一種將調(diào)試信息集中的方法。調(diào)試段包含了調(diào)試信息,但是調(diào)試目錄卻位于早先提到的.rdata段之中。這其中每個(gè)目錄都涉及了.debug段之中的調(diào)試信息。調(diào)試目錄的結(jié)構(gòu)IMAGE_DEBUG_DIRECTORY被定義為:
每種調(diào)試信息都擁有自己的頭部結(jié)構(gòu),該結(jié)構(gòu)定義了它自己的數(shù)據(jù)。這些結(jié)構(gòu)都列于WINNT.H之中。關(guān)于IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了兩個(gè)標(biāo)識(shí)調(diào)試信息的域。第一個(gè)是AddressOfRawData,為相對(duì)文件裝載的數(shù)據(jù)虛擬地址;另一個(gè)是PointerToRawData,為數(shù)據(jù)所在PE文件之中的實(shí)際偏移量。這就使得定位指定的調(diào)試信息相當(dāng)容易了。
作為最后的例子,請(qǐng)你考慮以下的函數(shù)代碼,它從IMAGE_DEBUG_MISC結(jié)構(gòu)中提取了映像名稱。
如上所述,調(diào)試信息可以被剝離到單獨(dú)的.DBG文件中。Windows NT SDK包含了一個(gè)名為REBASE.EXE的程序可以實(shí)現(xiàn)這一目的。例如,以下的語(yǔ)句可以將一個(gè)名為TEST.EXE的調(diào)試信息剝離:
rebase -b 40000 -x c:\samples\testdir test.exe
調(diào)試信息被置于一個(gè)新的文件中,這個(gè)文件名為TEST.DBG,位于c:\samples\testdir之中。這個(gè)文件起始于一個(gè)單獨(dú)的IMAGE_SEPARATE_DEBUG_HEADER結(jié)構(gòu),接著是存在于原可執(zhí)行映像之中的段頭部的一份拷貝。在段頭部之后,是.debug段的數(shù)據(jù)。也就是說,在段頭部之后,就是一系列的IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)及其相關(guān)的數(shù)據(jù)了。調(diào)試信息本身保留了如上所描述的常規(guī)映像文件調(diào)試信息。
PE文件格式總結(jié)
Windows NT的PE文件格式向熟悉Windows和MS-DOS環(huán)境的開發(fā)者引入了一種全新的結(jié)構(gòu)。然而熟悉UNIX環(huán)境的開發(fā)者會(huì)發(fā)現(xiàn)PE文件格式與COFF規(guī)范很相像(如果它不是以COFF為基礎(chǔ)的話)。
整個(gè)格式的組成:一個(gè)MS-DOS的MZ頭部,之后是一個(gè)實(shí)模式的殘余程序、PE文件標(biāo)志、PE文件頭部、PE可選頭部、所有的段頭部,最后是所有的段實(shí)體。
可選頭部的末尾是一個(gè)數(shù)據(jù)目錄入口的數(shù)組,這些相對(duì)虛擬地址指向段實(shí)體之中的數(shù)據(jù)目錄。每個(gè)數(shù)據(jù)目錄都表示了一個(gè)特定的段實(shí)體數(shù)據(jù)是如何組織的。
PE文件格式有11個(gè)預(yù)定義段,這是對(duì)Windows NT應(yīng)用程序所通用的,但是每個(gè)應(yīng)用程序可以為它自己的代碼以及數(shù)據(jù)定義它自己獨(dú)特的段。
.debug預(yù)定義段也可以分離為一個(gè)單獨(dú)的調(diào)試文件。如果這樣的話,就會(huì)有一個(gè)特定的調(diào)試頭部來用于解析這個(gè)調(diào)試文件,PE文件中也會(huì)有一個(gè)標(biāo)志來表示調(diào)試數(shù)據(jù)被分離了出去。
PEFILE.DLL函數(shù)描述
PEFILE.DLL主要由一些函數(shù)組成,這些函數(shù)或者被用來獲得一個(gè)給定的PE文件中的偏移量,或者被用來把文件中的一些數(shù)據(jù)復(fù)制到一個(gè)特定的結(jié)構(gòu)中去。每個(gè)函數(shù)都有一個(gè)需求——第一個(gè)參數(shù)是一個(gè)指針,這個(gè)指針指向PE文件的起始處。也就是說,這個(gè)文件必須首先被映射到你進(jìn)程的地址空間中,然后映射文件的位置就可以作為每個(gè)函數(shù)第一個(gè)參數(shù)的lpFile的值來傳入了。
我意在使函數(shù)的名稱使你能夠一見而知其意,并且每個(gè)函數(shù)都隨一個(gè)詳細(xì)描述其目的的注釋而列出。如果在讀完函數(shù)列表之后,你仍然不明白某個(gè)函數(shù)的功能,那么請(qǐng)參考EXEVIEW.EXE示例來查明這個(gè)函數(shù)是如何使用的。以下的函數(shù)原型列表可以在PEFILE.H中找到:
然而這一的文檔并未提供足夠的信息,所以開發(fā)者們無(wú)法很好地弄懂PE格式。本文旨在解決這一問題,它會(huì)對(duì)整個(gè)的PE文件格式作一個(gè)十分徹底的解釋,另外,本文中還帶有對(duì)所有必需結(jié)構(gòu)的描述以及示范如何使用這些信息的源碼示例。
為了獲得PE文件中所包含的重要信息,我編寫了一個(gè)名為PEFILE.DLL的動(dòng)態(tài)鏈接庫(kù),本文中所有出現(xiàn)的源碼示例亦均摘自于此。這個(gè)DLL和它的源代碼都作為PEFile示例程序的一部分包含在了CD中(譯注:示例程序請(qǐng)?jiān)贛SDN中尋找,本站恕不提供),你可以在你自己的應(yīng)用程序中使用這個(gè)DLL;同樣,你亦可以依你所愿地使用并構(gòu)建它的源碼。在本文末尾,你會(huì)找到PEFILE.DLL的函數(shù)導(dǎo)出列表和一個(gè)如何使用它們的說明。我覺得你會(huì)發(fā)現(xiàn)這些函數(shù)會(huì)讓你從容應(yīng)付PE文件格式的。
介紹
Windows操作系統(tǒng)家族最近增加的Windows NT為開發(fā)環(huán)境和應(yīng)用程序本身帶來了很大的改變,這之中一個(gè)最為重大的當(dāng)屬PE文件格式了。新的PE文件格式主要來自于UNIX操作系統(tǒng)所通用的COFF規(guī)范,同時(shí)為了保證與舊版本MS-DOS及Windows操作系統(tǒng)的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ頭部。
在本文之中,PE文件格式是以自頂而下的順序解釋的。在你從頭開始研究文件內(nèi)容的過程之中,本文會(huì)詳細(xì)討論P(yáng)E文件的每一個(gè)組成部分。
許多單獨(dú)的文件成分定義都來自于Microsoft Win32 SDK開發(fā)包中的WINNT.H文件,在這個(gè)文件中你會(huì)發(fā)現(xiàn)用來描述文件頭部和數(shù)據(jù)目錄等各種成分的結(jié)構(gòu)類型定義。但是,在WINNT.H中缺少對(duì)PE文件結(jié)構(gòu)足夠的定義,在這種情況下,我定義了自己的結(jié)構(gòu)來存取文件數(shù)據(jù)。你會(huì)在PEFILE.DLL工程的PEFILE.H中找到這些結(jié)構(gòu)的定義,整套的PEFILE.H開發(fā)文件包含在PEFile示例程序之中。
本文配套的示例程序除了PEFILE.DLL示例代碼之外,還有一個(gè)單獨(dú)的Win32示例應(yīng)用程序,名為EXEVIEW.EXE。創(chuàng)建這一示例目的有二:首先,我需要測(cè)試PEFILE.DLL的函數(shù),并且某些情況要求我同時(shí)查看多個(gè)文件;其次,很多解決PE文件格式的工作和直接觀看數(shù)據(jù)有關(guān)。例如,要弄懂導(dǎo)入地址名稱表是如何構(gòu)成的,我就得同時(shí)查看.idata段頭部、導(dǎo)入映像數(shù)據(jù)目錄、可選頭部以及當(dāng)前的.idata段實(shí)體,而EXEVIEW.EXE就是查看這些信息的最佳示例。
閑話少敘,讓我們開始吧。
PE文件結(jié)構(gòu)
PE文件格式被組織為一個(gè)線性的數(shù)據(jù)流,它由一個(gè)MS-DOS頭部開始,接著是一個(gè)是模式的程序殘余以及一個(gè)PE文件標(biāo)志,這之后緊接著PE文件頭和可選頭部。這些之后是所有的段頭部,段頭部之后跟隨著所有的段實(shí)體。文件的結(jié)束處是一些其它的區(qū)域,其中是一些混雜的信息,包括重分配信息、符號(hào)表信息、行號(hào)信息以及字串表數(shù)據(jù)。我將所有這些成分列于圖1。

圖1.PE文件映像結(jié)構(gòu)
從MS-DOS文件頭結(jié)構(gòu)開始,我將按照PE文件格式各成分的出現(xiàn)順序依次對(duì)其進(jìn)行討論,并且討論的大部分是以示例代碼為基礎(chǔ)來示范如何獲得文件的信息的。所有的源碼均摘自PEFILE.DLL模塊的PEFILE.C文件。這些示例都利用了Windows NT最酷的特色之一——內(nèi)存映射文件,這一特色允許用戶使用一個(gè)簡(jiǎn)單的指針來存取文件中所包含的數(shù)據(jù),因此所有的示例都使用了內(nèi)存映射文件來存取PE文件中的數(shù)據(jù)。
注意:請(qǐng)查閱本文末尾關(guān)于如何使用PEFILE.DLL的那一段。
MS-DOS頭部/實(shí)模式頭部
如上所述,PE文件格式的第一個(gè)組成部分是MS-DOS頭部。在PE文件格式中,它并非一個(gè)新概念,因?yàn)樗cMS-DOS 2.0以來就已有的MS-DOS頭部是完全一樣的。保留這個(gè)相同結(jié)構(gòu)的最主要原因是,當(dāng)你嘗試在Windows 3.1以下或MS-DOS 2.0以上的系統(tǒng)下裝載一個(gè)文件的時(shí)候,操作系統(tǒng)能夠讀取這個(gè)文件并明白它是和當(dāng)前系統(tǒng)不相兼容的。換句話說,當(dāng)你在MS-DOS 6.0下運(yùn)行一個(gè)Windows NT可執(zhí)行文件時(shí),你會(huì)得到這樣一條消息:“This program cannot be run in DOS mode.”如果MS-DOS頭部不是作為PE文件格式的第一部分的話,操作系統(tǒng)裝載文件的時(shí)候就會(huì)失敗,并提供一些完全沒用的信息,例如:“The name specified is not recognized as an internal or external command, operable program or batch file.”
MS-DOS頭部占據(jù)了PE文件的頭64個(gè)字節(jié),描述它內(nèi)容的結(jié)構(gòu)如下:
//WINNT.Htypedef struct _IMAGE_DOS_HEADER { // DOS的.EXE頭部USHORT e_magic; // 魔術(shù)數(shù)字USHORT e_cblp; // 文件最后頁(yè)的字節(jié)數(shù)USHORT e_cp; // 文件頁(yè)數(shù)USHORT e_crlc; // 重定義元素個(gè)數(shù)USHORT e_cparhdr; // 頭部尺寸,以段落為單位USHORT e_minalloc; // 所需的最小附加段USHORT e_maxalloc; // 所需的最大附加段USHORT e_ss; // 初始的SS值(相對(duì)偏移量)USHORT e_sp; // 初始的SP值USHORT e_csum; // 校驗(yàn)和USHORT e_ip; // 初始的IP值USHORT e_cs; // 初始的CS值(相對(duì)偏移量)USHORT e_lfarlc; // 重分配表文件地址USHORT e_ovno; // 覆蓋號(hào)USHORT e_res[4]; // 保留字USHORT e_oemid; // OEM標(biāo)識(shí)符(相對(duì)e_oeminfo)USHORT e_oeminfo; // OEM信息USHORT e_res2[10]; // 保留字LONG e_lfanew; // 新exe頭部的文件地址} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;第一個(gè)域e_magic,被稱為魔術(shù)數(shù)字,它被用于表示一個(gè)MS-DOS兼容的文件類型。所有MS-DOS兼容的可執(zhí)行文件都將這個(gè)值設(shè)為0x5A4D,表示ASCII字符MZ。MS-DOS頭部之所以有的時(shí)候被稱為MZ頭部,就是這個(gè)緣故。還有許多其它的域?qū)τ贛S-DOS操作系統(tǒng)來說都有用,但是對(duì)于Windows NT來說,這個(gè)結(jié)構(gòu)中只有一個(gè)有用的域——最后一個(gè)域e_lfnew,一個(gè)4字節(jié)的文件偏移量,PE文件頭部就是由它定位的。對(duì)于Windows NT的PE文件來說,PE文件頭部是緊跟在MS-DOS頭部和實(shí)模式程序殘余之后的。
實(shí)模式殘余程序
實(shí)模式殘余程序是一個(gè)在裝載時(shí)能夠被MS-DOS運(yùn)行的實(shí)際程序。對(duì)于一個(gè)MS-DOS的可執(zhí)行映像文件,應(yīng)用程序就是從這里執(zhí)行的。對(duì)于Windows、OS/2、Windows NT這些操作系統(tǒng)來說,MS-DOS殘余程序就代替了主程序的位置被放在這里。這種殘余程序通常什么也不做,而只是輸出一行文本,例如:“This program requires Microsoft Windows v3.1 or greater.”當(dāng)然,用戶可以在此放入任何的殘余程序,這就意味著你可能經(jīng)??吹较襁@樣的東西:“You can‘‘t run a Windows NT application on OS/2, it‘‘s simply not possible.”
當(dāng)為Windows 3.1構(gòu)建一個(gè)應(yīng)用程序的時(shí)候,鏈接器將向你的可執(zhí)行文件中鏈接一個(gè)名為WINSTUB.EXE的默認(rèn)殘余程序。你可以用一個(gè)基于MS-DOS的有效程序取代WINSTUB,并且用STUB模塊定義語(yǔ)句指示鏈接器,這樣就能夠取代鏈接器的默認(rèn)行為。為Windows NT開發(fā)的應(yīng)用程序可以通過使用-STUB:鏈接器選項(xiàng)來實(shí)現(xiàn)。
PE文件頭部與標(biāo)志
PE文件頭部是由MS-DOS頭部的e_lfanew域定位的,這個(gè)域只是給出了文件的偏移量,所以要確定PE頭部的實(shí)際內(nèi)存映射地址,就需要添加文件的內(nèi)存映射基地址。例如,以下的宏是包含在PEFILE.H源文件之中的:
//PEFILE.H#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew))在處理PE文件信息的時(shí)候,我發(fā)現(xiàn)文件之中有些位置需要經(jīng)常查閱。既然這些位置僅僅是對(duì)文件的偏移量,那么用宏來實(shí)現(xiàn)這些定位就比較容易,因?yàn)樗鼈冚^之函數(shù)有更好的表現(xiàn)。
請(qǐng)注意這個(gè)宏所獲得的是PE文件標(biāo)志,而并非PE文件頭部的偏移量。那是由于自Windows與OS/2的可執(zhí)行文件開始,.EXE文件都被賦予了目標(biāo)操作系統(tǒng)的標(biāo)志。對(duì)于Windows NT的PE文件格式而言,這一標(biāo)志在PE文件頭部結(jié)構(gòu)之前。在Windows和OS/2的某些版本中,這一標(biāo)志是文件頭的第一個(gè)字。同樣,對(duì)于PE文件格式,Windows NT使用了一個(gè)DWORD值。
以上的宏返回了文件標(biāo)志的偏移量,而不管它是哪種類型的可執(zhí)行文件。所以,文件頭部是在DWORD標(biāo)志之后,還是在WORD標(biāo)志處,是由這個(gè)標(biāo)志是否Windows NT文件標(biāo)志所決定的。要解決這個(gè)問題,我編寫了ImageFileType函數(shù)(如下),它返回了映像文件的類型:
//PEFILE.CDWORD WINAPI ImageFileType (LPVOID lpFile){/* 首先出現(xiàn)的是DOS文件標(biāo)志 */if (*(USHORT *)lpFile == IMAGE_DOS_SIGNATURE){/* 由DOS頭部決定PE文件頭部的位置 */if (LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==IMAGE_OS2_SIGNATURE ||LOWORD (*(DWORD *)NTSIGNATURE (lpFile)) ==IMAGE_OS2_SIGNATURE_LE)return (DWORD)LOWORD(*(DWORD *)NTSIGNATURE (lpFile));else if (*(DWORD *)NTSIGNATURE (lpFile) ==IMAGE_NT_SIGNATURE)return IMAGE_NT_SIGNATURE;elsereturn IMAGE_DOS_SIGNATURE;}else/* 不明文件種類 */return 0;}以上列出的代碼立即告訴了你NTSIGNATURE宏有多么有用。對(duì)于比較不同文件類型并且返回一個(gè)適當(dāng)?shù)奈募N類來說,這個(gè)宏就會(huì)使這兩件事變得非常簡(jiǎn)單。WINNT.H之中定義的四種不同文件類型有:
//WINNT.H#define IMAGE_DOS_SIGNATURE 0x5A4D // MZ#define IMAGE_OS2_SIGNATURE 0x454E // NE#define IMAGE_OS2_SIGNATURE_LE 0x454C // LE#define IMAGE_NT_SIGNATURE 0x00004550 // PE00首先,Windows的可執(zhí)行文件類型沒有出現(xiàn)在這一列表中,這一點(diǎn)看起來很奇怪。但是,在稍微研究一下之后,就能得到原因了:除了操作系統(tǒng)版本規(guī)范的不同之外,Windows的可執(zhí)行文件和OS/2的可執(zhí)行文件實(shí)在沒有什么區(qū)別。這兩個(gè)操作系統(tǒng)擁有相同的可執(zhí)行文件結(jié)構(gòu)。
現(xiàn)在把我們的注意力轉(zhuǎn)向Windows NT PE文件格式,我們會(huì)發(fā)現(xiàn)只要我們得到了文件標(biāo)志的位置,PE文件之后就會(huì)有4個(gè)字節(jié)相跟隨。下一個(gè)宏標(biāo)識(shí)了PE文件的頭部:
//PEFILE.C#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE))這個(gè)宏與上一個(gè)宏的唯一不同是這個(gè)宏加入了一個(gè)常量SIZE_OF_NT_SIGNATURE。不幸的是,這個(gè)常量并未定義在WINNT.H之中,于是我將它定義在了PEFILE.H中,它是一個(gè)DWORD的大小。
既然我們知道了PE文件頭的位置,那么就可以檢查頭部的數(shù)據(jù)了。我們只需要把這個(gè)位置賦值給一個(gè)結(jié)構(gòu),如下:
PIMAGE_FILE_HEADER pfh;pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);在這個(gè)例子中,lpFile表示一個(gè)指向可執(zhí)行文件內(nèi)存映像基地址的指針,這就顯出了內(nèi)存映射文件的好處:不需要執(zhí)行文件的I/O,只需使用指針pfh就能存取文件中的信息。PE文件頭結(jié)構(gòu)被定義為:
//WINNT.Htypedef struct _IMAGE_FILE_HEADER {USHORT Machine;USHORT NumberOfSections;ULONG TimeDateStamp;ULONG PointerToSymbolTable;ULONG NumberOfSymbols;USHORT SizeOfOptionalHeader;USHORT Characteristics;} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;#define IMAGE_SIZEOF_FILE_HEADER 20請(qǐng)注意這個(gè)文件頭部的大小已經(jīng)定義在這個(gè)包含文件之中了,這樣一來,想要得到這個(gè)結(jié)構(gòu)的大小就很方便了。但是我覺得對(duì)結(jié)構(gòu)本身使用sizeof運(yùn)算符(譯注:原文為“function”)更簡(jiǎn)單一些,因?yàn)檫@樣的話我就不必記住這個(gè)常量的名字IMAGE_SIZEOF_FILE_HEADER,而只需要記住結(jié)構(gòu)IMAGE_FILE_HEADER的名字就可以了。另一方面,記住所有結(jié)構(gòu)的名字已經(jīng)夠有挑戰(zhàn)性的了,尤其在是這些結(jié)構(gòu)只有WINNT.H中才有的情況下。
PE文件中的信息基本上是一些高級(jí)信息,這些信息是被操作系統(tǒng)或者應(yīng)用程序用來決定如何處理這個(gè)文件的。第一個(gè)域是用來表示這個(gè)可執(zhí)行文件被構(gòu)建的目標(biāo)機(jī)器種類,例如DEC(R) Alpha、MIPS R4000、Intel(R) x86或一些其它處理器。系統(tǒng)使用這一信息來在讀取這個(gè)文件的其它數(shù)據(jù)之前決定如何處理它。
Characteristics域表示了文件的一些特征。比如對(duì)于一個(gè)可執(zhí)行文件而言,分離調(diào)試文件是如何操作的。調(diào)試器通常使用的方法是將調(diào)試信息從PE文件中分離,并保存到一個(gè)調(diào)試文件(.DBG)中。要這么做的話,調(diào)試器需要了解是否要在一個(gè)單獨(dú)的文件中尋找調(diào)試信息,以及這個(gè)文件是否已經(jīng)將調(diào)試信息分離了。我們可以通過深入可執(zhí)行文件并尋找調(diào)試信息的方法來完成這一工作。要使調(diào)試器不在文件中查找的話,就需要用到IMAGE_FILE_DEBUG_STRIPPED這個(gè)特征,它表示文件的調(diào)試信息是否已經(jīng)被分離了。這樣一來,調(diào)試器可以通過快速查看PE文件的頭部的方法來決定文件中是否存在著調(diào)試信息。
WINNT.H定義了若干其它表示文件頭信息的標(biāo)記,就和以上的例子差不多。我把研究這些標(biāo)記的事情留給讀者作為練習(xí),由你們來看看它們是不是很有趣,這些標(biāo)記位于WINNT.H中的IMAGE_FILE_HEADER結(jié)構(gòu)之后。
PE文件頭結(jié)構(gòu)中另一個(gè)有用的入口是NumberOfSections域,它表示如果你要方便地提取文件信息的話,就需要了解多少個(gè)段——更明確一點(diǎn)來說,有多少個(gè)段頭部和多少個(gè)段實(shí)體。每一個(gè)段頭部和段實(shí)體都在文件中連續(xù)地排列著,所以要決定段頭部和段實(shí)體在哪里結(jié)束的話,段的數(shù)目是必需的。以下的函數(shù)從PE文件頭中提取了段的數(shù)目:
PEFILE.Cint WINAPI NumOfSections(LPVOID lpFile){/* 文件頭部中所表示出的段數(shù)目 */return (int)((PIMAGE_FILE_HEADER)PEFHDROFFSET (lpFile))->NumberOfSections);}如你所見,PEFHDROFFSET以及其它宏用起來非常方便。
PE可選頭部
PE可執(zhí)行文件中接下來的224個(gè)字節(jié)組成了PE可選頭部。雖然它的名字是“可選頭部”,但是請(qǐng)確信:這個(gè)頭部并非“可選”,而是“必需”的。OPTHDROFFSET宏可以獲得指向可選頭部的指針:
//PEFILE.H#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof(IMAGE_FILE_HEADER)))可選頭部包含了很多關(guān)于可執(zhí)行映像的重要信息,例如初始的堆棧大小、程序入口點(diǎn)的位置、首選基地址、操作系統(tǒng)版本、段對(duì)齊的信息等等。IMAGE_OPTIONAL_HEADER結(jié)構(gòu)如下:
//WINNT.Htypedef struct _IMAGE_OPTIONAL_HEADER {//// 標(biāo)準(zhǔn)域//USHORT Magic;UCHAR MajorLinkerVersion;UCHAR MinorLinkerVersion;ULONG SizeOfCode;ULONG SizeOfInitializedData;ULONG SizeOfUninitializedData;ULONG AddressOfEntryPoint;ULONG BaseOfCode;ULONG BaseOfData;//// NT附加域//ULONG ImageBase;ULONG SectionAlignment;ULONG FileAlignment;USHORT MajorOperatingSystemVersion;USHORT MinorOperatingSystemVersion;USHORT MajorImageVersion;USHORT MinorImageVersion;USHORT MajorSubsystemVersion;USHORT MinorSubsystemVersion;ULONG Reserved1;ULONG SizeOfImage;ULONG SizeOfHeaders;ULONG CheckSum;USHORT Subsystem;USHORT DllCharacteristics;ULONG SizeOfStackReserve;ULONG SizeOfStackCommit;ULONG SizeOfHeapReserve;ULONG SizeOfHeapCommit;ULONG LoaderFlags;ULONG NumberOfRvaAndSizes;IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;如你所見,這個(gè)結(jié)構(gòu)中所列出的域?qū)嵲谑侨唛L(zhǎng)得過分。為了不讓你對(duì)所有這些域感到厭煩,我會(huì)僅僅討論有用的——就是說,對(duì)于探究PE文件格式而言有用的。
標(biāo)準(zhǔn)域
首先,請(qǐng)注意這個(gè)結(jié)構(gòu)被劃分為“標(biāo)準(zhǔn)域”和“NT附加域”。所謂標(biāo)準(zhǔn)域,就是和UNIX可執(zhí)行文件的COFF格式所公共的部分。雖然標(biāo)準(zhǔn)域保留了COFF中定義的名字,但是Windows NT仍然將它們用作了不同的目的——盡管換個(gè)名字更好一些。
·Magic。我不知道這個(gè)域是干什么的,對(duì)于示例程序EXEVIEW.EXE示例程序而言,這個(gè)值是0x010B或267(譯注:0x010B為.EXE,0x0107為ROM映像,這個(gè)信息我是從eXeScope上得來的)。
·MajorLinkerVersion、MinorLinkerVersion。表示鏈接此映像的鏈接器版本。隨Window NT build 438配套的Windows NT SDK包含的鏈接器版本是2.39(十六進(jìn)制為2.27)。
·SizeOfCode??蓤?zhí)行代碼尺寸。
·SizeOfInitializedData。已初始化的數(shù)據(jù)尺寸。
·SizeOfUninitializedData。未初始化的數(shù)據(jù)尺寸。
·AddressOfEntryPoint。在標(biāo)準(zhǔn)域中,AddressOfEntryPoint域是對(duì)PE文件格式來說最為有趣的了。這個(gè)域表示應(yīng)用程序入口點(diǎn)的位置。并且,對(duì)于系統(tǒng)黑客來說,這個(gè)位置就是導(dǎo)入地址表(IAT)的末尾。以下的函數(shù)示范了如何從可選頭部獲得Windows NT可執(zhí)行映像的入口點(diǎn)。
//PEFILE.CLPVOID WINAPI GetModuleEntryPoint(LPVOID lpFile){PIMAGE_OPTIONAL_HEADER poh;poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);if (poh != NULL)return (LPVOID)poh->AddressOfEntryPoint;elsereturn NULL;}·BaseOfCode。已載入映像的代碼(“.text”段)的相對(duì)偏移量。
·BaseOfData。已載入映像的未初始化數(shù)據(jù)(“.bss”段)的相對(duì)偏移量。
Windows NT附加域
添加到Windows NT PE文件格式中的附加域?yàn)閃indows NT特定的進(jìn)程行為提供了裝載器的支持,以下為這些域的概述。
·ImageBase。進(jìn)程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK鏈接器將這個(gè)值默認(rèn)設(shè)為0x00400000,但是你可以使用-BASE:linker開關(guān)改變這個(gè)值。
·SectionAlignment。從ImageBase開始,每個(gè)段都被相繼的裝入進(jìn)程的地址空間中。SectionAlignment則規(guī)定了裝載時(shí)段能夠占據(jù)的最小空間數(shù)量——就是說,段是關(guān)于SectionAlignment對(duì)齊的。
Windows NT虛擬內(nèi)存管理器規(guī)定,段對(duì)齊不能少于頁(yè)尺寸(當(dāng)前的x86平臺(tái)是4096字節(jié)),并且必須是成倍的頁(yè)尺寸。4096字節(jié)是x86鏈接器的默認(rèn)值,但是它可以通過-ALIGN: linker開關(guān)來設(shè)置。
·FileAlignment。映像文件首先裝載的最小的信息塊間隔。例如,鏈接器將一個(gè)段實(shí)體(段的原始數(shù)據(jù))加零擴(kuò)展為文件中最接近的FileAlignment邊界。早先提及的2.39版鏈接器將映像文件以0x200字節(jié)的邊界對(duì)齊,這個(gè)值可以被強(qiáng)制改為512到65535這么多。
·MajorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的主版本號(hào);通常對(duì)Windows NT 1.0而言,這個(gè)值被設(shè)為1。
·MinorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的次版本號(hào);通常對(duì)Windows NT 1.0而言,這個(gè)值被設(shè)為0。
·MajorImageVersion。用來表示應(yīng)用程序的主版本號(hào);對(duì)于Microsoft Excel 4.0而言,這個(gè)值是4。
·MinorImageVersion。用來表示應(yīng)用程序的次版本號(hào);對(duì)于Microsoft Excel 4.0而言,這個(gè)值是0。
·MajorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的主版本號(hào);通常對(duì)于Windows NT 3.10而言,這個(gè)值被設(shè)為3。
·MinorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的次版本號(hào);通常對(duì)于Windows NT 3.10而言,這個(gè)值被設(shè)為10。
·Reserved1。未知目的,通常不被系統(tǒng)使用,并被鏈接器設(shè)為0。
·SizeOfImage。表示載入的可執(zhí)行映像的地址空間中要保留的地址空間大小,這個(gè)數(shù)字很大程度上受SectionAlignment的影響。例如,考慮一個(gè)擁有固定頁(yè)尺寸4096字節(jié)的系統(tǒng),如果你有一個(gè)11個(gè)段的可執(zhí)行文件,它的每個(gè)段都少于4096字節(jié),并且關(guān)于65536字節(jié)邊界對(duì)齊,那么SizeOfImage域?qū)?huì)被設(shè)為11 * 65536 = 720896(176頁(yè))。而如果一個(gè)相同的文件關(guān)于4096字節(jié)對(duì)齊的話,那么SizeOfImage域的結(jié)果將是11 * 4096 = 45056(11頁(yè))。這只是個(gè)簡(jiǎn)單的例子,它說明每個(gè)段需要少于一個(gè)頁(yè)面的內(nèi)存。在現(xiàn)實(shí)中,鏈接器通過個(gè)別地計(jì)算每個(gè)段的方法來決定SizeOfImage確切的值。它首先決定每個(gè)段需要多少字節(jié),并且最后將頁(yè)面總數(shù)向上取整至最接近的SectionAlignment邊界,然后總數(shù)就是每個(gè)段個(gè)別需求之和了。
·SizeOfHeaders。這個(gè)域表示文件中有多少空間用來保存所有的文件頭部,包括MS-DOS頭部、PE文件頭部、PE可選頭部以及PE段頭部。文件中所有的段實(shí)體就開始于這個(gè)位置。
·CheckSum。校驗(yàn)和是用來在裝載時(shí)驗(yàn)證可執(zhí)行文件的,它是由鏈接器設(shè)置并檢驗(yàn)的。由于創(chuàng)建這些校驗(yàn)和的算法是私有信息,所以在此不進(jìn)行討論。
·Subsystem。用于標(biāo)識(shí)該可執(zhí)行文件目標(biāo)子系統(tǒng)的域。每個(gè)可能的子系統(tǒng)取值列于WINNT.H的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)之后。
·DllCharacteristics。用來表示一個(gè)DLL映像是否為進(jìn)程和線程的初始化及終止包含入口點(diǎn)的標(biāo)記。
·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit。這些域控制要保留的地址空間數(shù)量,并且負(fù)責(zé)棧和默認(rèn)堆的申請(qǐng)。在默認(rèn)情況下,棧和堆都擁有1個(gè)頁(yè)面的申請(qǐng)值以及16個(gè)頁(yè)面的保留值。這些值可以使用鏈接器開關(guān)-STACKSIZE:與-HEAPSIZE:來設(shè)置。
·LoaderFlags。告知裝載器是否在裝載時(shí)中止和調(diào)試,或者默認(rèn)地正常運(yùn)行。
·NumberOfRvaAndSizes。這個(gè)域標(biāo)識(shí)了接下來的DataDirectory數(shù)組。請(qǐng)注意它被用來標(biāo)識(shí)這個(gè)數(shù)組,而不是數(shù)組中的各個(gè)入口數(shù)字,這一點(diǎn)非常重要。
·DataDirectory。數(shù)據(jù)目錄表示文件中其它可執(zhí)行信息重要組成部分的位置。它事實(shí)上就是一個(gè)IMAGE_DATA_DIRECTORY結(jié)構(gòu)的數(shù)組,位于可選頭部結(jié)構(gòu)的末尾。當(dāng)前的PE文件格式定義了16種可能的數(shù)據(jù)目錄,這之中的11種現(xiàn)在在使用中。
數(shù)據(jù)目錄
WINNT.H之中所定義的數(shù)據(jù)目錄為:
//WINNT.H// 目錄入口// 導(dǎo)出目錄#define IMAGE_DIRECTORY_ENTRY_EXPORT 0// 導(dǎo)入目錄#define IMAGE_DIRECTORY_ENTRY_IMPORT 1// 資源目錄#define IMAGE_DIRECTORY_ENTRY_RESOURCE 2// 異常目錄#define IMAGE_DIRECTORY_ENTRY_EXCEPTION 3// 安全目錄#define IMAGE_DIRECTORY_ENTRY_SECURITY 4// 重定位基本表#define IMAGE_DIRECTORY_ENTRY_BASERELOC 5// 調(diào)試目錄#define IMAGE_DIRECTORY_ENTRY_DEBUG 6// 描述字串#define IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7// 機(jī)器值(MIPS GP)#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8// TLS目錄#define IMAGE_DIRECTORY_ENTRY_TLS 9// 載入配置目錄#define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10基本上,每個(gè)數(shù)據(jù)目錄都是一個(gè)被定義為IMAGE_DATA_DIRECTORY的結(jié)構(gòu)。雖然數(shù)據(jù)目錄入口本身是相同的,但是每個(gè)特定的目錄種類卻是完全唯一的。每個(gè)數(shù)據(jù)目錄的定義在本文的以后部分被描述為“預(yù)定義段”。
//WINNT.Htypedef struct _IMAGE_DATA_DIRECTORY {ULONG VirtualAddress;ULONG Size;} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;每個(gè)數(shù)據(jù)目錄入口指定了該目錄的尺寸和相對(duì)虛擬地址。如果你要定義一個(gè)特定的目錄的話,就需要從可選頭部中的數(shù)據(jù)目錄數(shù)組中決定相對(duì)的地址,然后使用虛擬地址來決定該目錄位于哪個(gè)段中。一旦你決定了哪個(gè)段包含了該目錄,該段的段頭部就會(huì)被用于查找數(shù)據(jù)目錄的精確文件偏移量位置。
所以要獲得一個(gè)數(shù)據(jù)目錄的話,那么首先你需要了解段的概念。我在下面會(huì)對(duì)其進(jìn)行描述,這個(gè)討論之后還有一個(gè)有關(guān)如何定位數(shù)據(jù)目錄的示例。
PE文件段
PE文件規(guī)范由目前為止定義的那些頭部以及一個(gè)名為“段”的一般對(duì)象組成。段包含了文件的內(nèi)容,包括代碼、數(shù)據(jù)、資源以及其它可執(zhí)行信息,每個(gè)段都有一個(gè)頭部和一個(gè)實(shí)體(原始數(shù)據(jù))。我將在下面描述段頭部的有關(guān)信息,但是段實(shí)體則缺少一個(gè)嚴(yán)格的文件結(jié)構(gòu)。因此,它們幾乎可以被鏈接器按任何的方法組織,只要它的頭部填充了足夠能夠解釋數(shù)據(jù)的信息。
段頭部
PE文件格式中,所有的段頭部位于可選頭部之后。每個(gè)段頭部為40個(gè)字節(jié)長(zhǎng),并且沒有任何的填充信息。段頭部被定義為以下的結(jié)構(gòu):
//WINNT.H#define IMAGE_SIZEOF_SHORT_NAME 8typedef struct _IMAGE_SECTION_HEADER {UCHAR Name[IMAGE_SIZEOF_SHORT_NAME];union {ULONG PhysicalAddress;ULONG VirtualSize;} Misc;ULONG VirtualAddress;ULONG SizeOfRawData;ULONG PointerToRawData;ULONG PointerToRelocations;ULONG PointerToLinenumbers;USHORT NumberOfRelocations;USHORT NumberOfLinenumbers;ULONG Characteristics;} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;你如何才能獲得一個(gè)特定段的段頭部信息?既然段頭部是被連續(xù)的組織起來的,而且沒有一個(gè)特定的順序,那么段頭部必須由名稱來定位。以下的函數(shù)示范了如何從一個(gè)給定了段名稱的PE映像文件中獲得一個(gè)段頭部:
//PEFILE.CBOOL WINAPI GetSectionHdrByName(LPVOID lpFile, IMAGE_SECTION_HEADER *sh, char *szSection){PIMAGE_SECTION_HEADER psh;int nSections = NumOfSections (lpFile);int i;if ((psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile))!= NULL){/* 由名稱查找段 */for (i = 0; i < nSections; i++){if (!strcmp(psh->Name, szSection)){/* 向頭部復(fù)制數(shù)據(jù) */CopyMemory((LPVOID)sh, (LPVOID)psh,sizeof(IMAGE_SECTION_HEADER));return TRUE;}elsepsh++;}}return FALSE;}這個(gè)函數(shù)通過SECHDROFFSET宏將第一個(gè)段頭部定位,然后它開始在所有段中循環(huán),并將要尋找的段名稱和每個(gè)段的名稱相比較,直到找到了正確的那一個(gè)為止。當(dāng)找到了段的時(shí)候,函數(shù)將內(nèi)存映像文件的數(shù)據(jù)復(fù)制到傳入函數(shù)的結(jié)構(gòu)中,然后IMAGE_SECTION_HEADER結(jié)構(gòu)的各域就能夠被直接存取了。
段頭部的域
·Name。每個(gè)段都有一個(gè)8字符長(zhǎng)的名稱域,并且第一個(gè)字符必須是一個(gè)句點(diǎn)。
·PhysicalAddress或VirtualSize。第二個(gè)域是一個(gè)union域,現(xiàn)在已不使用了。
·VirtualAddress。這個(gè)域標(biāo)識(shí)了進(jìn)程地址空間中要裝載這個(gè)段的虛擬地址。實(shí)際的地址由將這個(gè)域的值加上可選頭部結(jié)構(gòu)中的ImageBase虛擬地址得到。切記,如果這個(gè)映像文件是一個(gè)DLL,那么這個(gè)DLL就不一定會(huì)裝載到ImageBase要求的位置。所以一旦這個(gè)文件被裝載進(jìn)入了一個(gè)進(jìn)程,實(shí)際的ImageBase值應(yīng)該通過使用GetModuleHandle來檢驗(yàn)。
·SizeOfRawData。這個(gè)域表示了相對(duì)FileAlignment的段實(shí)體尺寸。文件中實(shí)際的段實(shí)體尺寸將少于或等于FileAlignment的整倍數(shù)。一旦映像被裝載進(jìn)入了一個(gè)進(jìn)程的地址空間,段實(shí)體的尺寸將會(huì)變得少于或等于FileAlignment的整倍數(shù)。
·PointerToRawData。這是一個(gè)文件中段實(shí)體位置的偏移量。
·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。這些域在PE格式中不使用。
·Characteristics。定義了段的特征。這些值可以在WINNT.H及本光盤(譯注:MSDN的光盤)的PE格式規(guī)范中找到。
值 定義
0x00000020 代碼段
0x00000040 已初始化數(shù)據(jù)段
0x00000080 未初始化數(shù)據(jù)段
0x04000000 該段數(shù)據(jù)不能被緩存
0x08000000 該段不能被分頁(yè)
0x10000000 共享段
0x20000000 可執(zhí)行段
0x40000000 可讀段
0x80000000 可寫段
定位數(shù)據(jù)目錄
數(shù)據(jù)目錄存在于它們相應(yīng)的數(shù)據(jù)段中。典型地來說,數(shù)據(jù)目錄是段實(shí)體中的第一個(gè)結(jié)構(gòu),但不是必需的。由于這個(gè)緣故,如果你需要定位一個(gè)指定的數(shù)據(jù)目錄的話,就需要從段頭部和可選頭部中獲得信息。
為了讓這個(gè)過程簡(jiǎn)單一點(diǎn),我編寫了以下的函數(shù)來定位任何一個(gè)在WINNT.H之中定義的數(shù)據(jù)目錄。
// PEFILE.CLPVOID WINAPI ImageDirectoryOffset(LPVOID lpFile,DWORD dwIMAGE_DIRECTORY){PIMAGE_OPTIONAL_HEADER poh;PIMAGE_SECTION_HEADER psh;int nSections = NumOfSections(lpFile);int i = 0;LPVOID VAImageDir;/* 必須為0到(NumberOfRvaAndSizes-1)之間 */if (dwIMAGE_DIRECTORY >= poh->NumberOfRvaAndSizes)return NULL;/* 獲得可選頭部和段頭部的偏移量 */poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);psh = (PIMAGE_SECTION_HEADER)SECHDROFFSET(lpFile);/* 定位映像目錄的相對(duì)虛擬地址 */VAImageDir = (LPVOID)poh->DataDirectory[dwIMAGE_DIRECTORY].VirtualAddress;/* 定位包含映像目錄的段 */while (i++ < nSections){if (psh->VirtualAddress <= (DWORD)VAImageDir &&psh->VirtualAddress +psh->SizeOfRawData > (DWORD)VAImageDir)break;psh++;}if (i > nSections)return NULL;/* 返回映像導(dǎo)入目錄的偏移量 */return (LPVOID)(((int)lpFile +(int)VAImageDir. psh->VirtualAddress) +(int)psh->PointerToRawData);}該函數(shù)首先確認(rèn)被請(qǐng)求的數(shù)據(jù)目錄入口數(shù)字,然后它分別獲取指向可選頭部和第一個(gè)段頭部的兩個(gè)指針。它從可選頭部決定數(shù)據(jù)目錄的虛擬地址,然后它使用這個(gè)值來決定數(shù)據(jù)目錄定位在哪個(gè)段實(shí)體之中。如果適當(dāng)?shù)亩螌?shí)體已經(jīng)被標(biāo)識(shí)了,那么數(shù)據(jù)目錄特定的位置就可以通過將它的相對(duì)虛擬地址轉(zhuǎn)換為文件中地址的方法來找到。
預(yù)定義段
一個(gè)Windows NT的應(yīng)用程序典型地?fù)碛?個(gè)預(yù)定義段,它們是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些應(yīng)用程序不需要所有的這些段,同樣還有一些應(yīng)用程序?yàn)榱俗约禾厥獾男枰x了更多的段。這種做法與MS-DOS和Windows 3.1中的代碼段和數(shù)據(jù)段相似。事實(shí)上,應(yīng)用程序定義一個(gè)獨(dú)特的段的方法是使用標(biāo)準(zhǔn)編譯器來指示對(duì)代碼段和數(shù)據(jù)段的命名,或者使用名稱段編譯器選項(xiàng)-NT——就和Windows 3.1中應(yīng)用程序定義獨(dú)特的代碼段和數(shù)據(jù)段一樣。
以下是一個(gè)關(guān)于Windows NT PE文件之中一些有趣的公共段的討論。
可執(zhí)行代碼段,.text
Windows 3.1和Windows NT之間的一個(gè)區(qū)別就是Windows NT默認(rèn)的做法是將所有的代碼段(正如它們?cè)赪indows 3.1中所提到的那樣)組成了一個(gè)單獨(dú)的段,名為“.text”。既然Windows NT使用了基于頁(yè)面的虛擬內(nèi)存管理系統(tǒng),那么將分開的代碼放入不同的段之中的做法就不太明智了。因此,擁有一個(gè)大的代碼段對(duì)于操作系統(tǒng)和應(yīng)用程序開發(fā)者來說,都是十分方便的。
.text段也包含了早先提到過的入口點(diǎn)。IAT亦存在于.text段之中的模塊入口點(diǎn)之前。(IAT在.text段之中的存在非常有意義,因?yàn)檫@個(gè)表事實(shí)上是一系列的跳轉(zhuǎn)指令,并且它們的跳轉(zhuǎn)目標(biāo)位置是已固定的地址。)當(dāng)Windows NT的可執(zhí)行映像裝載入進(jìn)程的地址空間時(shí),IAT就和每一個(gè)導(dǎo)入函數(shù)的物理地址一同確定了。要在.text段之中查找IAT,裝載器只用將模塊的入口點(diǎn)定位,而IAT恰恰出現(xiàn)于入口點(diǎn)之前。既然每個(gè)入口擁有相同的尺寸,那么向后退查找這個(gè)表的起始位置就很容易了。
數(shù)據(jù)段,.bss、.rdata、.data
.bss段表示應(yīng)用程序的未初始化數(shù)據(jù),包括所有函數(shù)或源模塊中聲明為static的變量。
.rdata段表示只讀的數(shù)據(jù),比如字符串文字量、常量和調(diào)試目錄信息。
所有其它變量(除了出現(xiàn)在棧上的自動(dòng)變量)存儲(chǔ)在.data段之中?;旧希@些是應(yīng)用程序或模塊的全局變量。
資源段,.rsrc
.rsrc段包含了模塊的資源信息。它起始于一個(gè)資源目錄結(jié)構(gòu),這個(gè)結(jié)構(gòu)就像其它大多數(shù)結(jié)構(gòu)一樣,但是它的數(shù)據(jù)被更進(jìn)一步地組織在了一棵資源樹之中。以下的IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)形成了這棵樹的根和各個(gè)結(jié)點(diǎn)。
//WINNT.Htypedef struct _IMAGE_RESOURCE_DIRECTORY {ULONG Characteristics;ULONG TimeDateStamp;USHORT MajorVersion;USHORT MinorVersion;USHORT NumberOfNamedEntries;USHORT NumberOfIdEntries;} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;請(qǐng)看這個(gè)目錄結(jié)構(gòu),你將會(huì)發(fā)現(xiàn)其中竟然沒有指向下一個(gè)結(jié)點(diǎn)的指針。但是,在這個(gè)結(jié)構(gòu)中有兩個(gè)域NumberOfNamedEntries和NumberOfIdEntries代替了指針,它們被用來表示這個(gè)目錄附有多少入口。附帶說一句,我的意思是目錄入口就在段數(shù)據(jù)之中的目錄后邊。有名稱的入口按字母升序出現(xiàn),再往后是按數(shù)值升序排列的ID入口。
一個(gè)目錄入口由兩個(gè)域組成,正如下面IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)所描述的那樣:
// WINNT.Htypedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {ULONG Name;ULONG OffsetToData;} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;根據(jù)樹的層級(jí)不同,這兩個(gè)域也就有著不同的用途。Name域被用于標(biāo)識(shí)一個(gè)資源種類,或者一種資源名稱,或者一個(gè)資源的語(yǔ)言ID。OffsetToData與常常被用來在樹之中指向兄弟結(jié)點(diǎn)——即一個(gè)目錄結(jié)點(diǎn)或一個(gè)葉子結(jié)點(diǎn)。
葉子結(jié)點(diǎn)是資源樹之中最底層的結(jié)點(diǎn),它們定義了當(dāng)前資源數(shù)據(jù)的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu)被用于描述每個(gè)葉子結(jié)點(diǎn):
// WINNT.Htypedef struct _IMAGE_RESOURCE_DATA_ENTRY {ULONG OffsetToData;ULONG Size;ULONG CodePage;ULONG Reserved;} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;OffsetToData和Size這兩個(gè)域表示了當(dāng)前資源數(shù)據(jù)的位置和尺寸。既然這一信息主要是在應(yīng)用程序裝載以后由函數(shù)使用的,那么將OffsetToData作為一個(gè)相對(duì)虛擬的地址會(huì)更有意義一些。——幸甚,恰好是這樣沒錯(cuò)。非常有趣的是,所有其它的偏移量,比如從目錄入口到其它目錄的指針,都是相對(duì)于根結(jié)點(diǎn)位置的偏移量。
要更清楚地了解這些內(nèi)容,請(qǐng)參考圖2。

圖2.一個(gè)簡(jiǎn)單的資源樹結(jié)構(gòu)
圖2描述了一個(gè)非常簡(jiǎn)單的資源樹,它包含了僅僅兩個(gè)資源對(duì)象:一個(gè)菜單和一個(gè)字串表。更深一層地來說,它們各自都有一個(gè)子項(xiàng)。然而,你仍然可以看到資源樹有多么復(fù)雜——即使它像這個(gè)一樣只有一點(diǎn)點(diǎn)資源。
在樹的根部,第一個(gè)目錄有一個(gè)文件中包含的所有資源種類的入口,而不管資源種類有多少。在圖2中,有兩個(gè)由樹根標(biāo)識(shí)的入口,一個(gè)是菜單的,另一個(gè)是字串表的。如果文件中擁有一個(gè)或多個(gè)對(duì)話框資源,那么根結(jié)點(diǎn)會(huì)再擁有一個(gè)入口,因此,就有了對(duì)話框資源的另一個(gè)分支。
WINUSER.H中標(biāo)識(shí)了基本的資源種類,我將它們列到了下面:
//WINUSER.H/** 預(yù)定義的資源種類*/#define RT_CURSOR MAKEINTRESOURCE(1)#define RT_BITMAP MAKEINTRESOURCE(2)#define RT_ICON MAKEINTRESOURCE(3)#define RT_MENU MAKEINTRESOURCE(4)#define RT_DIALOG MAKEINTRESOURCE(5)#define RT_STRING MAKEINTRESOURCE(6)#define RT_FONTDIR MAKEINTRESOURCE(7)#define RT_FONT MAKEINTRESOURCE(8)#define RT_ACCELERATOR MAKEINTRESOURCE(9)#define RT_RCDATA MAKEINTRESOURCE(10)#define RT_MESSAGETABLE MAKEINTRESOURCE(11)在樹的第一層級(jí),以上列出的MAKEINTRESOURCE值被放置在每個(gè)種類入口的Name處,它標(biāo)識(shí)了不同的資源種類。
每個(gè)根目錄的入口都指向了樹中第二層級(jí)的一個(gè)兄弟結(jié)點(diǎn),這些結(jié)點(diǎn)也是目錄,并且每個(gè)都擁有它們自己的入口。在這一層級(jí),目錄被用來以給定的種類標(biāo)識(shí)每一個(gè)資源種類。如果你的應(yīng)用程序中有多個(gè)菜單,那么樹中的第二層級(jí)會(huì)為每個(gè)菜單都準(zhǔn)備一個(gè)入口。
你可能意識(shí)到了,資源可以由名稱或整數(shù)標(biāo)識(shí)。在這一層級(jí),它們是通過目錄結(jié)構(gòu)的Name域來分辨的。如果如果Name域最重要的位被設(shè)置了,那么其它的31個(gè)位就會(huì)被用作一個(gè)到IMAGE_RESOURCE_DIR_STRING_U結(jié)構(gòu)的偏移量。
// WINNT.Htypedef struct _IMAGE_RESOURCE_DIR_STRING_U {USHORT Length;WCHAR NameString[1];} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;這個(gè)結(jié)構(gòu)僅僅是由一個(gè)2字節(jié)長(zhǎng)的Length域和一個(gè)UNICODE字符Length組成的。
另一方面,如果Name域最重要的位被清空,那么它的低31位就被用于表示資源的整數(shù)ID。圖2示范的就是菜單資源作為一個(gè)命名的資源,以及字串表作為一個(gè)ID資源。
如果有兩個(gè)菜單資源,一個(gè)由名稱標(biāo)識(shí),另一個(gè)由資源標(biāo)識(shí),那么它們二者就會(huì)在菜單資源目錄之后擁有兩個(gè)入口。有名稱的資源入口在第一位,之后是由整數(shù)標(biāo)識(shí)的資源。目錄域NumberOfNamedEntries和NumberOfIdEntries將各自包含值1,表示當(dāng)前的1個(gè)入口。
在第二層級(jí)的下面,資源樹就不再更深一步地?cái)U(kuò)展分支了。第一層級(jí)分支至表示每個(gè)資源種類的目錄中,第二層級(jí)分支至由標(biāo)識(shí)符表示的每個(gè)資源的目錄中,第三層級(jí)是被個(gè)別標(biāo)識(shí)的資源與它們各自的語(yǔ)言ID之間一對(duì)一的映射。要表示一個(gè)資源的語(yǔ)言ID,目錄入口結(jié)構(gòu)的Name域就被用來表示資源的主語(yǔ)言ID和子語(yǔ)言ID了。Windows NT的Win32 SDK開發(fā)包中列出了默認(rèn)的值資源,例如對(duì)于0x0409這個(gè)值來說,0x09表示主語(yǔ)言LANG_ENGLISH,0x04則被定義為子語(yǔ)言的SUBLANG_ENGLISH_CAN。所有的語(yǔ)言ID值都定義于Windows NT Win32 SDK開發(fā)包的文件WINNT.H中。
既然語(yǔ)言ID結(jié)點(diǎn)是樹中最后的目錄結(jié)點(diǎn),那么入口結(jié)構(gòu)的OffsetToData域就是到一個(gè)葉子結(jié)點(diǎn)(即前面提到過的IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu))的偏移量。
再回過頭來參考圖2,你會(huì)發(fā)現(xiàn)每個(gè)語(yǔ)言目錄入口都對(duì)應(yīng)著一個(gè)數(shù)據(jù)入口。這個(gè)結(jié)點(diǎn)僅僅表示了資源數(shù)據(jù)的尺寸以及資源數(shù)據(jù)的相對(duì)虛擬地址。
在資源數(shù)據(jù)段(.rsrc)之中擁有這么多結(jié)構(gòu)有一個(gè)好處,就是你可以不存取資源本身而直接可以從這個(gè)段收集很多信息。例如,你可以獲得有多少種資源、哪些資源(如果有的話)使用了特別的語(yǔ)言ID、特定的資源是否存在以及單獨(dú)種類資源的尺寸。為了示范如何利用這一信息,以下的函數(shù)說明了如何決定一個(gè)文件中包含的不同種類的資源:
// PEFILE.Cint WINAPI GetListOfResourceTypes(LPVOID lpFile, HANDLE hHeap, char **pszResTypes){PIMAGE_RESOURCE_DIRECTORY prdRoot;PIMAGE_RESOURCE_DIRECTORY_ENTRY prde;char *pMem;int nCnt, i;/* 獲得資源樹的根目錄 */if ((prdRoot = (PIMAGE_RESOURCE_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_RESOURCE)) == NULL)return 0;/* 在堆上分配足夠的空間來包括所有類型 */nCnt = prdRoot->NumberOfIdEntries * (MAXRESOURCENAME + 1);*pszResTypes = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY,nCnt);if ((pMem = *pszResTypes) == NULL)return 0;/* 將指針指向第一個(gè)資源種類的入口 */prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot +sizeof (IMAGE_RESOURCE_DIRECTORY));/* 在所有的資源目錄入口類型中循環(huán) */for (i = 0; i < prdRoot->NumberOfIdEntries; i++){if (LoadString(hDll, prde->Name, pMem, MAXRESOURCENAME))pMem += strlen(pMem) + 1;prde++;}return nCnt;}這個(gè)函數(shù)將一個(gè)資源種類名稱的列表寫入了由pszResTypes標(biāo)識(shí)的變量中。請(qǐng)注意,在這個(gè)函數(shù)的核心部分,LoadString是使用各自資源種類目錄入口的Name域來作為字符串ID的。如果你查看PEFILE.RC,你會(huì)發(fā)現(xiàn)我定義了一系列的資源種類的字符串,并且它們的ID與它們?cè)谀夸浫肟谥械亩x完全相同。PEFILE.DLL還有有一個(gè)函數(shù),它返回了.rsrc段中的資源對(duì)象總數(shù)。這樣一來,從這個(gè)段中提取其它的信息,借助這些函數(shù)或另外編寫函數(shù)就方便多了。
導(dǎo)出數(shù)據(jù)段,.edata
.edata段包含了應(yīng)用程序或DLL的導(dǎo)出數(shù)據(jù)。在這個(gè)段出現(xiàn)的時(shí)候,它會(huì)包含一個(gè)到達(dá)導(dǎo)出信息的導(dǎo)出目錄。
// WINNT.Htypedef struct _IMAGE_EXPORT_DIRECTORY {ULONG Characteristics;ULONG TimeDateStamp;USHORT MajorVersion;USHORT MinorVersion;ULONG Name;ULONG Base;ULONG NumberOfFunctions;ULONG NumberOfNames;PULONG *AddressOfFunctions;PULONG *AddressOfNames;PUSHORT *AddressOfNameOrdinals;} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;導(dǎo)出目錄中的Name域標(biāo)識(shí)了可執(zhí)行模塊的名稱。NumberOfFunctions域和NumberOfNames域表示模塊中有多少導(dǎo)出的函數(shù)以及這些函數(shù)的名稱。
AddressOfFunctions域是一個(gè)到導(dǎo)出函數(shù)入口列表的偏移量。AddressOfNames域是到一個(gè)導(dǎo)出函數(shù)名稱列表起始處偏移量的地址,這個(gè)列表是由null分隔的。AddressOfNameOrdinals是一個(gè)到相同導(dǎo)出函數(shù)順序值(每個(gè)值2字節(jié)長(zhǎng))列表的偏移量。
三個(gè)AddressOf...域是當(dāng)模塊裝載時(shí)進(jìn)程地址空間中的相對(duì)虛擬地址。一旦模塊被裝載,那么要獲得進(jìn)程地質(zhì)空間中的確切地址的話,就應(yīng)該在相對(duì)虛擬地址上加上模塊的基地址??墒牵谖募谎b載前,仍然可以決定這一地址:只要從給定的域地址中減去段頭部的虛擬地址(VirtualAddress),再加上段實(shí)體的偏移量(PointerToRawData),這個(gè)結(jié)果就是映像文件中的偏移量了。以下的例子解說了這一技術(shù):
// PEFILE.Cint WINAPI GetExportFunctionNames(LPVOID lpFile, HANDLE hHeap, char **pszFunctions){IMAGE_SECTION_HEADER sh;PIMAGE_EXPORT_DIRECTORY ped;char *pNames, *pCnt;int i, nCnt;/* 獲得.edata域中的段頭部和指向數(shù)據(jù)目錄的指針 */if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)return 0;GetSectionHdrByName (lpFile, &sh, ".edata");/* 決定導(dǎo)出函數(shù)名稱的偏移量 */pNames = (char *)(*(int *)((int)ped->AddressOfNames -(int)sh.VirtualAddress + (int)sh.PointerToRawData +(int)lpFile) - (int)sh.VirtualAddress +(int)sh.PointerToRawData + (int)lpFile);/* 計(jì)算出要為所有的字符串分配多少內(nèi)存 */pCnt = pNames;for (i = 0; i < (int)ped->NumberOfNames; i++)while (*pCnt++);nCnt = (int)(pCnt.pNames);/* 在堆上為函數(shù)名稱分配內(nèi)存 */*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);/* 將所有字符串復(fù)制到緩沖區(qū) */CopyMemory((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);return nCnt;}請(qǐng)注意,在這個(gè)函數(shù)之中,變量pNames是由決定偏移量地址和當(dāng)前偏移量位置的方法來賦值的。偏移量的地址和偏移量本身都是相對(duì)虛擬地址,因此在使用之前必須進(jìn)行轉(zhuǎn)換——函數(shù)之中體現(xiàn)了這一點(diǎn)。雖然你可以編寫一個(gè)類似的函數(shù)來決定順序值或函數(shù)入口點(diǎn),但是我為什么不為你做好呢?——GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和GetExportFunctionOrdinals已經(jīng)存在于PEFILE.DLL之中了。
導(dǎo)入數(shù)據(jù)段,.idata
.idata段是導(dǎo)入數(shù)據(jù),包括導(dǎo)入庫(kù)和導(dǎo)入地址名稱表。雖然定義了IMAGE_DIRECTORY_ENTRY_IMPORT,但是WINNT.H之中并無(wú)相應(yīng)的導(dǎo)入目錄結(jié)構(gòu)。作為代替,其中有若干其它的結(jié)構(gòu),名為IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA與IMAGE_IMPORT_DESCRIPTOR。在我個(gè)人看來,我實(shí)在不知道這些結(jié)構(gòu)是如何和.idata段發(fā)生關(guān)聯(lián)的,所以我花了若干個(gè)小時(shí)來破譯.idata段實(shí)體并且得到了一個(gè)更簡(jiǎn)單的結(jié)構(gòu),我名之為IMAGE_IMPORT_MODULE_DIRECTORY。
// PEFILE.Htypedef struct tagImportDirectory{DWORD dwRVAFunctionNameList;DWORD dwUseless1;DWORD dwUseless2;DWORD dwRVAModuleName;DWORD dwRVAFunctionAddressList;} IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;和其它段的數(shù)據(jù)目錄不同的是,這個(gè)是作為文件中的每個(gè)導(dǎo)入模塊重復(fù)出現(xiàn)的。你可以將它看作模塊數(shù)據(jù)目錄列表中的一個(gè)入口,而不是一個(gè)整個(gè)數(shù)據(jù)段的數(shù)據(jù)目錄。每個(gè)入口都是一個(gè)指向特定模塊導(dǎo)入信息的目錄。
IMAGE_IMPORT_MODULE_DIRECTORY結(jié)構(gòu)中的一個(gè)域dwRVAModuleName是一個(gè)相對(duì)虛擬地址,它指向模塊的名稱。結(jié)構(gòu)中還有兩個(gè)dwUseless參數(shù),它們是為了保持段的對(duì)齊。PE文件格式規(guī)范提到了一些東西,關(guān)于導(dǎo)入標(biāo)記、時(shí)間/日期標(biāo)志以及主/次版本,但是在我的實(shí)驗(yàn)中,這兩個(gè)域自始而終都是空的,所以我仍然認(rèn)為它們沒有什么用處。
基于這個(gè)結(jié)構(gòu)的定義,你便可以獲得可執(zhí)行文件中導(dǎo)入的所有模塊和函數(shù)名稱了。以下的函數(shù)示范了如何獲得特定的PE文件中的所有導(dǎo)入函數(shù)名稱:
//PEFILE.Cint WINAPI GetImportModuleNames(LPVOID lpFile, HANDLE hHeap, char **pszModules){PIMAGE_IMPORT_MODULE_DIRECTORY pid;IMAGE_SECTION_HEADER idsh;BYTE *pData;int nCnt = 0, nSize = 0, i;char *pModule[1024];char *psz;pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);pData = (BYTE *)pid;/* 定位.idata段頭部 */if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))return 0;/* 提取所有導(dǎo)入模塊 */while (pid->dwRVAModuleName){/* 為絕對(duì)字符串偏移量分配緩沖區(qū) */pModule[nCnt] = (char *)(pData +(pid->dwRVAModuleName-idsh.VirtualAddress));nSize += strlen(pModule[nCnt]) + 1;/* 增至下一個(gè)導(dǎo)入目錄入口 */pid++;nCnt++;}/* 將所有字符串賦值到一大塊的堆內(nèi)存中 */*pszModules = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nSize);psz = *pszModules;for (i = 0; i < nCnt; i++){strcpy(psz, pModule[i]);psz += strlen (psz) + 1;}return nCnt;}這個(gè)函數(shù)非常好懂,然而有一點(diǎn)值得指出——注意while循環(huán)。這個(gè)循環(huán)當(dāng)pid->dwRVAModuleName為0的時(shí)候終止,這就暗示了在IMAGE_IMPORT_MODULE_DIRECTORY結(jié)構(gòu)列表的末尾有一個(gè)空的結(jié)構(gòu),這個(gè)結(jié)構(gòu)擁有一個(gè)0值,至少dwRVAModuleName域?yàn)?。這便是我在對(duì)文件的實(shí)驗(yàn)中以及之后在PE文件格式中研究的行為。
這個(gè)結(jié)構(gòu)中的第一個(gè)域dwRVAFunctionNameList是一個(gè)相對(duì)虛擬地址,這個(gè)地址指向一個(gè)相對(duì)虛擬地址的列表,這些地址是文件中的一些文件名。如下面的數(shù)據(jù)所示,所有導(dǎo)入模塊的模塊和函數(shù)名稱都列于.idata段數(shù)據(jù)中了:
E6A7 0000 F6A7 0000 08A8 0000 1AA8 0000 ................28A8 0000 3CA8 0000 4CA8 0000 0000 0000 (...<...L.......0000 4765 744F 7065 6E46 696C 654E 616D ..GetOpenFileNam6541 0000 636F 6D64 6C67 3332 2E64 6C6C eA..comdlg32.dll0000 2500 4372 6561 7465 466F 6E74 496E ..%.CreateFontIn6469 7265 6374 4100 4744 4933 322E 646C directA.GDI32.dl6C00 A000 4765 7444 6576 6963 6543 6170 l...GetDeviceCap7300 C600 4765 7453 746F 636B 4F62 6A65 s...GetStockObje6374 0000 D500 4765 7454 6578 744D 6574 ct....GetTextMet7269 6373 4100 1001 5365 6C65 6374 4F62 ricsA...SelectOb6A65 6374 0000 1601 5365 7442 6B43 6F6C ject....SetBkCol6F72 0000 3501 5365 7454 6578 7443 6F6C or..5.SetTextCol6F72 0000 4501 5465 7874 4F75 7441 0000 or..E.TextOutA..以上的數(shù)據(jù)是EXEVIEW.EXE示例程序.idata段的一部分。這個(gè)特別的段表示了導(dǎo)入模塊列表和函數(shù)名稱列表的起始處。如果你開始檢查數(shù)據(jù)中的這個(gè)段,你應(yīng)該認(rèn)出一些熟悉的Win32 API函數(shù)以及模塊名稱。從上往下讀的話,你可以找到GetOpenFileNameA,緊接著是COMDLG32.DLL。然后你能發(fā)現(xiàn)CreateFontIndirectA,緊接著是模塊GDI32.DLL,以及之后的GetDeviceCaps、GetStockObject、GetTextMetrics等等。
這樣的式樣會(huì)在.idata段中重復(fù)出現(xiàn)。第一個(gè)模塊是COMDLG32.DLL,第二個(gè)是GDI32.DLL。請(qǐng)注意第一個(gè)模塊只導(dǎo)出了一個(gè)函數(shù),而第二個(gè)模塊導(dǎo)出了很多函數(shù)。在這兩種情況下,函數(shù)和模塊的排列的方法是首先出現(xiàn)一個(gè)函數(shù)名,之后是模塊名,然后是其它的函數(shù)名(如果有的話)。
以下的函數(shù)示范了如何獲得指定模塊的所有函數(shù)名。
// PEFILE.Cint WINAPI GetImportFunctionNamesByModule(LPVOID lpFile, HANDLE hHeap,char *pszModule, char **pszFunctions){PIMAGE_IMPORT_MODULE_DIRECTORY pid;IMAGE_SECTION_HEADER idsh;DWORD dwBase;int nCnt = 0, nSize = 0;DWORD dwFunction;char *psz;/* 定位.idata段的頭部 */if (!GetSectionHdrByName(lpFile, &idsh, ".idata"))return 0;pid = (PIMAGE_IMPORT_MODULE_DIRECTORY)ImageDirectoryOffset(lpFile, IMAGE_DIRECTORY_ENTRY_IMPORT);dwBase = ((DWORD)pid. idsh.VirtualAddress);/* 查找模塊的pid */while (pid->dwRVAModuleName && strcmp (pszModule,(char *)(pid->dwRVAModuleName+dwBase)))pid++;/* 如果模塊未找到,就退出 */if (!pid->dwRVAModuleName)return 0;/* 函數(shù)的總數(shù)和字符串長(zhǎng)度 */dwFunction = pid->dwRVAFunctionNameList;while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&*(char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2)){nSize += strlen ((char *)((*(DWORD *)(dwFunction +dwBase)) + dwBase+2)) + 1;dwFunction += 4;nCnt++;}/* 在堆上分配函數(shù)名稱的空間 */*pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);psz = *pszFunctions;/* 向內(nèi)存指針復(fù)制函數(shù)名稱 */dwFunction = pid->dwRVAFunctionNameList;while (dwFunction && *(DWORD *)(dwFunction + dwBase) &&*((char *)((*(DWORD *)(dwFunction + dwBase)) + dwBase+2))){strcpy (psz, (char *)((*(DWORD *)(dwFunction + dwBase)) +dwBase+2));psz += strlen((char *)((*(DWORD *)(dwFunction + dwBase))+dwBase+2)) + 1;dwFunction += 4;}return nCnt;}就像GetImportModuleNames函數(shù)一樣,這一函數(shù)依靠每個(gè)信息列表的末端來獲得一個(gè)置零的入口。這在種情況下,函數(shù)名稱列表就是以零結(jié)尾的。
最后一個(gè)域dwRVAFunctionAddressList是一個(gè)相對(duì)虛擬地址,它指向一個(gè)虛擬地址表。在文件裝載的時(shí)候,這個(gè)虛擬地址表會(huì)被裝載器置于段數(shù)據(jù)之中。但是在文件裝載前,這些虛擬地址會(huì)被一些嚴(yán)密符合函數(shù)名稱列表的虛擬地址替換。所以在文件裝載之前,有兩個(gè)同樣的虛擬地址列表,它們指向?qū)牒瘮?shù)列表。
調(diào)試信息段,.debug
調(diào)試信息位于.debug段之中,同時(shí)PE文件格式也支持單獨(dú)的調(diào)試文件(通常由.DBG擴(kuò)展名標(biāo)識(shí))作為一種將調(diào)試信息集中的方法。調(diào)試段包含了調(diào)試信息,但是調(diào)試目錄卻位于早先提到的.rdata段之中。這其中每個(gè)目錄都涉及了.debug段之中的調(diào)試信息。調(diào)試目錄的結(jié)構(gòu)IMAGE_DEBUG_DIRECTORY被定義為:
// WINNT.Htypedef struct _IMAGE_DEBUG_DIRECTORY {ULONG Characteristics;ULONG TimeDateStamp;USHORT MajorVersion;USHORT MinorVersion;ULONG Type;ULONG SizeOfData;ULONG AddressOfRawData;ULONG PointerToRawData;} IMAGE_DEBUG_DIRECTORY, *PIMAGE_DEBUG_DIRECTORY;這個(gè)段被分為單獨(dú)的部分,每個(gè)部分為不同種類的調(diào)試信息數(shù)據(jù)。對(duì)于每個(gè)部分來說都是一個(gè)像上邊一樣的調(diào)試目錄。不同的調(diào)試信息種類如下:
// WINNT.H#define IMAGE_DEBUG_TYPE_UNKNOWN 0#define IMAGE_DEBUG_TYPE_COFF 1#define IMAGE_DEBUG_TYPE_CODEVIEW 2#define IMAGE_DEBUG_TYPE_FPO 3#define IMAGE_DEBUG_TYPE_MISC 4每個(gè)目錄之中的Type域表示該目錄的調(diào)試信息種類。如你所見,在上邊的表中,PE文件格式支持很多不同的調(diào)試信息種類,以及一些其它的信息域。對(duì)于那些來說,IMAGE_DEBUG_TYPE_MISC信息是唯一的。這一信息被添加到描述可執(zhí)行映像的混雜信息之中,這些混雜信息不能被添加到PE文件格式任何結(jié)構(gòu)化的數(shù)據(jù)段之中。這就是映像文件中最合適的位置,映像名稱則肯定會(huì)出現(xiàn)在這里。如果映像導(dǎo)出了信息,那么導(dǎo)出數(shù)據(jù)段也會(huì)包含這一映像名稱。
每種調(diào)試信息都擁有自己的頭部結(jié)構(gòu),該結(jié)構(gòu)定義了它自己的數(shù)據(jù)。這些結(jié)構(gòu)都列于WINNT.H之中。關(guān)于IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了兩個(gè)標(biāo)識(shí)調(diào)試信息的域。第一個(gè)是AddressOfRawData,為相對(duì)文件裝載的數(shù)據(jù)虛擬地址;另一個(gè)是PointerToRawData,為數(shù)據(jù)所在PE文件之中的實(shí)際偏移量。這就使得定位指定的調(diào)試信息相當(dāng)容易了。
作為最后的例子,請(qǐng)你考慮以下的函數(shù)代碼,它從IMAGE_DEBUG_MISC結(jié)構(gòu)中提取了映像名稱。
//PEFILE.Cint WINAPI RetrieveModuleName(LPVOID lpFile, HANDLE hHeap, char **pszModule){PIMAGE_DEBUG_DIRECTORY pdd;PIMAGE_DEBUG_MISC pdm = NULL;int nCnt;if (!(pdd = (PIMAGE_DEBUG_DIRECTORY)ImageDirectoryOffset(lpFile,IMAGE_DIRECTORY_ENTRY_DEBUG)))return 0;while (pdd->SizeOfData){if (pdd->Type == IMAGE_DEBUG_TYPE_MISC){pdm = (PIMAGE_DEBUG_MISC)((DWORD)pdd->PointerToRawData + (DWORD)lpFile);nCnt = lstrlen(pdm->Data) * (pdm->Unicode ? 2 : 1);*pszModule = (char *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, nCnt+1);CopyMemory(*pszModule, pdm->Data, nCnt);break;}pdd ++;}if (pdm != NULL)return nCnt;elsereturn 0;}你看到了,調(diào)試目錄結(jié)構(gòu)使得定位一個(gè)特定種類的調(diào)試信息變得相對(duì)容易了些。只要定位了IMAGE_DEBUG_MISC結(jié)構(gòu),提取映像名稱就如同調(diào)用CopyMemory函數(shù)一樣簡(jiǎn)單。
如上所述,調(diào)試信息可以被剝離到單獨(dú)的.DBG文件中。Windows NT SDK包含了一個(gè)名為REBASE.EXE的程序可以實(shí)現(xiàn)這一目的。例如,以下的語(yǔ)句可以將一個(gè)名為TEST.EXE的調(diào)試信息剝離:
rebase -b 40000 -x c:\samples\testdir test.exe
調(diào)試信息被置于一個(gè)新的文件中,這個(gè)文件名為TEST.DBG,位于c:\samples\testdir之中。這個(gè)文件起始于一個(gè)單獨(dú)的IMAGE_SEPARATE_DEBUG_HEADER結(jié)構(gòu),接著是存在于原可執(zhí)行映像之中的段頭部的一份拷貝。在段頭部之后,是.debug段的數(shù)據(jù)。也就是說,在段頭部之后,就是一系列的IMAGE_DEBUG_DIRECTORY結(jié)構(gòu)及其相關(guān)的數(shù)據(jù)了。調(diào)試信息本身保留了如上所描述的常規(guī)映像文件調(diào)試信息。
PE文件格式總結(jié)
Windows NT的PE文件格式向熟悉Windows和MS-DOS環(huán)境的開發(fā)者引入了一種全新的結(jié)構(gòu)。然而熟悉UNIX環(huán)境的開發(fā)者會(huì)發(fā)現(xiàn)PE文件格式與COFF規(guī)范很相像(如果它不是以COFF為基礎(chǔ)的話)。
整個(gè)格式的組成:一個(gè)MS-DOS的MZ頭部,之后是一個(gè)實(shí)模式的殘余程序、PE文件標(biāo)志、PE文件頭部、PE可選頭部、所有的段頭部,最后是所有的段實(shí)體。
可選頭部的末尾是一個(gè)數(shù)據(jù)目錄入口的數(shù)組,這些相對(duì)虛擬地址指向段實(shí)體之中的數(shù)據(jù)目錄。每個(gè)數(shù)據(jù)目錄都表示了一個(gè)特定的段實(shí)體數(shù)據(jù)是如何組織的。
PE文件格式有11個(gè)預(yù)定義段,這是對(duì)Windows NT應(yīng)用程序所通用的,但是每個(gè)應(yīng)用程序可以為它自己的代碼以及數(shù)據(jù)定義它自己獨(dú)特的段。
.debug預(yù)定義段也可以分離為一個(gè)單獨(dú)的調(diào)試文件。如果這樣的話,就會(huì)有一個(gè)特定的調(diào)試頭部來用于解析這個(gè)調(diào)試文件,PE文件中也會(huì)有一個(gè)標(biāo)志來表示調(diào)試數(shù)據(jù)被分離了出去。
PEFILE.DLL函數(shù)描述
PEFILE.DLL主要由一些函數(shù)組成,這些函數(shù)或者被用來獲得一個(gè)給定的PE文件中的偏移量,或者被用來把文件中的一些數(shù)據(jù)復(fù)制到一個(gè)特定的結(jié)構(gòu)中去。每個(gè)函數(shù)都有一個(gè)需求——第一個(gè)參數(shù)是一個(gè)指針,這個(gè)指針指向PE文件的起始處。也就是說,這個(gè)文件必須首先被映射到你進(jìn)程的地址空間中,然后映射文件的位置就可以作為每個(gè)函數(shù)第一個(gè)參數(shù)的lpFile的值來傳入了。
我意在使函數(shù)的名稱使你能夠一見而知其意,并且每個(gè)函數(shù)都隨一個(gè)詳細(xì)描述其目的的注釋而列出。如果在讀完函數(shù)列表之后,你仍然不明白某個(gè)函數(shù)的功能,那么請(qǐng)參考EXEVIEW.EXE示例來查明這個(gè)函數(shù)是如何使用的。以下的函數(shù)原型列表可以在PEFILE.H中找到:
// PEFILE.H/* 獲得指向MS-DOS MZ頭部的指針 */BOOL WINAPI GetDosHeader(LPVOID, PIMAGE_DOS_HEADER);/* 決定.EXE文件的類型 */DWORD WINAPI ImageFileType(LPVOID);/* 獲得指向PE文件頭部的指針 */BOOL WINAPI GetPEFileHeader(LPVOID, PIMAGE_FILE_HEADER);/* 獲得指向PE可選頭部的指針 */BOOL WINAPI GetPEOptionalHeader(LPVOID, PIMAGE_OPTIONAL_HEADER);/* 返回模塊入口點(diǎn)的地址 */LPVOID WINAPI GetModuleEntryPoint(LPVOID);/* 返回文件中段的總數(shù) */int WINAPI NumOfSections(LPVOID);/* 返回當(dāng)可執(zhí)行文件被裝載入進(jìn)程地址空間時(shí)的首選基地址 */LPVOID WINAPI GetImageBase(LPVOID);/* 決定文件中一個(gè)特定的映像數(shù)據(jù)目錄的位置 */LPVOID WINAPI ImageDirectoryOffset(LPVOID, DWORD);/* 獲得文件中所有段的名稱 */int WINAPI GetSectionNames(LPVOID, HANDLE, char **);/* 復(fù)制一個(gè)特定段的頭部信息 */BOOL WINAPI GetSectionHdrByName(LPVOID, PIMAGE_SECTION_HEADER, char *);/* 獲得由空字符分隔的導(dǎo)入模塊名稱列表 */int WINAPI GetImportModuleNames(LPVOID, HANDLE, char **);/* 獲得一個(gè)模塊由空字符分隔的導(dǎo)入函數(shù)列表 */int WINAPI GetImportFunctionNamesByModule(LPVOID, HANDLE, char *, char **);/* 獲得由空字符分隔的導(dǎo)出函數(shù)列表 */int WINAPI GetExportFunctionNames(LPVOID, HANDLE, char **);/* 獲得導(dǎo)出函數(shù)總數(shù) */int WINAPI GetNumberOfExportedFunctions(LPVOID);/* 獲得導(dǎo)出函數(shù)的虛擬地址入口點(diǎn)列表 */LPVOID WINAPI GetExportFunctionEntryPoints(LPVOID);/* 獲得導(dǎo)出函數(shù)順序值列表 */LPVOID WINAPI GetExportFunctionOrdinals(LPVOID);/* 決定資源對(duì)象的種類 */int WINAPI GetNumberOfResources (LPVOID);/* 返回文件中所使用的所有資源對(duì)象的種類 */int WINAPI GetListOfResourceTypes(LPVOID, HANDLE, char **);/* 決定調(diào)試信息是否已從文件中分離 */BOOL WINAPI IsDebugInfoStripped(LPVOID);/* 獲得映像文件名稱 */int WINAPI RetrieveModuleName(LPVOID, HANDLE, char **);/* 決定文件是否是一個(gè)有效的調(diào)試文件 */BOOL WINAPI IsDebugFile(LPVOID);/* 從調(diào)試文件中返回調(diào)試頭部 */BOOL WINAPI GetSeparateDebugHeader(LPVOID, PIMAGE_SEPARATE_DEBUG_HEADER); 除了以上所列的函數(shù)之外,本文中早先提到的宏也定義在了PEFILE.H中,完整的列表如下:/* PE文件標(biāo)志的偏移量 */#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew))/* MS操作系統(tǒng)頭部標(biāo)識(shí)了雙字的NT PE文件標(biāo)志;PE文件頭部就緊跟在這個(gè)雙字之后 */#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE))/* PE可選頭部緊跟在PE文件頭部之后 */#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof(IMAGE_FILE_HEADER)))/* 段頭部緊跟在PE可選頭部之后 */#define SECHDROFFSET(a) ((LPVOID)((BYTE *)a + ((PIMAGE_DOS_HEADER)a)->e_lfanew + SIZE_OF_NT_SIGNATURE + sizeof(IMAGE_FILE_HEADER) + sizeof(IMAGE_OPTIONAL_HEADER)))要使用PEFILE.DLL,你只用包含PEFILE.H文件并在應(yīng)用程序中鏈接到這個(gè)DLL即可。所有的這些函數(shù)都是互斥性的函數(shù),但是有些函數(shù)的功能可以相互支持以獲得文件信息。例如,GetSectionNames可以用于獲得所有段的名稱,這樣一來,為了獲得一個(gè)擁有獨(dú)特段名稱(在編譯期由應(yīng)用程序開發(fā)者定義的)的段頭部,你就需要首先獲得所有名稱的列表,然后再對(duì)那個(gè)準(zhǔn)確的段名稱調(diào)用函數(shù)GetSectionHeaderByName了?,F(xiàn)在,你可以享受我為你帶來的這一切了!