PE文件格式和ELF文件格式(上)----PE文件

做者:MSDN
譯者:李馬算法

Windows NT 3.1引入了一種名爲PE文件格式的新可執行文件格式。PE文件格式的規範包含在了MSDN的CD中(Specs and Strategy, Specifications, Windows NT File Format Specifications),可是它很是之晦澀。 
然而這一的文檔並未提供足夠的信息,因此開發者們沒法很好地弄懂PE格式。本文旨在解決這一問題,它會對整個的PE文件格式做一個十分完全的解釋,另外,本文中還帶有對全部必需結構的描述以及示範如何使用這些信息的源碼示例。 
爲了得到PE文件中所包含的重要信息,我編寫了一個名爲PEFILE.DLL的動態連接庫,本文中全部出現的源碼示例亦均摘自於此。這個DLL和它的源代碼都做爲PEFile示例程序的一部分包含在了CD中(譯註:示例程序請在MSDN中尋找,本站恕不提供),你能夠在你本身的應用程序中使用這個DLL;一樣,你亦能夠依你所願地使用並構建它的源碼。在本文末尾,你會找到PEFILE.DLL的函數導出列表和一個如何使用它們的說明。我以爲你會發現這些函數會讓你從容應付PE文件格式的。 

介紹 

Windows操做系統家族最近增長的Windows NT爲開發環境和應用程序自己帶來了很大的改變,這之中一個最爲重大的當屬PE文件格式了。新的PE文件格式主要來自於UNIX操做系統所通用的COFF規範,同時爲了保證與舊版本MS-DOS及Windows操做系統的兼容,PE文件格式也保留了MS-DOS中那熟悉的MZ頭部。 
在本文之中,PE文件格式是以自頂而下的順序解釋的。在你從頭開始研究文件內容的過程之中,本文會詳細討論PE文件的每個組成部分。 
許多單獨的文件成分定義都來自於Microsoft Win32 SDK開發包中的WINNT.H文件,在這個文件中你會發現用來描述文件頭部和數據目錄等各類成分的結構類型定義。可是,在WINNT.H中缺乏對PE文件結構足夠的定義,在這種狀況下,我定義了本身的結構來存取文件數據。你會在PEFILE.DLL工程的PEFILE.H中找到這些結構的定義,整套的PEFILE.H開發文件包含在PEFile示例程序之中。 
本文配套的示例程序除了PEFILE.DLL示例代碼以外,還有一個單獨的Win32示例應用程序,名爲EXEVIEW.EXE。建立這一示例目的有二:首先,我須要測試PEFILE.DLL的函數,而且某些狀況要求我同時查看多個文件;其次,不少解決PE文件格式的工做和直接觀看數據有關。例如,要弄懂導入地址名稱表是如何構成的,我就得同時查看.idata段頭部、導入映像數據目錄、可選頭部以及當前的.idata段實體,而EXEVIEW.EXE就是查看這些信息的最佳示例。 
閒話少敘,讓咱們開始吧。 

PE文件結構 

PE文件格式被組織爲一個線性的數據流,它由一個MS-DOS頭部開始,接着是一個是模式的程序殘餘以及一個PE文件標誌,這以後緊接着PE文件頭和可選頭部。這些以後是全部的段頭部,段頭部以後跟隨着全部的段實體。文件的結束處是一些其它的區域,其中是一些混雜的信息,包括重分配信息、符號表信息、行號信息以及字串表數據。我將全部這些成分列於圖1。

圖1.PE文件映像結構 
從MS-DOS文件頭結構開始,我將按照PE文件格式各成分的出現順序依次對其進行討論,而且討論的大部分是以示例代碼爲基礎來示範如何得到文件的信息的。全部的源碼均摘自PEFILE.DLL模塊的PEFILE.C文件。這些示例都利用了Windows NT最酷的特點之一——內存映射文件,這一特點容許用戶使用一個簡單的指針來存取文件中所包含的數據,所以全部的示例都使用了內存映射文件來存取PE文件中的數據。 
注意:請查閱本文末尾關於如何使用PEFILE.DLL的那一段。 

MS-DOS頭部/實模式頭部 

如上所述,PE文件格式的第一個組成部分是MS-DOS頭部。在PE文件格式中,它並不是一個新概念,由於它與MS-DOS 2.0以來就已有的MS-DOS頭部是徹底同樣的。保留這個相同結構的最主要緣由是,當你嘗試在Windows 3.1如下或MS-DOS 2.0以上的系統下裝載一個文件的時候,操做系統可以讀取這個文件並明白它是和當前系統不相兼容的。換句話說,當你在MS-DOS 6.0下運行一個Windows NT可執行文件時,你會獲得這樣一條消息:「This program cannot be run in DOS mode.」若是MS-DOS頭部不是做爲PE文件格式的第一部分的話,操做系統裝載文件的時候就會失敗,並提供一些徹底沒用的信息,例如:「The name specified is not recognized as an internal or external command, operable program or batch file.」 
MS-DOS頭部佔據了PE文件的頭64個字節,描述它內容的結構以下:數組

//WINNT.H

typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE頭部
  USHORT e_magic; // 魔術數字
  USHORT e_cblp; // 文件最後頁的字節數
  USHORT e_cp; // 文件頁數
  USHORT e_crlc; // 重定義元素個數
  USHORT e_cparhdr; // 頭部尺寸,以段落爲單位
  USHORT e_minalloc; // 所需的最小附加段
  USHORT e_maxalloc; // 所需的最大附加段
  USHORT e_ss; // 初始的SS值(相對偏移量)
  USHORT e_sp; // 初始的SP值
  USHORT e_csum; // 校驗和
  USHORT e_ip; // 初始的IP值
  USHORT e_cs; // 初始的CS值(相對偏移量)
  USHORT e_lfarlc; // 重分配表文件地址
  USHORT e_ovno; // 覆蓋號
  USHORT e_res[4]; // 保留字
  USHORT e_oemid; // OEM標識符(相對e_oeminfo)
  USHORT e_oeminfo; // OEM信息
  USHORT e_res2[10]; // 保留字
  LONG e_lfanew; // 新exe頭部的文件地址
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
第一個域e_magic,被稱爲魔術數字,它被用於表示一個MS-DOS兼容的文件類型。全部MS-DOS兼容的可執行文件都將這個值設爲0x5A4D,表示ASCII字符MZ。MS-DOS頭部之因此有的時候被稱爲MZ頭部,就是這個緣故。還有許多其它的域對於MS-DOS操做系統來講都有用,可是對於Windows NT來講,這個結構中只有一個有用的域——最後一個域e_lfnew,一個4字節的文件偏移量,PE文件頭部就是由它定位的。對於Windows NT的PE文件來講,PE文件頭部是緊跟在MS-DOS頭部和實模式程序殘餘以後的。 

