PE文件格式和ELF文件格式(上)----PE文件
PE文件格式詳解
?
作者:MSDN
譯者:李馬
?
Windows NT 3.1引入了一種名為PE文件格式的新可執(zhí)行文件格式。PE文件格式的規(guī)范包含在了MSDN的CD中(Specs and Strategy, Specifications, Windows NT File Format Specifications),但是它非常之晦澀。
然而這一的文檔并未提供足夠的信息,所以開發(fā)者們無法很好地弄懂PE格式。本文旨在解決這一問題,它會對整個的PE文件格式作一個十分徹底的解釋,另外,本文中還帶有對所有必需結(jié)構(gòu)的描述以及示范如何使用這些信息的源碼示例。
為了獲得PE文件中所包含的重要信息,我編寫了一個名為PEFILE.DLL的動態(tài)鏈接庫,本文中所有出現(xiàn)的源碼示例亦均摘自于此。這個DLL和它的源代碼都作為PEFile示例程序的一部分包含在了CD中(譯注:示例程序請在MSDN中尋找,本站恕不提供),你可以在你自己的應(yīng)用程序中使用這個DLL;同樣,你亦可以依你所愿地使用并構(gòu)建它的源碼。在本文末尾,你會找到PEFILE.DLL的函數(shù)導(dǎo)出列表和一個如何使用它們的說明。我覺得你會發(fā)現(xiàn)這些函數(shù)會讓你從容應(yīng)付PE文件格式的。
介紹
Windows操作系統(tǒng)家族最近增加的Windows NT為開發(fā)環(huán)境和應(yīng)用程序本身帶來了很大的改變,這之中一個最為重大的當(dāng)屬PE文件格式了。新的PE文件格式主要來自于UNIX操作系統(tǒng)所通用的COFF規(guī)范,同時為了保證與舊版本MS-DOS及Windows操作系統(tǒng)的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ頭部。
在本文之中,PE文件格式是以自頂而下的順序解釋的。在你從頭開始研究文件內(nèi)容的過程之中,本文會詳細討論PE文件的每一個組成部分。
許多單獨的文件成分定義都來自于Microsoft Win32 SDK開發(fā)包中的WINNT.H文件,在這個文件中你會發(fā)現(xiàn)用來描述文件頭部和數(shù)據(jù)目錄等各種成分的結(jié)構(gòu)類型定義。但是,在WINNT.H中缺少對PE文件結(jié)構(gòu)足夠的定義,在這種情況下,我定義了自己的結(jié)構(gòu)來存取文件數(shù)據(jù)。你會在PEFILE.DLL工程的PEFILE.H中找到這些結(jié)構(gòu)的定義,整套的PEFILE.H開發(fā)文件包含在PEFile示例程序之中。
本文配套的示例程序除了PEFILE.DLL示例代碼之外,還有一個單獨的Win32示例應(yīng)用程序,名為EXEVIEW.EXE。創(chuàng)建這一示例目的有二:首先,我需要測試PEFILE.DLL的函數(shù),并且某些情況要求我同時查看多個文件;其次,很多解決PE文件格式的工作和直接觀看數(shù)據(jù)有關(guān)。例如,要弄懂導(dǎo)入地址名稱表是如何構(gòu)成的,我就得同時查看.idata段頭部、導(dǎo)入映像數(shù)據(jù)目錄、可選頭部以及當(dāng)前的.idata段實體,而EXEVIEW.EXE就是查看這些信息的最佳示例。
閑話少敘,讓我們開始吧。
PE文件結(jié)構(gòu)
PE文件格式被組織為一個線性的數(shù)據(jù)流,它由一個MS-DOS頭部開始,接著是一個是模式的程序殘余以及一個PE文件標(biāo)志,這之后緊接著PE文件頭和可選頭部。這些之后是所有的段頭部,段頭部之后跟隨著所有的段實體。文件的結(jié)束處是一些其它的區(qū)域,其中是一些混雜的信息,包括重分配信息、符號表信息、行號信息以及字串表數(shù)據(jù)。我將所有這些成分列于圖1。
圖1.PE文件映像結(jié)構(gòu)
從MS-DOS文件頭結(jié)構(gòu)開始,我將按照PE文件格式各成分的出現(xiàn)順序依次對其進行討論,并且討論的大部分是以示例代碼為基礎(chǔ)來示范如何獲得文件的信息的。所有的源碼均摘自PEFILE.DLL模塊的PEFILE.C文件。這些示例都利用了Windows NT最酷的特色之一——內(nèi)存映射文件,這一特色允許用戶使用一個簡單的指針來存取文件中所包含的數(shù)據(jù),因此所有的示例都使用了內(nèi)存映射文件來存取PE文件中的數(shù)據(jù)。
注意:請查閱本文末尾關(guān)于如何使用PEFILE.DLL的那一段。
MS-DOS頭部/實模式頭部
如上所述,PE文件格式的第一個組成部分是MS-DOS頭部。在PE文件格式中,它并非一個新概念,因為它與MS-DOS 2.0以來就已有的MS-DOS頭部是完全一樣的。保留這個相同結(jié)構(gòu)的最主要原因是,當(dāng)你嘗試在Windows 3.1以下或MS-DOS 2.0以上的系統(tǒng)下裝載一個文件的時候,操作系統(tǒng)能夠讀取這個文件并明白它是和當(dāng)前系統(tǒng)不相兼容的。換句話說,當(dāng)你在MS-DOS 6.0下運行一個Windows NT可執(zhí)行文件時,你會得到這樣一條消息:“This program cannot be run in DOS mode.”如果MS-DOS頭部不是作為PE文件格式的第一部分的話,操作系統(tǒng)裝載文件的時候就會失敗,并提供一些完全沒用的信息,例如:“The name specified is not recognized as an internal or external command, operable program or batch file.”
MS-DOS頭部占據(jù)了PE文件的頭64個字節(jié),描述它內(nèi)容的結(jié)構(gòu)如下:
實模式殘余程序
實模式殘余程序是一個在裝載時能夠被MS-DOS運行的實際程序。對于一個MS-DOS的可執(zhí)行映像文件,應(yīng)用程序就是從這里執(zhí)行的。對于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)建一個應(yīng)用程序的時候,鏈接器將向你的可執(zhí)行文件中鏈接一個名為WINSTUB.EXE的默認殘余程序。你可以用一個基于MS-DOS的有效程序取代WINSTUB,并且用STUB模塊定義語句指示鏈接器,這樣就能夠取代鏈接器的默認行為。為Windows NT開發(fā)的應(yīng)用程序可以通過使用-STUB:鏈接器選項來實現(xiàn)。
PE文件頭部與標(biāo)志
PE文件頭部是由MS-DOS頭部的e_lfanew域定位的,這個域只是給出了文件的偏移量,所以要確定PE頭部的實際內(nèi)存映射地址,就需要添加文件的內(nèi)存映射基地址。例如,以下的宏是包含在PEFILE.H源文件之中的:
//PEFILE.H#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \((PIMAGE_DOS_HEADER)a)->e_lfanew))在處理PE文件信息的時候,我發(fā)現(xiàn)文件之中有些位置需要經(jīng)常查閱。既然這些位置僅僅是對文件的偏移量,那么用宏來實現(xiàn)這些定位就比較容易,因為它們較之函數(shù)有更好的表現(xiàn)。
請注意這個宏所獲得的是PE文件標(biāo)志,而并非PE文件頭部的偏移量。那是由于自Windows與OS/2的可執(zhí)行文件開始,.EXE文件都被賦予了目標(biāo)操作系統(tǒng)的標(biāo)志。對于Windows NT的PE文件格式而言,這一標(biāo)志在PE文件頭部結(jié)構(gòu)之前。在Windows和OS/2的某些版本中,這一標(biāo)志是文件頭的第一個字。同樣,對于PE文件格式,Windows NT使用了一個DWORD值。
以上的宏返回了文件標(biāo)志的偏移量,而不管它是哪種類型的可執(zhí)行文件。所以,文件頭部是在DWORD標(biāo)志之后,還是在WORD標(biāo)志處,是由這個標(biāo)志是否Windows NT文件標(biāo)志所決定的。要解決這個問題,我編寫了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宏有多么有用。對于比較不同文件類型并且返回一個適當(dāng)?shù)奈募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)在這一列表中,這一點看起來很奇怪。但是,在稍微研究一下之后,就能得到原因了:除了操作系統(tǒng)版本規(guī)范的不同之外,Windows的可執(zhí)行文件和OS/2的可執(zhí)行文件實在沒有什么區(qū)別。這兩個操作系統(tǒng)擁有相同的可執(zhí)行文件結(jié)構(gòu)。
現(xiàn)在把我們的注意力轉(zhuǎn)向Windows NT PE文件格式,我們會發(fā)現(xiàn)只要我們得到了文件標(biāo)志的位置,PE文件之后就會有4個字節(jié)相跟隨。下一個宏標(biāo)識了PE文件的頭部: //PEFILE.C#define PEFHDROFFSET(a) ((LPVOID)((BYTE *)a + \((PIMAGE_DOS_HEADER)a)->e_lfanew + \SIZE_OF_NT_SIGNATURE))這個宏與上一個宏的唯一不同是這個宏加入了一個常量SIZE_OF_NT_SIGNATURE。不幸的是,這個常量并未定義在WINNT.H之中,于是我將它定義在了PEFILE.H中,它是一個DWORD的大小。
既然我們知道了PE文件頭的位置,那么就可以檢查頭部的數(shù)據(jù)了。我們只需要把這個位置賦值給一個結(jié)構(gòu),如下: PIMAGE_FILE_HEADER pfh; pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);在這個例子中,lpFile表示一個指向可執(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 請注意這個文件頭部的大小已經(jīng)定義在這個包含文件之中了,這樣一來,想要得到這個結(jié)構(gòu)的大小就很方便了。但是我覺得對結(jié)構(gòu)本身使用sizeof運算符(譯注:原文為“function”)更簡單一些,因為這樣的話我就不必記住這個常量的名字IMAGE_SIZEOF_FILE_HEADER,而只需要記住結(jié)構(gòu)IMAGE_FILE_HEADER的名字就可以了。另一方面,記住所有結(jié)構(gòu)的名字已經(jīng)夠有挑戰(zhàn)性的了,尤其在是這些結(jié)構(gòu)只有WINNT.H中才有的情況下。
PE文件中的信息基本上是一些高級信息,這些信息是被操作系統(tǒng)或者應(yīng)用程序用來決定如何處理這個文件的。第一個域是用來表示這個可執(zhí)行文件被構(gòu)建的目標(biāo)機器種類,例如DEC(R) Alpha、MIPS R4000、Intel(R) x86或一些其它處理器。系統(tǒng)使用這一信息來在讀取這個文件的其它數(shù)據(jù)之前決定如何處理它。
Characteristics域表示了文件的一些特征。比如對于一個可執(zhí)行文件而言,分離調(diào)試文件是如何操作的。調(diào)試器通常使用的方法是將調(diào)試信息從PE文件中分離,并保存到一個調(diào)試文件(.DBG)中。要這么做的話,調(diào)試器需要了解是否要在一個單獨的文件中尋找調(diào)試信息,以及這個文件是否已經(jīng)將調(diào)試信息分離了。我們可以通過深入可執(zhí)行文件并尋找調(diào)試信息的方法來完成這一工作。要使調(diào)試器不在文件中查找的話,就需要用到IMAGE_FILE_DEBUG_STRIPPED這個特征,它表示文件的調(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)中另一個有用的入口是NumberOfSections域,它表示如果你要方便地提取文件信息的話,就需要了解多少個段——更明確一點來說,有多少個段頭部和多少個段實體。每一個段頭部和段實體都在文件中連續(xù)地排列著,所以要決定段頭部和段實體在哪里結(jié)束的話,段的數(shù)目是必需的。以下的函數(shù)從PE文件頭中提取了段的數(shù)目: PEFILE.C int WINAPI NumOfSections(LPVOID lpFile) {/* 文件頭部中所表示出的段數(shù)目 */return (int)((PIMAGE_FILE_HEADER)PEFHDROFFSET (lpFile))->NumberOfSections); } 如你所見,PEFHDROFFSET以及其它宏用起來非常方便。
PE可選頭部
PE可執(zhí)行文件中接下來的224個字節(jié)組成了PE可選頭部。雖然它的名字是“可選頭部”,但是請確信:這個頭部并非“可選”,而是“必需”的。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í)行映像的重要信息,例如初始的堆棧大小、程序入口點的位置、首選基地址、操作系統(tǒng)版本、段對齊的信息等等。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; 如你所見,這個結(jié)構(gòu)中所列出的域?qū)嵲谑侨唛L得過分。為了不讓你對所有這些域感到厭煩,我會僅僅討論有用的——就是說,對于探究PE文件格式而言有用的。
標(biāo)準(zhǔn)域
首先,請注意這個結(jié)構(gòu)被劃分為“標(biāo)準(zhǔn)域”和“NT附加域”。所謂標(biāo)準(zhǔn)域,就是和UNIX可執(zhí)行文件的COFF格式所公共的部分。雖然標(biāo)準(zhǔn)域保留了COFF中定義的名字,但是Windows NT仍然將它們用作了不同的目的——盡管換個名字更好一些。
·Magic。我不知道這個域是干什么的,對于示例程序EXEVIEW.EXE示例程序而言,這個值是0x010B或267(譯注:0x010B為.EXE,0x0107為ROM映像,這個信息我是從eXeScope上得來的)。
·MajorLinkerVersion、MinorLinkerVersion。表示鏈接此映像的鏈接器版本。隨Window NT build 438配套的Windows NT SDK包含的鏈接器版本是2.39(十六進制為2.27)。
·SizeOfCode。可執(zhí)行代碼尺寸。
·SizeOfInitializedData。已初始化的數(shù)據(jù)尺寸。
·SizeOfUninitializedData。未初始化的數(shù)據(jù)尺寸。
·AddressOfEntryPoint。在標(biāo)準(zhǔn)域中,AddressOfEntryPoint域是對PE文件格式來說最為有趣的了。這個域表示應(yīng)用程序入口點的位置。并且,對于系統(tǒng)黑客來說,這個位置就是導(dǎo)入地址表(IAT)的末尾。以下的函數(shù)示范了如何從可選頭部獲得Windows NT可執(zhí)行映像的入口點。 //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”段)的相對偏移量。
·BaseOfData。已載入映像的未初始化數(shù)據(jù)(“.bss”段)的相對偏移量。
Windows NT附加域
添加到Windows NT PE文件格式中的附加域為Windows NT特定的進程行為提供了裝載器的支持,以下為這些域的概述。
·ImageBase。進程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK鏈接器將這個值默認設(shè)為0x00400000,但是你可以使用-BASE:linker開關(guān)改變這個值。
·SectionAlignment。從ImageBase開始,每個段都被相繼的裝入進程的地址空間中。SectionAlignment則規(guī)定了裝載時段能夠占據(jù)的最小空間數(shù)量——就是說,段是關(guān)于SectionAlignment對齊的。
Windows NT虛擬內(nèi)存管理器規(guī)定,段對齊不能少于頁尺寸(當(dāng)前的x86平臺是4096字節(jié)),并且必須是成倍的頁尺寸。4096字節(jié)是x86鏈接器的默認值,但是它可以通過-ALIGN: linker開關(guān)來設(shè)置。
·FileAlignment。映像文件首先裝載的最小的信息塊間隔。例如,鏈接器將一個段實體(段的原始數(shù)據(jù))加零擴展為文件中最接近的FileAlignment邊界。早先提及的2.39版鏈接器將映像文件以0x200字節(jié)的邊界對齊,這個值可以被強制改為512到65535這么多。
·MajorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的主版本號;通常對Windows NT 1.0而言,這個值被設(shè)為1。
·MinorOperatingSystemVersion。表示W(wǎng)indows NT操作系統(tǒng)的次版本號;通常對Windows NT 1.0而言,這個值被設(shè)為0。
·MajorImageVersion。用來表示應(yīng)用程序的主版本號;對于Microsoft Excel 4.0而言,這個值是4。
·MinorImageVersion。用來表示應(yīng)用程序的次版本號;對于Microsoft Excel 4.0而言,這個值是0。
·MajorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的主版本號;通常對于Windows NT 3.10而言,這個值被設(shè)為3。
·MinorSubsystemVersion。表示W(wǎng)indows NT Win32子系統(tǒng)的次版本號;通常對于Windows NT 3.10而言,這個值被設(shè)為10。
·Reserved1。未知目的,通常不被系統(tǒng)使用,并被鏈接器設(shè)為0。
·SizeOfImage。表示載入的可執(zhí)行映像的地址空間中要保留的地址空間大小,這個數(shù)字很大程度上受SectionAlignment的影響。例如,考慮一個擁有固定頁尺寸4096字節(jié)的系統(tǒng),如果你有一個11個段的可執(zhí)行文件,它的每個段都少于4096字節(jié),并且關(guān)于65536字節(jié)邊界對齊,那么SizeOfImage域?qū)辉O(shè)為11 * 65536 = 720896(176頁)。而如果一個相同的文件關(guān)于4096字節(jié)對齊的話,那么SizeOfImage域的結(jié)果將是11 * 4096 = 45056(11頁)。這只是個簡單的例子,它說明每個段需要少于一個頁面的內(nèi)存。在現(xiàn)實中,鏈接器通過個別地計算每個段的方法來決定SizeOfImage確切的值。它首先決定每個段需要多少字節(jié),并且最后將頁面總數(shù)向上取整至最接近的SectionAlignment邊界,然后總數(shù)就是每個段個別需求之和了。
·SizeOfHeaders。這個域表示文件中有多少空間用來保存所有的文件頭部,包括MS-DOS頭部、PE文件頭部、PE可選頭部以及PE段頭部。文件中所有的段實體就開始于這個位置。
·CheckSum。校驗和是用來在裝載時驗證可執(zhí)行文件的,它是由鏈接器設(shè)置并檢驗的。由于創(chuàng)建這些校驗和的算法是私有信息,所以在此不進行討論。
·Subsystem。用于標(biāo)識該可執(zhí)行文件目標(biāo)子系統(tǒng)的域。每個可能的子系統(tǒng)取值列于WINNT.H的IMAGE_OPTIONAL_HEADER結(jié)構(gòu)之后。
·DllCharacteristics。用來表示一個DLL映像是否為進程和線程的初始化及終止包含入口點的標(biāo)記。
·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit。這些域控制要保留的地址空間數(shù)量,并且負責(zé)棧和默認堆的申請。在默認情況下,棧和堆都擁有1個頁面的申請值以及16個頁面的保留值。這些值可以使用鏈接器開關(guān)-STACKSIZE:與-HEAPSIZE:來設(shè)置。
·LoaderFlags。告知裝載器是否在裝載時中止和調(diào)試,或者默認地正常運行。
·NumberOfRvaAndSizes。這個域標(biāo)識了接下來的DataDirectory數(shù)組。請注意它被用來標(biāo)識這個數(shù)組,而不是數(shù)組中的各個入口數(shù)字,這一點非常重要。
·DataDirectory。數(shù)據(jù)目錄表示文件中其它可執(zhí)行信息重要組成部分的位置。它事實上就是一個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 // 機器值(MIPS GP) #define IMAGE_DIRECTORY_ENTRY_GLOBALPTR 8 // TLS目錄 #define IMAGE_DIRECTORY_ENTRY_TLS 9 // 載入配置目錄 #define IMAGE_DIRECTORY_ENTRY_LOAD_CONFIG 10基本上,每個數(shù)據(jù)目錄都是一個被定義為IMAGE_DATA_DIRECTORY的結(jié)構(gòu)。雖然數(shù)據(jù)目錄入口本身是相同的,但是每個特定的目錄種類卻是完全唯一的。每個數(shù)據(jù)目錄的定義在本文的以后部分被描述為“預(yù)定義段”。 //WINNT.Htypedef struct _IMAGE_DATA_DIRECTORY {ULONG VirtualAddress;ULONG Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; 每個數(shù)據(jù)目錄入口指定了該目錄的尺寸和相對虛擬地址。如果你要定義一個特定的目錄的話,就需要從可選頭部中的數(shù)據(jù)目錄數(shù)組中決定相對的地址,然后使用虛擬地址來決定該目錄位于哪個段中。一旦你決定了哪個段包含了該目錄,該段的段頭部就會被用于查找數(shù)據(jù)目錄的精確文件偏移量位置。
所以要獲得一個數(shù)據(jù)目錄的話,那么首先你需要了解段的概念。我在下面會對其進行描述,這個討論之后還有一個有關(guān)如何定位數(shù)據(jù)目錄的示例。
PE文件段
PE文件規(guī)范由目前為止定義的那些頭部以及一個名為“段”的一般對象組成。段包含了文件的內(nèi)容,包括代碼、數(shù)據(jù)、資源以及其它可執(zhí)行信息,每個段都有一個頭部和一個實體(原始數(shù)據(jù))。我將在下面描述段頭部的有關(guān)信息,但是段實體則缺少一個嚴格的文件結(jié)構(gòu)。因此,它們幾乎可以被鏈接器按任何的方法組織,只要它的頭部填充了足夠能夠解釋數(shù)據(jù)的信息。
段頭部
PE文件格式中,所有的段頭部位于可選頭部之后。每個段頭部為40個字節(jié)長,并且沒有任何的填充信息。段頭部被定義為以下的結(jié)構(gòu): //WINNT.H#define IMAGE_SIZEOF_SHORT_NAME 8 typedef 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;你如何才能獲得一個特定段的段頭部信息?既然段頭部是被連續(xù)的組織起來的,而且沒有一個特定的順序,那么段頭部必須由名稱來定位。以下的函數(shù)示范了如何從一個給定了段名稱的PE映像文件中獲得一個段頭部: //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; }這個函數(shù)通過SECHDROFFSET宏將第一個段頭部定位,然后它開始在所有段中循環(huán),并將要尋找的段名稱和每個段的名稱相比較,直到找到了正確的那一個為止。當(dāng)找到了段的時候,函數(shù)將內(nèi)存映像文件的數(shù)據(jù)復(fù)制到傳入函數(shù)的結(jié)構(gòu)中,然后IMAGE_SECTION_HEADER結(jié)構(gòu)的各域就能夠被直接存取了。
段頭部的域
·Name。每個段都有一個8字符長的名稱域,并且第一個字符必須是一個句點。
·PhysicalAddress或VirtualSize。第二個域是一個union域,現(xiàn)在已不使用了。
·VirtualAddress。這個域標(biāo)識了進程地址空間中要裝載這個段的虛擬地址。實際的地址由將這個域的值加上可選頭部結(jié)構(gòu)中的ImageBase虛擬地址得到。切記,如果這個映像文件是一個DLL,那么這個DLL就不一定會裝載到ImageBase要求的位置。所以一旦這個文件被裝載進入了一個進程,實際的ImageBase值應(yīng)該通過使用GetModuleHandle來檢驗。
·SizeOfRawData。這個域表示了相對FileAlignment的段實體尺寸。文件中實際的段實體尺寸將少于或等于FileAlignment的整倍數(shù)。一旦映像被裝載進入了一個進程的地址空間,段實體的尺寸將會變得少于或等于FileAlignment的整倍數(shù)。
·PointerToRawData。這是一個文件中段實體位置的偏移量。
·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。這些域在PE格式中不使用。
·Characteristics。定義了段的特征。這些值可以在WINNT.H及本光盤(譯注:MSDN的光盤)的PE格式規(guī)范中找到。
值 ????????定義
0x00000020 代碼段
0x00000040 已初始化數(shù)據(jù)段
0x00000080 未初始化數(shù)據(jù)段
0x04000000 該段數(shù)據(jù)不能被緩存
0x08000000 該段不能被分頁
0x10000000 共享段
0x20000000 可執(zhí)行段
0x40000000 可讀段
0x80000000 可寫段
定位數(shù)據(jù)目錄
數(shù)據(jù)目錄存在于它們相應(yīng)的數(shù)據(jù)段中。典型地來說,數(shù)據(jù)目錄是段實體中的第一個結(jié)構(gòu),但不是必需的。由于這個緣故,如果你需要定位一個指定的數(shù)據(jù)目錄的話,就需要從段頭部和可選頭部中獲得信息。
為了讓這個過程簡單一點,我編寫了以下的函數(shù)來定位任何一個在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);/* 定位映像目錄的相對虛擬地址 */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ù)首先確認被請求的數(shù)據(jù)目錄入口數(shù)字,然后它分別獲取指向可選頭部和第一個段頭部的兩個指針。它從可選頭部決定數(shù)據(jù)目錄的虛擬地址,然后它使用這個值來決定數(shù)據(jù)目錄定位在哪個段實體之中。如果適當(dāng)?shù)亩螌嶓w已經(jīng)被標(biāo)識了,那么數(shù)據(jù)目錄特定的位置就可以通過將它的相對虛擬地址轉(zhuǎn)換為文件中地址的方法來找到。
預(yù)定義段
一個Windows NT的應(yīng)用程序典型地擁有9個預(yù)定義段,它們是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些應(yīng)用程序不需要所有的這些段,同樣還有一些應(yīng)用程序為了自己特殊的需要而定義了更多的段。這種做法與MS-DOS和Windows 3.1中的代碼段和數(shù)據(jù)段相似。事實上,應(yīng)用程序定義一個獨特的段的方法是使用標(biāo)準(zhǔn)編譯器來指示對代碼段和數(shù)據(jù)段的命名,或者使用名稱段編譯器選項-NT——就和Windows 3.1中應(yīng)用程序定義獨特的代碼段和數(shù)據(jù)段一樣。
以下是一個關(guān)于Windows NT PE文件之中一些有趣的公共段的討論。
可執(zhí)行代碼段,.text
Windows 3.1和Windows NT之間的一個區(qū)別就是Windows NT默認的做法是將所有的代碼段(正如它們在Windows 3.1中所提到的那樣)組成了一個單獨的段,名為“.text”。既然Windows NT使用了基于頁面的虛擬內(nèi)存管理系統(tǒng),那么將分開的代碼放入不同的段之中的做法就不太明智了。因此,擁有一個大的代碼段對于操作系統(tǒng)和應(yīng)用程序開發(fā)者來說,都是十分方便的。
.text段也包含了早先提到過的入口點。IAT亦存在于.text段之中的模塊入口點之前。(IAT在.text段之中的存在非常有意義,因為這個表事實上是一系列的跳轉(zhuǎn)指令,并且它們的跳轉(zhuǎn)目標(biāo)位置是已固定的地址。)當(dāng)Windows NT的可執(zhí)行映像裝載入進程的地址空間時,IAT就和每一個導(dǎo)入函數(shù)的物理地址一同確定了。要在.text段之中查找IAT,裝載器只用將模塊的入口點定位,而IAT恰恰出現(xiàn)于入口點之前。既然每個入口擁有相同的尺寸,那么向后退查找這個表的起始位置就很容易了。
數(shù)據(jù)段,.bss、.rdata、.data
.bss段表示應(yīng)用程序的未初始化數(shù)據(jù),包括所有函數(shù)或源模塊中聲明為static的變量。
.rdata段表示只讀的數(shù)據(jù),比如字符串文字量、常量和調(diào)試目錄信息。
所有其它變量(除了出現(xiàn)在棧上的自動變量)存儲在.data段之中。基本上,這些是應(yīng)用程序或模塊的全局變量。
資源段,.rsrc
.rsrc段包含了模塊的資源信息。它起始于一個資源目錄結(jié)構(gòu),這個結(jié)構(gòu)就像其它大多數(shù)結(jié)構(gòu)一樣,但是它的數(shù)據(jù)被更進一步地組織在了一棵資源樹之中。以下的IMAGE_RESOURCE_DIRECTORY結(jié)構(gòu)形成了這棵樹的根和各個結(jié)點。
請看這個目錄結(jié)構(gòu),你將會發(fā)現(xiàn)其中竟然沒有指向下一個結(jié)點的指針。但是,在這個結(jié)構(gòu)中有兩個域NumberOfNamedEntries和NumberOfIdEntries代替了指針,它們被用來表示這個目錄附有多少入口。附帶說一句,我的意思是目錄入口就在段數(shù)據(jù)之中的目錄后邊。有名稱的入口按字母升序出現(xiàn),再往后是按數(shù)值升序排列的ID入口。
一個目錄入口由兩個域組成,正如下面IMAGE_RESOURCE_DIRECTORY_ENTRY結(jié)構(gòu)所描述的那樣:
根據(jù)樹的層級不同,這兩個域也就有著不同的用途。Name域被用于標(biāo)識一個資源種類,或者一種資源名稱,或者一個資源的語言ID。OffsetToData與常常被用來在樹之中指向兄弟結(jié)點——即一個目錄結(jié)點或一個葉子結(jié)點。
葉子結(jié)點是資源樹之中最底層的結(jié)點,它們定義了當(dāng)前資源數(shù)據(jù)的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu)被用于描述每個葉子結(jié)點:
OffsetToData和Size這兩個域表示了當(dāng)前資源數(shù)據(jù)的位置和尺寸。既然這一信息主要是在應(yīng)用程序裝載以后由函數(shù)使用的,那么將OffsetToData作為一個相對虛擬的地址會更有意義一些。——幸甚,恰好是這樣沒錯。非常有趣的是,所有其它的偏移量,比如從目錄入口到其它目錄的指針,都是相對于根結(jié)點位置的偏移量。
要更清楚地了解這些內(nèi)容,請參考圖2。
圖2.一個簡單的資源樹結(jié)構(gòu)
圖2描述了一個非常簡單的資源樹,它包含了僅僅兩個資源對象:一個菜單和一個字串表。更深一層地來說,它們各自都有一個子項。然而,你仍然可以看到資源樹有多么復(fù)雜——即使它像這個一樣只有一點點資源。
在樹的根部,第一個目錄有一個文件中包含的所有資源種類的入口,而不管資源種類有多少。在圖2中,有兩個由樹根標(biāo)識的入口,一個是菜單的,另一個是字串表的。如果文件中擁有一個或多個對話框資源,那么根結(jié)點會再擁有一個入口,因此,就有了對話框資源的另一個分支。
WINUSER.H中標(biāo)識了基本的資源種類,我將它們列到了下面:
在樹的第一層級,以上列出的MAKEINTRESOURCE值被放置在每個種類入口的Name處,它標(biāo)識了不同的資源種類。
每個根目錄的入口都指向了樹中第二層級的一個兄弟結(jié)點,這些結(jié)點也是目錄,并且每個都擁有它們自己的入口。在這一層級,目錄被用來以給定的種類標(biāo)識每一個資源種類。如果你的應(yīng)用程序中有多個菜單,那么樹中的第二層級會為每個菜單都準(zhǔn)備一個入口。
你可能意識到了,資源可以由名稱或整數(shù)標(biāo)識。在這一層級,它們是通過目錄結(jié)構(gòu)的Name域來分辨的。如果如果Name域最重要的位被設(shè)置了,那么其它的31個位就會被用作一個到IMAGE_RESOURCE_DIR_STRING_U結(jié)構(gòu)的偏移量。
這個結(jié)構(gòu)僅僅是由一個2字節(jié)長的Length域和一個UNICODE字符Length組成的。
另一方面,如果Name域最重要的位被清空,那么它的低31位就被用于表示資源的整數(shù)ID。圖2示范的就是菜單資源作為一個命名的資源,以及字串表作為一個ID資源。
如果有兩個菜單資源,一個由名稱標(biāo)識,另一個由資源標(biāo)識,那么它們二者就會在菜單資源目錄之后擁有兩個入口。有名稱的資源入口在第一位,之后是由整數(shù)標(biāo)識的資源。目錄域NumberOfNamedEntries和NumberOfIdEntries將各自包含值1,表示當(dāng)前的1個入口。
在第二層級的下面,資源樹就不再更深一步地擴展分支了。第一層級分支至表示每個資源種類的目錄中,第二層級分支至由標(biāo)識符表示的每個資源的目錄中,第三層級是被個別標(biāo)識的資源與它們各自的語言ID之間一對一的映射。要表示一個資源的語言ID,目錄入口結(jié)構(gòu)的Name域就被用來表示資源的主語言ID和子語言ID了。Windows NT的Win32 SDK開發(fā)包中列出了默認的值資源,例如對于0x0409這個值來說,0x09表示主語言LANG_ENGLISH,0x04則被定義為子語言的SUBLANG_ENGLISH_CAN。所有的語言ID值都定義于Windows NT Win32 SDK開發(fā)包的文件WINNT.H中。
既然語言ID結(jié)點是樹中最后的目錄結(jié)點,那么入口結(jié)構(gòu)的OffsetToData域就是到一個葉子結(jié)點(即前面提到過的IMAGE_RESOURCE_DATA_ENTRY結(jié)構(gòu))的偏移量。
再回過頭來參考圖2,你會發(fā)現(xiàn)每個語言目錄入口都對應(yīng)著一個數(shù)據(jù)入口。這個結(jié)點僅僅表示了資源數(shù)據(jù)的尺寸以及資源數(shù)據(jù)的相對虛擬地址。
在資源數(shù)據(jù)段(.rsrc)之中擁有這么多結(jié)構(gòu)有一個好處,就是你可以不存取資源本身而直接可以從這個段收集很多信息。例如,你可以獲得有多少種資源、哪些資源(如果有的話)使用了特別的語言ID、特定的資源是否存在以及單獨種類資源的尺寸。為了示范如何利用這一信息,以下的函數(shù)說明了如何決定一個文件中包含的不同種類的資源:
這個函數(shù)將一個資源種類名稱的列表寫入了由pszResTypes標(biāo)識的變量中。請注意,在這個函數(shù)的核心部分,LoadString是使用各自資源種類目錄入口的Name域來作為字符串ID的。如果你查看PEFILE.RC,你會發(fā)現(xiàn)我定義了一系列的資源種類的字符串,并且它們的ID與它們在目錄入口中的定義完全相同。PEFILE.DLL還有有一個函數(shù),它返回了.rsrc段中的資源對象總數(shù)。這樣一來,從這個段中提取其它的信息,借助這些函數(shù)或另外編寫函數(shù)就方便多了。
導(dǎo)出數(shù)據(jù)段,.edata
.edata段包含了應(yīng)用程序或DLL的導(dǎo)出數(shù)據(jù)。在這個段出現(xiàn)的時候,它會包含一個到達導(dǎo)出信息的導(dǎo)出目錄。
導(dǎo)出目錄中的Name域標(biāo)識了可執(zhí)行模塊的名稱。NumberOfFunctions域和NumberOfNames域表示模塊中有多少導(dǎo)出的函數(shù)以及這些函數(shù)的名稱。
AddressOfFunctions域是一個到導(dǎo)出函數(shù)入口列表的偏移量。AddressOfNames域是到一個導(dǎo)出函數(shù)名稱列表起始處偏移量的地址,這個列表是由null分隔的。AddressOfNameOrdinals是一個到相同導(dǎo)出函數(shù)順序值(每個值2字節(jié)長)列表的偏移量。
三個AddressOf...域是當(dāng)模塊裝載時進程地址空間中的相對虛擬地址。一旦模塊被裝載,那么要獲得進程地質(zhì)空間中的確切地址的話,就應(yīng)該在相對虛擬地址上加上模塊的基地址。可是,在文件被裝載前,仍然可以決定這一地址:只要從給定的域地址中減去段頭部的虛擬地址(VirtualAddress),再加上段實體的偏移量(PointerToRawData),這個結(jié)果就是映像文件中的偏移量了。以下的例子解說了這一技術(shù):
請注意,在這個函數(shù)之中,變量pNames是由決定偏移量地址和當(dāng)前偏移量位置的方法來賦值的。偏移量的地址和偏移量本身都是相對虛擬地址,因此在使用之前必須進行轉(zhuǎn)換——函數(shù)之中體現(xiàn)了這一點。雖然你可以編寫一個類似的函數(shù)來決定順序值或函數(shù)入口點,但是我為什么不為你做好呢?——GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和GetExportFunctionOrdinals已經(jīng)存在于PEFILE.DLL之中了。
導(dǎo)入數(shù)據(jù)段,.idata
.idata段是導(dǎo)入數(shù)據(jù),包括導(dǎo)入庫和導(dǎo)入地址名稱表。雖然定義了IMAGE_DIRECTORY_ENTRY_IMPORT,但是WINNT.H之中并無相應(yīng)的導(dǎo)入目錄結(jié)構(gòu)。作為代替,其中有若干其它的結(jié)構(gòu),名為IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA與IMAGE_IMPORT_DESCRIPTOR。在我個人看來,我實在不知道這些結(jié)構(gòu)是如何和.idata段發(fā)生關(guān)聯(lián)的,所以我花了若干個小時來破譯.idata段實體并且得到了一個更簡單的結(jié)構(gòu),我名之為IMAGE_IMPORT_MODULE_DIRECTORY。
和其它段的數(shù)據(jù)目錄不同的是,這個是作為文件中的每個導(dǎo)入模塊重復(fù)出現(xiàn)的。你可以將它看作模塊數(shù)據(jù)目錄列表中的一個入口,而不是一個整個數(shù)據(jù)段的數(shù)據(jù)目錄。每個入口都是一個指向特定模塊導(dǎo)入信息的目錄。
IMAGE_IMPORT_MODULE_DIRECTORY結(jié)構(gòu)中的一個域dwRVAModuleName是一個相對虛擬地址,它指向模塊的名稱。結(jié)構(gòu)中還有兩個dwUseless參數(shù),它們是為了保持段的對齊。PE文件格式規(guī)范提到了一些東西,關(guān)于導(dǎo)入標(biāo)記、時間/日期標(biāo)志以及主/次版本,但是在我的實驗中,這兩個域自始而終都是空的,所以我仍然認為它們沒有什么用處。
基于這個結(jié)構(gòu)的定義,你便可以獲得可執(zhí)行文件中導(dǎo)入的所有模塊和函數(shù)名稱了。以下的函數(shù)示范了如何獲得特定的PE文件中的所有導(dǎo)入函數(shù)名稱:
這個函數(shù)非常好懂,然而有一點值得指出——注意while循環(huán)。這個循環(huán)當(dāng)pid->dwRVAModuleName為0的時候終止,這就暗示了在IMAGE_IMPORT_MODULE_DIRECTORY結(jié)構(gòu)列表的末尾有一個空的結(jié)構(gòu),這個結(jié)構(gòu)擁有一個0值,至少dwRVAModuleName域為0。這便是我在對文件的實驗中以及之后在PE文件格式中研究的行為。
這個結(jié)構(gòu)中的第一個域dwRVAFunctionNameList是一個相對虛擬地址,這個地址指向一個相對虛擬地址的列表,這些地址是文件中的一些文件名。如下面的數(shù)據(jù)所示,所有導(dǎo)入模塊的模塊和函數(shù)名稱都列于.idata段數(shù)據(jù)中了:
以上的數(shù)據(jù)是EXEVIEW.EXE示例程序.idata段的一部分。這個特別的段表示了導(dǎo)入模塊列表和函數(shù)名稱列表的起始處。如果你開始檢查數(shù)據(jù)中的這個段,你應(yīng)該認出一些熟悉的Win32 API函數(shù)以及模塊名稱。從上往下讀的話,你可以找到GetOpenFileNameA,緊接著是COMDLG32.DLL。然后你能發(fā)現(xiàn)CreateFontIndirectA,緊接著是模塊GDI32.DLL,以及之后的GetDeviceCaps、GetStockObject、GetTextMetrics等等。
這樣的式樣會在.idata段中重復(fù)出現(xiàn)。第一個模塊是COMDLG32.DLL,第二個是GDI32.DLL。請注意第一個模塊只導(dǎo)出了一個函數(shù),而第二個模塊導(dǎo)出了很多函數(shù)。在這兩種情況下,函數(shù)和模塊的排列的方法是首先出現(xiàn)一個函數(shù)名,之后是模塊名,然后是其它的函數(shù)名(如果有的話)。
以下的函數(shù)示范了如何獲得指定模塊的所有函數(shù)名。
就像GetImportModuleNames函數(shù)一樣,這一函數(shù)依靠每個信息列表的末端來獲得一個置零的入口。這在種情況下,函數(shù)名稱列表就是以零結(jié)尾的。
最后一個域dwRVAFunctionAddressList是一個相對虛擬地址,它指向一個虛擬地址表。在文件裝載的時候,這個虛擬地址表會被裝載器置于段數(shù)據(jù)之中。但是在文件裝載前,這些虛擬地址會被一些嚴密符合函數(shù)名稱列表的虛擬地址替換。所以在文件裝載之前,有兩個同樣的虛擬地址列表,它們指向?qū)牒瘮?shù)列表。
調(diào)試信息段,.debug
調(diào)試信息位于.debug段之中,同時PE文件格式也支持單獨的調(diào)試文件(通常由.DBG擴展名標(biāo)識)作為一種將調(diào)試信息集中的方法。調(diào)試段包含了調(diào)試信息,但是調(diào)試目錄卻位于早先提到的.rdata段之中。這其中每個目錄都涉及了.debug段之中的調(diào)試信息。調(diào)試目錄的結(jié)構(gòu)IMAGE_DEBUG_DIRECTORY被定義為:
這個段被分為單獨的部分,每個部分為不同種類的調(diào)試信息數(shù)據(jù)。對于每個部分來說都是一個像上邊一樣的調(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每個目錄之中的Type域表示該目錄的調(diào)試信息種類。如你所見,在上邊的表中,PE文件格式支持很多不同的調(diào)試信息種類,以及一些其它的信息域。對于那些來說,IMAGE_DEBUG_TYPE_MISC信息是唯一的。這一信息被添加到描述可執(zhí)行映像的混雜信息之中,這些混雜信息不能被添加到PE文件格式任何結(jié)構(gòu)化的數(shù)據(jù)段之中。這就是映像文件中最合適的位置,映像名稱則肯定會出現(xiàn)在這里。如果映像導(dǎo)出了信息,那么導(dǎo)出數(shù)據(jù)段也會包含這一映像名稱。
每種調(diào)試信息都擁有自己的頭部結(jié)構(gòu),該結(jié)構(gòu)定義了它自己的數(shù)據(jù)。這些結(jié)構(gòu)都列于WINNT.H之中。關(guān)于IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了兩個標(biāo)識調(diào)試信息的域。第一個是AddressOfRawData,為相對文件裝載的數(shù)據(jù)虛擬地址;另一個是PointerToRawData,為數(shù)據(jù)所在PE文件之中的實際偏移量。這就使得定位指定的調(diào)試信息相當(dāng)容易了。
作為最后的例子,請你考慮以下的函數(shù)代碼,它從IMAGE_DEBUG_MISC結(jié)構(gòu)中提取了映像名稱。
你看到了,調(diào)試目錄結(jié)構(gòu)使得定位一個特定種類的調(diào)試信息變得相對容易了些。只要定位了IMAGE_DEBUG_MISC結(jié)構(gòu),提取映像名稱就如同調(diào)用CopyMemory函數(shù)一樣簡單。
如上所述,調(diào)試信息可以被剝離到單獨的.DBG文件中。Windows NT SDK包含了一個名為REBASE.EXE的程序可以實現(xiàn)這一目的。例如,以下的語句可以將一個名為TEST.EXE的調(diào)試信息剝離:
rebase -b 40000 -x c:\samples\testdir test.exe
調(diào)試信息被置于一個新的文件中,這個文件名為TEST.DBG,位于c:\samples\testdir之中。這個文件起始于一個單獨的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ā)者會發(fā)現(xiàn)PE文件格式與COFF規(guī)范很相像(如果它不是以COFF為基礎(chǔ)的話)。
整個格式的組成:一個MS-DOS的MZ頭部,之后是一個實模式的殘余程序、PE文件標(biāo)志、PE文件頭部、PE可選頭部、所有的段頭部,最后是所有的段實體。
可選頭部的末尾是一個數(shù)據(jù)目錄入口的數(shù)組,這些相對虛擬地址指向段實體之中的數(shù)據(jù)目錄。每個數(shù)據(jù)目錄都表示了一個特定的段實體數(shù)據(jù)是如何組織的。
PE文件格式有11個預(yù)定義段,這是對Windows NT應(yīng)用程序所通用的,但是每個應(yīng)用程序可以為它自己的代碼以及數(shù)據(jù)定義它自己獨特的段。
.debug預(yù)定義段也可以分離為一個單獨的調(diào)試文件。如果這樣的話,就會有一個特定的調(diào)試頭部來用于解析這個調(diào)試文件,PE文件中也會有一個標(biāo)志來表示調(diào)試數(shù)據(jù)被分離了出去。
PEFILE.DLL函數(shù)描述
PEFILE.DLL主要由一些函數(shù)組成,這些函數(shù)或者被用來獲得一個給定的PE文件中的偏移量,或者被用來把文件中的一些數(shù)據(jù)復(fù)制到一個特定的結(jié)構(gòu)中去。每個函數(shù)都有一個需求——第一個參數(shù)是一個指針,這個指針指向PE文件的起始處。也就是說,這個文件必須首先被映射到你進程的地址空間中,然后映射文件的位置就可以作為每個函數(shù)第一個參數(shù)的lpFile的值來傳入了。
我意在使函數(shù)的名稱使你能夠一見而知其意,并且每個函數(shù)都隨一個詳細描述其目的的注釋而列出。如果在讀完函數(shù)列表之后,你仍然不明白某個函數(shù)的功能,那么請參考EXEVIEW.EXE示例來查明這個函數(shù)是如何使用的。以下的函數(shù)原型列表可以在PEFILE.H中找到:
要使用PEFILE.DLL,你只用包含PEFILE.H文件并在應(yīng)用程序中鏈接到這個DLL即可。所有的這些函數(shù)都是互斥性的函數(shù),但是有些函數(shù)的功能可以相互支持以獲得文件信息。例如,GetSectionNames可以用于獲得所有段的名稱,這樣一來,為了獲得一個擁有獨特段名稱(在編譯期由應(yīng)用程序開發(fā)者定義的)的段頭部,你就需要首先獲得所有名稱的列表,然后再對那個準(zhǔn)確的段名稱調(diào)用函數(shù)GetSectionHeaderByName了。現(xiàn)在,你可以享受我為你帶來的這一切了!
總結(jié)
以上是生活随笔為你收集整理的PE文件格式和ELF文件格式(上)----PE文件的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: RHEL6 让 root可以登录桌面
- 下一篇: 搞商业智能需要“智慧”