1. 程式人生 > >PE檔案格式詳解(2)

PE檔案格式詳解(2)

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以及其它巨集用起來非常方便。(未完待續)