實模式殘餘程序  

實模式殘餘程序是一個在裝載時可以被MS-DOS運行的實際程序。對於一個MS-DOS的可執行映像文件,應用程序就是從這裏執行的。對於Windows、OS/二、Windows NT這些操做系統來講,MS-DOS殘餘程序就代替了主程序的位置被放在這裏。這種殘餘程序一般什麼也不作,而只是輸出一行文本,例如:「This program requires Microsoft Windows v3.1 or greater.」固然,用戶能夠在此放入任何的殘餘程序,這就意味着你可能常常看到像這樣的東西:「You can''t run a Windows NT application on OS/2, it''s simply not possible.」 
當爲Windows 3.1構建一個應用程序的時候,連接器將向你的可執行文件中連接一個名爲WINSTUB.EXE的默認殘餘程序。你能夠用一個基於MS-DOS的有效程序取代WINSTUB,而且用STUB模塊定義語句指示連接器,這樣就可以取代連接器的默認行爲。爲Windows NT開發的應用程序能夠經過使用-STUB:連接器選項來實現。 

PE文件頭部與標誌  

PE文件頭部是由MS-DOS頭部的e_lfanew域定位的,這個域只是給出了文件的偏移量,因此要肯定PE頭部的實際內存映射地址,就須要添加文件的內存映射基地址。例如,如下的宏是包含在PEFILE.H源文件之中的: 
//PEFILE.H

#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \
                       ((PIMAGE_DOS_HEADER)a)->e_lfanew))
在處理PE文件信息的時候,我發現文件之中有些位置須要常常查閱。既然這些位置僅僅是對文件的偏移量,那麼用宏來實現這些定位就比較容易,由於它們較之函數有更好的表現。 
請注意這個宏所得到的是PE文件標誌,而並不是PE文件頭部的偏移量。那是因爲自Windows與OS/2的可執行文件開始,.EXE文件都被賦予了目標操做系統的標誌。對於Windows NT的PE文件格式而言,這一標誌在PE文件頭部結構以前。在Windows和OS/2的某些版本中,這一標誌是文件頭的第一個字。一樣,對於PE文件格式,Windows NT使用了一個DWORD值。 
以上的宏返回了文件標誌的偏移量,而無論它是哪一種類型的可執行文件。因此,文件頭部是在DWORD標誌以後,仍是在WORD標誌處,是由這個標誌是否Windows NT文件標誌所決定的。要解決這個問題,我編寫了ImageFileType函數(以下),它返回了映像文件的類型: 
//PEFILE.C

DWORD WINAPI ImageFileType (LPVOID lpFile)
{
  /* 首先出現的是DOS文件標誌 */
  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;
    else
      return IMAGE_DOS_SIGNATURE;
  }
  else
    /* 不明文件種類 */
    return 0;
}
以上列出的代碼當即告訴了你NTSIGNATURE宏有多麼有用。對於比較不一樣文件類型而且返回一個適當的文件種類來講,這個宏就會使這兩件事變得很是簡單。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的可執行文件類型沒有出如今這一列表中,這一點看起來很奇怪。可是,在稍微研究一下以後,就能獲得緣由了:除了操做系統版本規範的不一樣以外,Windows的可執行文件和OS/2的可執行文件實在沒有什麼區別。這兩個操做系統擁有相同的可執行文件結構。 
如今把咱們的注意力轉向Windows NT PE文件格式,咱們會發現只要咱們獲得了文件標誌的位置,PE文件以後就會有4個字節相跟隨。下一個宏標識了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文件頭的位置,那麼就能夠檢查頭部的數據了。咱們只須要把這個位置賦值給一個結構,以下:
PIMAGE_FILE_HEADER pfh;
pfh = (PIMAGE_FILE_HEADER)PEFHDROFFSET(lpFile);
在這個例子中,lpFile表示一個指向可執行文件內存映像基地址的指針,這就顯出了內存映射文件的好處:不須要執行文件的I/O,只需使用指針pfh就能存取文件中的信息。PE文件頭結構被定義爲:
//WINNT.H

typedef 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
請注意這個文件頭部的大小已經定義在這個包含文件之中了,這樣一來,想要獲得這個結構的大小就很方便了。可是我以爲對結構自己使用sizeof運算符(譯註:原文爲「function」)更簡單一些,由於這樣的話我就沒必要記住這個常量的名字IMAGE_SIZEOF_FILE_HEADER,而只須要記住結構IMAGE_FILE_HEADER的名字就能夠了。另外一方面,記住全部結構的名字已經夠有挑戰性的了,尤爲在是這些結構只有WINNT.H中才有的狀況下。 
PE文件中的信息基本上是一些高級信息,這些信息是被操做系統或者應用程序用來決定如何處理這個文件的。第一個域是用來表示這個可執行文件被構建的目標機器種類,例如DEC(R) Alpha、MIPS R4000、Intel(R) x86或一些其它處理器。系統使用這一信息來在讀取這個文件的其它數據以前決定如何處理它。 
Characteristics域表示了文件的一些特徵。好比對於一個可執行文件而言,分離調試文件是如何操做的。調試器一般使用的方法是將調試信息從PE文件中分離,並保存到一個調試文件(.DBG)中。要這麼作的話,調試器須要瞭解是否要在一個單獨的文件中尋找調試信息,以及這個文件是否已經將調試信息分離了。咱們能夠經過深刻可執行文件並尋找調試信息的方法來完成這一工做。要使調試器不在文件中查找的話,就須要用到IMAGE_FILE_DEBUG_STRIPPED這個特徵,它表示文件的調試信息是否已經被分離了。這樣一來,調試器能夠經過快速查看PE文件的頭部的方法來決定文件中是否存在着調試信息。 
WINNT.H定義了若干其它表示文件頭信息的標記,就和以上的例子差很少。我把研究這些標記的事情留給讀者做爲練習,由大家來看看它們是否是頗有趣,這些標記位於WINNT.H中的IMAGE_FILE_HEADER結構以後。 
PE文件頭結構中另外一個有用的入口是NumberOfSections域,它表示若是你要方便地提取文件信息的話,就須要瞭解多少個段——更明確一點來講,有多少個段頭部和多少個段實體。每個段頭部和段實體都在文件中連續地排列着,因此要決定段頭部和段實體在哪裏結束的話,段的數目是必需的。如下的函數從PE文件頭中提取了段的數目:
PEFILE.C
int WINAPI NumOfSections(LPVOID lpFile)
{
  /* 文件頭部中所表示出的段數目 */
  return (int)((PIMAGE_FILE_HEADER)
    PEFHDROFFSET (lpFile))->NumberOfSections);
}
如你所見,PEFHDROFFSET以及其它宏用起來很是方便。 

