PE文件格式詳解(上)

PE文件格式詳解(上)

作者:MSDN
譯者:李馬 (http://home.nuc.edu.cn/~titilima)

摘要

   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/2、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);
}
  
該函數首先確認被請求的數據目錄入口數字,然後它分別獲取指向可選頭部和第一個段頭部的兩個指針。它從可選頭部決定數據目錄的虛擬地址,然後它使用這個值來決定數據目錄定位在哪個段實體之中。如果適當的段實體已經被標識了,那麼數據目錄特定的位置就可以通過將它的相對虛擬地址轉換爲文件中地址的方法來找到。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章