PE可選頭部  

PE可執行文件中接下來的224個字節組成了PE可選頭部。雖然它的名字是「可選頭部」,可是請確信:這個頭部並不是「可選」,而是「必需」的。OPTHDROFFSET宏能夠得到指向可選頭部的指針:
//PEFILE.H

#define OPTHDROFFSET(a) ((LPVOID)((BYTE *)a + \
                        ((PIMAGE_DOS_HEADER)a)->e_lfanew + \
                        SIZE_OF_NT_SIGNATURE + \
                        sizeof(IMAGE_FILE_HEADER)))
  
可選頭部包含了不少關於可執行映像的重要信息,例如初始的堆棧大小、程序入口點的位置、首選基地址、操做系統版本、段對齊的信息等等。IMAGE_OPTIONAL_HEADER結構以下:
//WINNT.H

typedef struct _IMAGE_OPTIONAL_HEADER {
  //
  // 標準域
  //
  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;
如你所見,這個結構中所列出的域實在是冗長得過度。爲了避免讓你對全部這些域感到厭煩,我會僅僅討論有用的——就是說,對於探究PE文件格式而言有用的。 

標準域  

首先,請注意這個結構被劃分爲「標準域」和「NT附加域」。所謂標準域,就是和UNIX可執行文件的COFF格式所公共的部分。雖然標準域保留了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。可執行代碼尺寸。 
·SizeOfInitializedData。已初始化的數據尺寸。 
·SizeOfUninitializedData。未初始化的數據尺寸。 
·AddressOfEntryPoint。在標準域中,AddressOfEntryPoint域是對PE文件格式來講最爲有趣的了。這個域表示應用程序入口點的位置。而且,對於系統黑客來講,這個位置就是導入地址表(IAT)的末尾。如下的函數示範瞭如何從可選頭部得到Windows NT可執行映像的入口點。
//PEFILE.C

LPVOID WINAPI GetModuleEntryPoint(LPVOID lpFile)
{
  PIMAGE_OPTIONAL_HEADER poh;
  poh = (PIMAGE_OPTIONAL_HEADER)OPTHDROFFSET(lpFile);
  if (poh != NULL)
    return (LPVOID)poh->AddressOfEntryPoint;
  else
    return NULL;
}
·BaseOfCode。已載入映像的代碼(「.text」段)的相對偏移量。 
·BaseOfData。已載入映像的未初始化數據(「.bss」段)的相對偏移量。 

Windows NT附加域  

添加到Windows NT PE文件格式中的附加域爲Windows NT特定的進程行爲提供了裝載器的支持,如下爲這些域的概述。 
·ImageBase。進程映像地址空間中的首選基地址。Windows NT的Microsoft Win32 SDK連接器將這個值默認設爲0x00400000,可是你可使用-BASE:linker開關改變這個值。 
·SectionAlignment。從ImageBase開始,每一個段都被相繼的裝入進程的地址空間中。SectionAlignment則規定了裝載時段可以佔據的最小空間數量——就是說,段是關於SectionAlignment對齊的。 
Windows NT虛擬內存管理器規定,段對齊不能少於頁尺寸(當前的x86平臺是4096字節),而且必須是成倍的頁尺寸。4096字節是x86連接器的默認值,可是它能夠經過-ALIGN: linker開關來設置。 
·FileAlignment。映像文件首先裝載的最小的信息塊間隔。例如,連接器將一個段實體(段的原始數據)加零擴展爲文件中最接近的FileAlignment邊界。早先說起的2.39版連接器將映像文件以0x200字節的邊界對齊,這個值能夠被強制改成512到65535這麼多。 
·MajorOperatingSystemVersion。表示Windows NT操做系統的主版本號;一般對Windows NT 1.0而言,這個值被設爲1。 
·MinorOperatingSystemVersion。表示Windows NT操做系統的次版本號;一般對Windows NT 1.0而言,這個值被設爲0。 
·MajorImageVersion。用來表示應用程序的主版本號;對於Microsoft Excel 4.0而言,這個值是4。 
·MinorImageVersion。用來表示應用程序的次版本號;對於Microsoft Excel 4.0而言,這個值是0。 
·MajorSubsystemVersion。表示Windows NT Win32子系統的主版本號;一般對於Windows NT 3.10而言,這個值被設爲3。 
·MinorSubsystemVersion。表示Windows NT Win32子系統的次版本號;一般對於Windows NT 3.10而言,這個值被設爲10。 
·Reserved1。未知目的,一般不被系統使用,並被連接器設爲0。 
·SizeOfImage。表示載入的可執行映像的地址空間中要保留的地址空間大小,這個數字很大程度上受SectionAlignment的影響。例如,考慮一個擁有固定頁尺寸4096字節的系統,若是你有一個11個段的可執行文件,它的每一個段都少於4096字節,而且關於65536字節邊界對齊,那麼SizeOfImage域將會被設爲11 * 65536 = 720896(176頁)。而若是一個相同的文件關於4096字節對齊的話,那麼SizeOfImage域的結果將是11 * 4096 = 45056(11頁)。這只是個簡單的例子,它說明每一個段須要少於一個頁面的內存。在現實中,連接器經過個別地計算每一個段的方法來決定SizeOfImage確切的值。它首先決定每一個段須要多少字節,而且最後將頁面總數向上取整至最接近的SectionAlignment邊界,而後總數就是每一個段個別需求之和了。 
·SizeOfHeaders。這個域表示文件中有多少空間用來保存全部的文件頭部,包括MS-DOS頭部、PE文件頭部、PE可選頭部以及PE段頭部。文件中全部的段實體就開始於這個位置。 
·CheckSum。校驗和是用來在裝載時驗證可執行文件的,它是由連接器設置並檢驗的。因爲建立這些校驗和的算法是私有信息,因此在此不進行討論。 
·Subsystem。用於標識該可執行文件目標子系統的域。每一個可能的子系統取值列於WINNT.H的IMAGE_OPTIONAL_HEADER結構以後。 
·DllCharacteristics。用來表示一個DLL映像是否爲進程和線程的初始化及終止包含入口點的標記。 
·SizeOfStackReserve、SizeOfStackCommit、SizeOfHeapReserve、SizeOfHeapCommit。這些域控制要保留的地址空間數量,而且負責棧和默認堆的申請。在默認狀況下,棧和堆都擁有1個頁面的申請值以及16個頁面的保留值。這些值可使用連接器開關-STACKSIZE:與-HEAPSIZE:來設置。 
·LoaderFlags。告知裝載器是否在裝載時停止和調試,或者默認地正常運行。 
·NumberOfRvaAndSizes。這個域標識了接下來的DataDirectory數組。請注意它被用來標識這個數組,而不是數組中的各個入口數字,這一點很是重要。 
·DataDirectory。數據目錄表示文件中其它可執行信息重要組成部分的位置。它事實上就是一個IMAGE_DATA_DIRECTORY結構的數組,位於可選頭部結構的末尾。當前的PE文件格式定義了16種可能的數據目錄,這之中的11種如今在使用中。 

數據目錄  

WINNT.H之中所定義的數據目錄爲:
//WINNT.H
 
// 目錄入口
// 導出目錄
#define IMAGE_DIRECTORY_ENTRY_EXPORT 0
// 導入目錄
#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
// 調試目錄
#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
  
基本上,每一個數據目錄都是一個被定義爲IMAGE_DATA_DIRECTORY的結構。雖然數據目錄入口自己是相同的,可是每一個特定的目錄種類倒是徹底惟一的。每一個數據目錄的定義在本文的之後部分被描述爲「預約義段」。
//WINNT.H

typedef struct _IMAGE_DATA_DIRECTORY {
  ULONG VirtualAddress;
  ULONG Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
每一個數據目錄入口指定了該目錄的尺寸和相對虛擬地址。若是你要定義一個特定的目錄的話,就須要從可選頭部中的數據目錄數組中決定相對的地址,而後使用虛擬地址來決定該目錄位於哪一個段中。一旦你決定了哪一個段包含了該目錄,該段的段頭部就會被用於查找數據目錄的精確文件偏移量位置。 
因此要得到一個數據目錄的話,那麼首先你須要瞭解段的概念。我在下面會對其進行描述,這個討論以後還有一個有關如何定位數據目錄的示例。 

PE文件段  

PE文件規範由目前爲止定義的那些頭部以及一個名爲「段」的通常對象組成。段包含了文件的內容,包括代碼、數據、資源以及其它可執行信息,每一個段都有一個頭部和一個實體(原始數據)。我將在下面描述段頭部的有關信息,可是段實體則缺乏一個嚴格的文件結構。所以,它們幾乎能夠被連接器按任何的方法組織,只要它的頭部填充了足夠可以解釋數據的信息。 

段頭部  

PE文件格式中,全部的段頭部位於可選頭部以後。每一個段頭部爲40個字節長,而且沒有任何的填充信息。段頭部被定義爲如下的結構:
//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;
  
你如何才能得到一個特定段的段頭部信息?既然段頭部是被連續的組織起來的,並且沒有一個特定的順序,那麼段頭部必須由名稱來定位。如下的函數示範瞭如何從一個給定了段名稱的PE映像文件中得到一個段頭部:
//PEFILE.C

BOOL 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))
      {
        /* 向頭部複製數據 */
        CopyMemory((LPVOID)sh, (LPVOID)psh,
            sizeof(IMAGE_SECTION_HEADER));
        return TRUE;
      }
      else
        psh++;
    }
  }
  return FALSE;
}
這個函數經過SECHDROFFSET宏將第一個段頭部定位,而後它開始在全部段中循環,並將要尋找的段名稱和每一個段的名稱相比較,直到找到了正確的那一個爲止。當找到了段的時候,函數將內存映像文件的數據複製到傳入函數的結構中,而後IMAGE_SECTION_HEADER結構的各域就可以被直接存取了。 

段頭部的域  

·Name。每一個段都有一個8字符長的名稱域,而且第一個字符必須是一個句點。 
·PhysicalAddress或VirtualSize。第二個域是一個union域,如今已不使用了。 
·VirtualAddress。這個域標識了進程地址空間中要裝載這個段的虛擬地址。實際的地址由將這個域的值加上可選頭部結構中的ImageBase虛擬地址獲得。切記,若是這個映像文件是一個DLL,那麼這個DLL就不必定會裝載到ImageBase要求的位置。因此一旦這個文件被裝載進入了一個進程,實際的ImageBase值應該經過使用GetModuleHandle來檢驗。 
·SizeOfRawData。這個域表示了相對FileAlignment的段實體尺寸。文件中實際的段實體尺寸將少於或等於FileAlignment的整倍數。一旦映像被裝載進入了一個進程的地址空間,段實體的尺寸將會變得少於或等於FileAlignment的整倍數。 
·PointerToRawData。這是一個文件中段實體位置的偏移量。 
·PointerToRelocations、PointerToLinenumbers、NumberOfRelocations、NumberOfLinenumbers。這些域在PE格式中不使用。 
·Characteristics。定義了段的特徵。這些值能夠在WINNT.H及本光盤(譯註:MSDN的光盤)的PE格式規範中找到。 

值         定義 
0x00000020 代碼段 
0x00000040 已初始化數據段 
0x00000080 未初始化數據段 
0x04000000 該段數據不能被緩存 
0x08000000 該段不能被分頁 
0x10000000 共享段 
0x20000000 可執行段 
0x40000000 可讀段 
0x80000000 可寫段 

定位數據目錄  

數據目錄存在於它們相應的數據段中。典型地來講,數據目錄是段實體中的第一個結構,但不是必需的。因爲這個緣故,若是你須要定位一個指定的數據目錄的話,就須要從段頭部和可選頭部中得到信息。 
爲了讓這個過程簡單一點,我編寫了如下的函數來定位任何一個在WINNT.H之中定義的數據目錄。
// PEFILE.C

LPVOID 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;
  /* 返回映像導入目錄的偏移量 */
  return (LPVOID)(((int)lpFile + 
    (int)VAImageDir. psh->VirtualAddress) +
    (int)psh->PointerToRawData);
}
  

該函數首先確認被請求的數據目錄入口數字,而後它分別獲取指向可選頭部和第一個段頭部的兩個指針。它從可選頭部決定數據目錄的虛擬地址,而後它使用這個值來決定數據目錄定位在哪一個段實體之中。若是適當的段實體已經被標識了,那麼數據目錄特定的位置就能夠經過將它的相對虛擬地址轉換爲文件中地址的方法來找到。 
緩存

預約義段 

一個Windows NT的應用程序典型地擁有9個預約義段,它們是.text、.bss、.rdata、.data、.rsrc、.edata、.idata、.pdata和.debug。一些應用程序不須要全部的這些段,一樣還有一些應用程序爲了本身特殊的須要而定義了更多的段。這種作法與MS-DOS和Windows 3.1中的代碼段和數據段類似。事實上,應用程序定義一個獨特的段的方法是使用標準編譯器來指示對代碼段和數據段的命名,或者使用名稱段編譯器選項-NT——就和Windows 3.1中應用程序定義獨特的代碼段和數據段同樣。 
如下是一個關於Windows NT PE文件之中一些有趣的公共段的討論。 

可執行代碼段,.text 

Windows 3.1和Windows NT之間的一個區別就是Windows NT默認的作法是將全部的代碼段(正如它們在Windows 3.1中所提到的那樣)組成了一個單獨的段,名爲「.text」。既然Windows NT使用了基於頁面的虛擬內存管理系統,那麼將分開的代碼放入不一樣的段之中的作法就不太明智了。所以,擁有一個大的代碼段對於操做系統和應用程序開發者來講,都是十分方便的。 
.text段也包含了早先提到過的入口點。IAT亦存在於.text段之中的模塊入口點以前。(IAT在.text段之中的存在很是有意義,由於這個表事實上是一系列的跳轉指令,而且它們的跳轉目標位置是已固定的地址。)當Windows NT的可執行映像裝載入進程的地址空間時,IAT就和每個導入函數的物理地址一同肯定了。要在.text段之中查找IAT,裝載器只用將模塊的入口點定位,而IAT偏偏出現於入口點以前。既然每一個入口擁有相同的尺寸,那麼向後退查找這個表的起始位置就很容易了。 

數據段,.bss、.rdata、.data 

.bss段表示應用程序的未初始化數據,包括全部函數或源模塊中聲明爲static的變量。 
.rdata段表示只讀的數據,好比字符串文字量、常量和調試目錄信息。 
全部其它變量(除了出如今棧上的自動變量)存儲在.data段之中。基本上,這些是應用程序或模塊的全局變量。 

資源段,.rsrc 

.rsrc段包含了模塊的資源信息。它起始於一個資源目錄結構,這個結構就像其它大多數結構同樣,可是它的數據被更進一步地組織在了一棵資源樹之中。如下的IMAGE_RESOURCE_DIRECTORY結構造成了這棵樹的根和各個結點。安全

//WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY {
  ULONG Characteristics;
  ULONG TimeDateStamp;
  USHORT MajorVersion;
  USHORT MinorVersion;
  USHORT NumberOfNamedEntries;
  USHORT NumberOfIdEntries;
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
  

請看這個目錄結構,你將會發現其中居然沒有指向下一個結點的指針。可是,在這個結構中有兩個域NumberOfNamedEntries和NumberOfIdEntries代替了指針,它們被用來表示這個目錄附有多少入口。附帶說一句,個人意思是目錄入口就在段數據之中的目錄後邊。有名稱的入口按字母升序出現,再日後是按數值升序排列的ID入口。 
一個目錄入口由兩個域組成,正以下面IMAGE_RESOURCE_DIRECTORY_ENTRY結構所描述的那樣:app

// WINNT.H

typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
  ULONG Name;
  ULONG OffsetToData;
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;

根據樹的層級不一樣,這兩個域也就有着不一樣的用途。Name域被用於標識一個資源種類,或者一種資源名稱,或者一個資源的語言ID。OffsetToData與經常被用來在樹之中指向兄弟結點——即一個目錄結點或一個葉子結點。 
葉子結點是資源樹之中最底層的結點,它們定義了當前資源數據的尺寸和位置。IMAGE_RESOURCE_DATA_ENTRY結構被用於描述每一個葉子結點:less

// WINNT.H

typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
  ULONG OffsetToData;
  ULONG Size;
  ULONG CodePage;
  ULONG Reserved;
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;

OffsetToData和Size這兩個域表示了當前資源數據的位置和尺寸。既然這一信息主要是在應用程序裝載之後由函數使用的,那麼將OffsetToData做爲一個相對虛擬的地址會更有意義一些。——幸甚,剛好是這樣沒錯。很是有趣的是,全部其它的偏移量,好比從目錄入口到其它目錄的指針,都是相對於根結點位置的偏移量。 
要更清楚地瞭解這些內容,請參考圖2。 

圖2.一個簡單的資源樹結構 
圖2描述了一個很是簡單的資源樹,它包含了僅僅兩個資源對象:一個菜單和一個字串表。更深一層地來講,它們各自都有一個子項。然而,你仍然能夠看到資源樹有多麼複雜——即便它像這個同樣只有一點點資源。 
在樹的根部,第一個目錄有一個文件中包含的全部資源種類的入口,而無論資源種類有多少。在圖2中,有兩個由樹根標識的入口,一個是菜單的,另外一個是字串表的。若是文件中擁有一個或多個對話框資源,那麼根結點會再擁有一個入口,所以,就有了對話框資源的另外一個分支。 
WINUSER.H中標識了基本的資源種類,我將它們列到了下面:函數

//WINUSER.H

/*
* 預約義的資源種類
*/
#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)
  

在樹的第一層級,以上列出的MAKEINTRESOURCE值被放置在每一個種類入口的Name處,它標識了不一樣的資源種類。 
每一個根目錄的入口都指向了樹中第二層級的一個兄弟結點,這些結點也是目錄,而且每一個都擁有它們本身的入口。在這一層級,目錄被用來以給定的種類標識每個資源種類。若是你的應用程序中有多個菜單,那麼樹中的第二層級會爲每一個菜單都準備一個入口。 
你可能意識到了,資源能夠由名稱或整數標識。在這一層級,它們是經過目錄結構的Name域來分辨的。若是若是Name域最重要的位被設置了,那麼其它的31個位就會被用做一個到IMAGE_RESOURCE_DIR_STRING_U結構的偏移量。測試

// WINNT.H

typedef struct _IMAGE_RESOURCE_DIR_STRING_U {
  USHORT Length;
  WCHAR NameString[1];
} IMAGE_RESOURCE_DIR_STRING_U, *PIMAGE_RESOURCE_DIR_STRING_U;
  

這個結構僅僅是由一個2字節長的Length域和一個UNICODE字符Length組成的。 
另外一方面,若是Name域最重要的位被清空,那麼它的低31位就被用於表示資源的整數ID。圖2示範的就是菜單資源做爲一個命名的資源,以及字串表做爲一個ID資源。 
若是有兩個菜單資源,一個由名稱標識,另外一個由資源標識,那麼它們兩者就會在菜單資源目錄以後擁有兩個入口。有名稱的資源入口在第一位,以後是由整數標識的資源。目錄域NumberOfNamedEntries和NumberOfIdEntries將各自包含值1,表示當前的1個入口。 
在第二層級的下面,資源樹就再也不更深一步地擴展分支了。第一層級分支至表示每一個資源種類的目錄中,第二層級分支至由標識符表示的每一個資源的目錄中,第三層級是被個別標識的資源與它們各自的語言ID之間一對一的映射。要表示一個資源的語言ID,目錄入口結構的Name域就被用來表示資源的主語言ID和子語言ID了。Windows NT的Win32 SDK開發包中列出了默認的值資源,例如對於0x0409這個值來講,0x09表示主語言LANG_ENGLISH,0x04則被定義爲子語言的SUBLANG_ENGLISH_CAN。全部的語言ID值都定義於Windows NT Win32 SDK開發包的文件WINNT.H中。 
既然語言ID結點是樹中最後的目錄結點,那麼入口結構的OffsetToData域就是到一個葉子結點(即前面提到過的IMAGE_RESOURCE_DATA_ENTRY結構)的偏移量。 
再回過頭來參考圖2,你會發現每一個語言目錄入口都對應着一個數據入口。這個結點僅僅表示了資源數據的尺寸以及資源數據的相對虛擬地址。 
在資源數據段(.rsrc)之中擁有這麼多結構有一個好處,就是你能夠不存取資源自己而直接能夠從這個段收集不少信息。例如,你能夠得到有多少種資源、哪些資源(若是有的話)使用了特別的語言ID、特定的資源是否存在以及單獨種類資源的尺寸。爲了示範如何利用這一信息,如下的函數說明了如何決定一個文件中包含的不一樣種類的資源:ui

// PEFILE.C

int 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;
  /* 將指針指向第一個資源種類的入口 */
  prde = (PIMAGE_RESOURCE_DIRECTORY_ENTRY)((DWORD)prdRoot +
      sizeof (IMAGE_RESOURCE_DIRECTORY));
  /* 在全部的資源目錄入口類型中循環 */
  for (i = 0; i < prdRoot->NumberOfIdEntries; i++)
  {
    if (LoadString(hDll, prde->Name, pMem, MAXRESOURCENAME))
      pMem += strlen(pMem) + 1;
    prde++;
  }
  return nCnt;
}
  

這個函數將一個資源種類名稱的列表寫入了由pszResTypes標識的變量中。請注意,在這個函數的核心部分,LoadString是使用各自資源種類目錄入口的Name域來做爲字符串ID的。若是你查看PEFILE.RC,你會發現我定義了一系列的資源種類的字符串,而且它們的ID與它們在目錄入口中的定義徹底相同。PEFILE.DLL還有有一個函數,它返回了.rsrc段中的資源對象總數。這樣一來,從這個段中提取其它的信息,藉助這些函數或另外編寫函數就方便多了。 

導出數據段,.edata 

.edata段包含了應用程序或DLL的導出數據。在這個段出現的時候,它會包含一個到達導出信息的導出目錄。spa

// WINNT.H

typedef 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;
  

導出目錄中的Name域標識了可執行模塊的名稱。NumberOfFunctions域和NumberOfNames域表示模塊中有多少導出的函數以及這些函數的名稱。 
AddressOfFunctions域是一個到導出函數入口列表的偏移量。AddressOfNames域是到一個導出函數名稱列表起始處偏移量的地址,這個列表是由null分隔的。AddressOfNameOrdinals是一個到相同導出函數順序值(每一個值2字節長)列表的偏移量。 
三個AddressOf...域是當模塊裝載時進程地址空間中的相對虛擬地址。一旦模塊被裝載,那麼要得到進程地質空間中的確切地址的話,就應該在相對虛擬地址上加上模塊的基地址。但是,在文件被裝載前,仍然能夠決定這一地址:只要從給定的域地址中減去段頭部的虛擬地址(VirtualAddress),再加上段實體的偏移量(PointerToRawData),這個結果就是映像文件中的偏移量了。如下的例子解說了這一技術: 

// PEFILE.C

int WINAPI GetExportFunctionNames(LPVOID lpFile, HANDLE hHeap, char **pszFunctions)
{
  IMAGE_SECTION_HEADER sh;
  PIMAGE_EXPORT_DIRECTORY ped;
  char *pNames, *pCnt;
  int i, nCnt;
  /* 得到.edata域中的段頭部和指向數據目錄的指針 */
  if ((ped = (PIMAGE_EXPORT_DIRECTORY)ImageDirectoryOffset
      (lpFile, IMAGE_DIRECTORY_ENTRY_EXPORT)) == NULL)
    return 0;
  GetSectionHdrByName (lpFile, &sh, ".edata");
  /* 決定導出函數名稱的偏移量 */
  pNames = (char *)(*(int *)((int)ped->AddressOfNames -
    (int)sh.VirtualAddress + (int)sh.PointerToRawData +
    (int)lpFile) - (int)sh.VirtualAddress +
    (int)sh.PointerToRawData + (int)lpFile);
  /* 計算出要爲全部的字符串分配多少內存 */
  pCnt = pNames;
  for (i = 0; i < (int)ped->NumberOfNames; i++)
    while (*pCnt++);
  nCnt = (int)(pCnt.pNames);
  /* 在堆上爲函數名稱分配內存 */
  *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nCnt);
  /* 將全部字符串複製到緩衝區 */
  CopyMemory((LPVOID)*pszFunctions, (LPVOID)pNames, nCnt);
  return nCnt;
}

請注意,在這個函數之中,變量pNames是由決定偏移量地址和當前偏移量位置的方法來賦值的。偏移量的地址和偏移量自己都是相對虛擬地址,所以在使用以前必須進行轉換——函數之中體現了這一點。雖然你能夠編寫一個相似的函數來決定順序值或函數入口點,可是我爲何不爲你作好呢?——GetNumberOfExportedFunctions、GetExportFunctionEntryPoints和GetExportFunctionOrdinals已經存在於PEFILE.DLL之中了。 

導入數據段,.idata 

.idata段是導入數據,包括導入庫和導入地址名稱表。雖然定義了IMAGE_DIRECTORY_ENTRY_IMPORT,可是WINNT.H之中並沒有相應的導入目錄結構。做爲代替,其中有若干其它的結構,名爲IMAGE_IMPORT_BY_NAME、IMAGE_THUNK_DATA與IMAGE_IMPORT_DESCRIPTOR。在我我的看來,我實在不知道這些結構是如何和.idata段發生關聯的,因此我花了若干個小時來破譯.idata段實體而且獲得了一個更簡單的結構,我名之爲IMAGE_IMPORT_MODULE_DIRECTORY。

// PEFILE.H

typedef struct tagImportDirectory
{
  DWORD dwRVAFunctionNameList;
  DWORD dwUseless1;
  DWORD dwUseless2;
  DWORD dwRVAModuleName;
  DWORD dwRVAFunctionAddressList;
} IMAGE_IMPORT_MODULE_DIRECTORY, *PIMAGE_IMPORT_MODULE_DIRECTORY;

和其它段的數據目錄不一樣的是,這個是做爲文件中的每一個導入模塊重複出現的。你能夠將它看做模塊數據目錄列表中的一個入口,而不是一個整個數據段的數據目錄。每一個入口都是一個指向特定模塊導入信息的目錄。 
IMAGE_IMPORT_MODULE_DIRECTORY結構中的一個域dwRVAModuleName是一個相對虛擬地址,它指向模塊的名稱。結構中還有兩個dwUseless參數,它們是爲了保持段的對齊。PE文件格式規範提到了一些東西,關於導入標記、時間/日期標誌以及主/次版本,可是在個人實驗中,這兩個域自始而終都是空的,因此我仍然認爲它們沒有什麼用處。 
基於這個結構的定義,你即可以得到可執行文件中導入的全部模塊和函數名稱了。如下的函數示範瞭如何得到特定的PE文件中的全部導入函數名稱:

//PEFILE.C

int 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;
  /* 提取全部導入模塊 */
  while (pid->dwRVAModuleName)
  {
    /* 爲絕對字符串偏移量分配緩衝區 */
    pModule[nCnt] = (char *)(pData + 
        (pid->dwRVAModuleName-idsh.VirtualAddress));
    nSize += strlen(pModule[nCnt]) + 1;
    /* 增至下一個導入目錄入口 */
    pid++;
    nCnt++;
  }
  /* 將全部字符串賦值到一大塊的堆內存中 */
  *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;
}

這個函數很是好懂,然而有一點值得指出——注意while循環。這個循環當pid->dwRVAModuleName爲0的時候終止,這就暗示了在IMAGE_IMPORT_MODULE_DIRECTORY結構列表的末尾有一個空的結構,這個結構擁有一個0值,至少dwRVAModuleName域爲0。這即是我在對文件的實驗中以及以後在PE文件格式中研究的行爲。 
這個結構中的第一個域dwRVAFunctionNameList是一個相對虛擬地址,這個地址指向一個相對虛擬地址的列表,這些地址是文件中的一些文件名。以下面的數據所示,全部導入模塊的模塊和函數名稱都列於.idata段數據中了:

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 ..GetOpenFileNam
6541 0000 636F 6D64 6C67 3332 2E64 6C6C eA..comdlg32.dll
0000 2500 4372 6561 7465 466F 6E74 496E ..%.CreateFontIn
6469 7265 6374 4100 4744 4933 322E 646C directA.GDI32.dl
6C00 A000 4765 7444 6576 6963 6543 6170 l...GetDeviceCap
7300 C600 4765 7453 746F 636B 4F62 6A65 s...GetStockObje
6374 0000 D500 4765 7454 6578 744D 6574 ct....GetTextMet
7269 6373 4100 1001 5365 6C65 6374 4F62 ricsA...SelectOb
6A65 6374 0000 1601 5365 7442 6B43 6F6C ject....SetBkCol
6F72 0000 3501 5365 7454 6578 7443 6F6C or..5.SetTextCol
6F72 0000 4501 5465 7874 4F75 7441 0000 or..E.TextOutA..

以上的數據是EXEVIEW.EXE示例程序.idata段的一部分。這個特別的段表示了導入模塊列表和函數名稱列表的起始處。若是你開始檢查數據中的這個段,你應該認出一些熟悉的Win32 API函數以及模塊名稱。從上往下讀的話,你能夠找到GetOpenFileNameA,緊接着是COMDLG32.DLL。而後你能發現CreateFontIndirectA,緊接着是模塊GDI32.DLL,以及以後的GetDeviceCaps、GetStockObject、GetTextMetrics等等。 
這樣的式樣會在.idata段中重複出現。第一個模塊是COMDLG32.DLL,第二個是GDI32.DLL。請注意第一個模塊只導出了一個函數,而第二個模塊導出了不少函數。在這兩種狀況下,函數和模塊的排列的方法是首先出現一個函數名,以後是模塊名,而後是其它的函數名(若是有的話)。 
如下的函數示範瞭如何得到指定模塊的全部函數名。

// PEFILE.C

int 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;
  /* 函數的總數和字符串長度 */
  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++;
  }
  /* 在堆上分配函數名稱的空間 */
  *pszFunctions = HeapAlloc (hHeap, HEAP_ZERO_MEMORY, nSize);
  psz = *pszFunctions;
  /* 向內存指針複製函數名稱 */
  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函數同樣,這一函數依靠每一個信息列表的末端來得到一個置零的入口。這在種狀況下,函數名稱列表就是以零結尾的。 
最後一個域dwRVAFunctionAddressList是一個相對虛擬地址,它指向一個虛擬地址表。在文件裝載的時候,這個虛擬地址表會被裝載器置於段數據之中。可是在文件裝載前,這些虛擬地址會被一些嚴密符合函數名稱列表的虛擬地址替換。因此在文件裝載以前,有兩個一樣的虛擬地址列表,它們指向導入函數列表。 

調試信息段,.debug 

調試信息位於.debug段之中,同時PE文件格式也支持單獨的調試文件(一般由.DBG擴展名標識)做爲一種將調試信息集中的方法。調試段包含了調試信息,可是調試目錄卻位於早先提到的.rdata段之中。這其中每一個目錄都涉及了.debug段之中的調試信息。調試目錄的結構IMAGE_DEBUG_DIRECTORY被定義爲:

// WINNT.H

typedef 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;

這個段被分爲單獨的部分,每一個部分爲不一樣種類的調試信息數據。對於每一個部分來講都是一個像上邊同樣的調試目錄。不一樣的調試信息種類以下:

// 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域表示該目錄的調試信息種類。如你所見,在上邊的表中,PE文件格式支持不少不一樣的調試信息種類,以及一些其它的信息域。對於那些來講,IMAGE_DEBUG_TYPE_MISC信息是惟一的。這一信息被添加到描述可執行映像的混雜信息之中,這些混雜信息不能被添加到PE文件格式任何結構化的數據段之中。這就是映像文件中最合適的位置,映像名稱則確定會出如今這裏。若是映像導出了信息,那麼導出數據段也會包含這一映像名稱。 
每種調試信息都擁有本身的頭部結構,該結構定義了它本身的數據。這些結構都列於WINNT.H之中。關於IMAGE_DEBUG_DIRECTORY一件有趣的事就是它包括了兩個標識調試信息的域。第一個是AddressOfRawData,爲相對文件裝載的數據虛擬地址;另外一個是PointerToRawData,爲數據所在PE文件之中的實際偏移量。這就使得定位指定的調試信息至關容易了。 
做爲最後的例子,請你考慮如下的函數代碼,它從IMAGE_DEBUG_MISC結構中提取了映像名稱。

//PEFILE.C

int 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;
  else
    return 0;
}

你看到了,調試目錄結構使得定位一個特定種類的調試信息變得相對容易了些。只要定位了IMAGE_DEBUG_MISC結構,提取映像名稱就如同調用CopyMemory函數同樣簡單。 
如上所述,調試信息能夠被剝離到單獨的.DBG文件中。Windows NT SDK包含了一個名爲REBASE.EXE的程序能夠實現這一目的。例如,如下的語句能夠將一個名爲TEST.EXE的調試信息剝離: 
rebase -b 40000 -x c:\samples\testdir test.exe 
調試信息被置於一個新的文件中,這個文件名爲TEST.DBG,位於c:\samples\testdir之中。這個文件起始於一個單獨的IMAGE_SEPARATE_DEBUG_HEADER結構,接着是存在於原可執行映像之中的段頭部的一份拷貝。在段頭部以後,是.debug段的數據。也就是說,在段頭部以後,就是一系列的IMAGE_DEBUG_DIRECTORY結構及其相關的數據了。調試信息自己保留了如上所描述的常規映像文件調試信息。 

PE文件格式總結 

Windows NT的PE文件格式向熟悉Windows和MS-DOS環境的開發者引入了一種全新的結構。然而熟悉UNIX環境的開發者會發現PE文件格式與COFF規範很相像(若是它不是以COFF爲基礎的話)。 
整個格式的組成:一個MS-DOS的MZ頭部,以後是一個實模式的殘餘程序、PE文件標誌、PE文件頭部、PE可選頭部、全部的段頭部,最後是全部的段實體。 
可選頭部的末尾是一個數據目錄入口的數組,這些相對虛擬地址指向段實體之中的數據目錄。每一個數據目錄都表示了一個特定的段實體數據是如何組織的。 
PE文件格式有11個預約義段,這是對Windows NT應用程序所通用的,可是每一個應用程序能夠爲它本身的代碼以及數據定義它本身獨特的段。 
.debug預約義段也能夠分離爲一個單獨的調試文件。若是這樣的話,就會有一個特定的調試頭部來用於解析這個調試文件,PE文件中也會有一個標誌來表示調試數據被分離了出去。 

PEFILE.DLL函數描述 

PEFILE.DLL主要由一些函數組成,這些函數或者被用來得到一個給定的PE文件中的偏移量,或者被用來把文件中的一些數據複製到一個特定的結構中去。每一個函數都有一個需求——第一個參數是一個指針,這個指針指向PE文件的起始處。也就是說,這個文件必須首先被映射到你進程的地址空間中,而後映射文件的位置就能夠做爲每一個函數第一個參數的lpFile的值來傳入了。 
我意在使函數的名稱使你可以一見而知其意,而且每一個函數都隨一個詳細描述其目的的註釋而列出。若是在讀完函數列表以後,你仍然不明白某個函數的功能,那麼請參考EXEVIEW.EXE示例來查明這個函數是如何使用的。如下的函數原型列表能夠在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);

/* 返回模塊入口點的地址 */
LPVOID WINAPI GetModuleEntryPoint(LPVOID);

/* 返回文件中段的總數 */
int WINAPI NumOfSections(LPVOID);

/* 返回當可執行文件被裝載入進程地址空間時的首選基地址 */
LPVOID WINAPI GetImageBase(LPVOID);

/* 決定文件中一個特定的映像數據目錄的位置 */
LPVOID WINAPI ImageDirectoryOffset(LPVOID, DWORD);

/* 得到文件中全部段的名稱 */
int WINAPI GetSectionNames(LPVOID, HANDLE, char **);

/* 複製一個特定段的頭部信息 */
BOOL WINAPI GetSectionHdrByName(LPVOID, PIMAGE_SECTION_HEADER, char *);

/* 得到由空字符分隔的導入模塊名稱列表 */
int WINAPI GetImportModuleNames(LPVOID, HANDLE, char **);

/* 得到一個模塊由空字符分隔的導入函數列表 */
int WINAPI GetImportFunctionNamesByModule(LPVOID, HANDLE, char *, char **);

/* 得到由空字符分隔的導出函數列表 */
int WINAPI GetExportFunctionNames(LPVOID, HANDLE, char **);

/* 得到導出函數總數 */
int WINAPI GetNumberOfExportedFunctions(LPVOID);

/* 得到導出函數的虛擬地址入口點列表 */
LPVOID WINAPI GetExportFunctionEntryPoints(LPVOID);

/* 得到導出函數順序值列表 */
LPVOID WINAPI GetExportFunctionOrdinals(LPVOID);

/* 決定資源對象的種類 */
int WINAPI GetNumberOfResources (LPVOID);

/* 返回文件中所使用的全部資源對象的種類 */
int WINAPI GetListOfResourceTypes(LPVOID, HANDLE, char **);

/* 決定調試信息是否已從文件中分離 */
BOOL WINAPI IsDebugInfoStripped(LPVOID);

/* 得到映像文件名稱 */
int WINAPI RetrieveModuleName(LPVOID, HANDLE, char **);

/* 決定文件是不是一個有效的調試文件 */
BOOL WINAPI IsDebugFile(LPVOID);

/* 從調試文件中返回調試頭部 */
BOOL WINAPI GetSeparateDebugHeader(LPVOID, PIMAGE_SEPARATE_DEBUG_HEADER);
  除了以上所列的函數以外,本文中早先提到的宏也定義在了PEFILE.H中,完整的列表以下:
/* PE文件標誌的偏移量 */
#define NTSIGNATURE(a) ((LPVOID)((BYTE *)a + \
                       ((PIMAGE_DOS_HEADER)a)->e_lfanew))

/* MS操做系統頭部標識了雙字的NT PE文件標誌;PE文件頭部就緊跟在這個雙字以後 */
#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文件並在應用程序中連接到這個DLL便可。全部的這些函數都是互斥性的函數,可是有些函數的功能能夠相互支持以得到文件信息。例如,GetSectionNames能夠用於得到全部段的名稱,這樣一來,爲了得到一個擁有獨特段名稱(在編譯期由應用程序開發者定義的)的段頭部,你就須要首先得到全部名稱的列表,而後再對那個準確的段名稱調用函數GetSectionHeaderByName了。如今,你能夠享受我爲你帶來的這一切了!

相關文章
相關標籤/搜索