1. 程式人生 > >windows PE檔案結構及其載入機制

windows PE檔案結構及其載入機制

1. 概述

PE檔案的全稱是Portable Executable,意為可移植的可執行的檔案,常見的EXE、DLL、OCX、SYS、COM都是PE檔案,PE檔案是微軟Windows作業系統上的程式檔案(可能是間接被執行,如DLL)。它是1993年Windows NT系統引入的新可執行檔案格式,到現在已經經過20多年了。雖然使用PE作為可執行檔案格式的Windows作業系統已經更換了很多版本,其結構的變化、新特性的增加、檔案格式的轉換,以及核心的重新定位等,都發生了翻天覆地的變化,硬體架構也從16位發展到現在的64位架構,而這些變化對PE格式的影響卻不大。由於PE格式有較好的資料組織方式和資料管理演算法,面對如此多的變化卻能保持其設計的優雅。

眾所周知,Windows NT繼承自VAX® VMS®和UNIX。Windows NT的許多建立者在到Microsoft之前都曾為這些平臺設計和編寫程式。當他們設計Windows NT時,很自然會使用以前寫過的和測試過的工具以儘快開始他們的新專案。這些工具產生的和使用的可執行檔案和目標模組的格式被稱為COFF(Common Object File Format的首字母,通用目標檔案格式)。Microsoft編譯器生成的OBJ檔案就使用COFF格式。

PE檔案之所以被稱為“可移植”是因為Windows NT在各種平臺(x86、MIPS®、Alpha等等)上的所有實現都使用同樣的可執行檔案格式。當然,像CPU指令的二進位制編碼之類的內容會有所不同。重要的是作業系統載入器和程式設計工具不需要針對遇到的每種新的CPU再完全重寫。Windows NT及其以後版本,Windows 95及其以後版本和Windows CE一直到現在的win10都使用了這個相同的格式,所以說在很大程度上,這個目的達到了。對於64位的Windows,PE格式只是進行了很少的修改。這種新的格式被叫做PE32+。沒有加入新的域,只有一個域被去除。剩下的改變只是一些域從32位擴充套件到了64位。在這種情況下,你能寫出和32位與64位PE檔案都能一起工作的程式碼。對於C++程式碼,Windows標頭檔案的能力使這些改變很不明顯。EXE和DLL檔案之間的不同完全是語義上的。它們都使用完全相同的PE格式。僅有的區別是用了一個單個的位來指出這個檔案應該被作為EXE還是一個DLL。甚至DLL檔案的副檔名也是不固定的,一些具有完全不同的副檔名的檔案也是DLL,比如.OCX控制元件和控制面板程式(.CPL檔案)。

PE檔案格式主要來自於UNIX作業系統所通用的COFF規範,同時為了保證與舊版本MS-DOS及Windows作業系統的相容,也保留了MS-DOS中那熟悉的MZ頭部。PE格式被公開在WINNT.H標頭檔案中(非常零散)。在WINNT.H檔案的中間有一個“Image Format”節。這個節以MS-DOS MZ格式和PE格式開頭,後面才是新的PE格式。WINNT.H提供了PE檔案使用的原始資料結構的定義。

2. PE檔案分析相關的概念

在詳細瞭解PE檔案結構之前,我們需要先了解幾個基本的概念。理解這些概念有利於我們更好地把握和分析PE中的資料結構。

2.1. 地址

PE中涉及的地址有四類,他們分別是:

  • 虛擬記憶體地址(VA)
  • 相對虛擬地址(RVA)
  • 檔案偏移地址(FOA)
  • 特殊地址

2.1.1. 虛擬地址(Virtual Addresses)

由於Windows NT推出時程式是執行在保護模式下,使用者的PE檔案被作業系統載入進記憶體後,PE對應的程序支配了自己獨立的4GB虛擬空間。在這個空間中定位的地址稱為虛擬記憶體地址(Virual Address,VA)。到了現在系統執行在X64架構的硬體上(長模式),記憶體訪問可以採用線性地址的方式,同時可訪問的記憶體也突破了4GB的限制,但是獨立的程序擁有獨立的虛擬地址空間的記憶體管理機制還在沿用。在PE中,程序本身的VA被解釋為:程序的基地址 + 相對虛擬記憶體地址。

2.1.2. 相對虛擬地址(Relative Virtual Addresses)

PE格式大量地使用所謂的RVA(相對虛擬地址)。一個RVA,亦即一個“Relative Virtual Addresses(相對虛擬地址)”,是在你不知道基地址時,被用來描述一個記憶體地址的。它是需要加上基地址才能獲得線性地址的數值。基地址就是PE映象檔案被裝入記憶體的地址,並且可能會隨著一次又一次的呼叫而變化。

在一個可執行檔案中,有許多在記憶體中的地址必須被指定的位置。例如,當引用一個全域性變數時就必須指定它的地址。PE檔案可以被載入到程序地址空間的任何位置。雖然它們有一個首選載入地址,但你不能依賴於可執行檔案真的會被載入到那個位置。因為這個原因,指定一個地址而不依賴於可執行檔案的載入位置就很重要。

為了消除PE檔案中對記憶體地址的硬編碼,於是產生了RVA。一個RVA是在記憶體中相對於PE檔案被載入的地址的一個偏移。例如,如果一個EXE檔案被載入到地址0x400000,它的程式碼節位於地址0x401000處。那麼程式碼節的RVA就是:

(目標地址) 0x401000 - (載入地址)0x400000 = (RVA)0x1000.

因為PE-檔案中的各部分(各節)不需要像已載入的映象檔案那樣對齊,事情變得複雜起來。例如,檔案中的各節常按照512(十六進位制的0x200)位元組邊界對齊,而已載入的映象檔案則可能按照4096(十六進位制的0x1000)位元組邊界對齊。參見下面的“SectionAlignment(節對齊)”和“FileAlignment(檔案對齊)”。

因此,為了在PE檔案中找到一個特定RVA地址的資訊,你得按照檔案已被載入時的那樣來計算偏移量,但要按照檔案的偏移量來跳過。

試舉一例,假若你已知道執行開始處位於RVA 0x1560地址處,並且想從那裡開始的程式碼處反彙編。為了從檔案中找到這個地址,你得先查明在RAM(記憶體)中各節是按照4096位元組對齊的,並且“.code”節是從RVA 0x1000地址處開始,有16384位元組長;然後你才知道RVA 0x1560地址位於此節的偏移量0x560處。你還要查明在檔案中那節是按512位元組邊界對齊,且“.code”節在檔案中從偏移量0x800處開始,然後你就知道在檔案中程式碼的執行開始處就在0x800+0x560=0xd60位元組處。

然後你反彙編它並發現訪問一個變數的線性地址位於0x1051d0處。二進位制檔案的線性地址在裝入時將被重定位,並常被假定使用的是優先載入地址。因為你已查明優先載入地址為0x100000,因此我們可開始處理RVA 0x51d0了。因資料節開始於RVA 0x5000處,且有2048位元組長,所以它處於資料節中。又因資料節在檔案中開始於偏移量0x4800處,所以該變數就可以在檔案中的0x4800+0x51d0-0x5000=0x49d0處找到。

2.1.3. 檔案偏移地址

檔案偏移地址(File Offset Address,FOA)和記憶體無關,他是指某個位置距離檔案頭的偏移。

2.1.4. 特殊地址

在PE結構中海油一種特殊地址,其計算方法並不是從檔案頭算起,也不是從記憶體的某個位置的基地址算起,而是從特定的位置算起。這個地址在PE結構中很少見,如:在資源表裡就出現過這樣的地址。

2.2. 節(Section)

無論是結構化程式設計,還是面向物件程式設計,都是倡導程式和資料的獨立性,因此,程式中的程式碼和資料通通常是分開存放的。為了保證程式執行的安全,保證核心的穩定,Windows作業系統通常對不同用途的資料設定不同的許可權。比如:程式碼段中的位元組碼在程式執行的時候,一般不允許使用者進行修改,資料段則允許在程式執行過程中讀和寫,常量只能讀等。Windows作業系統在載入可執行程式時,會為這些具有不同屬性的資料分別分配標記有不同屬性的頁面(當然,相同屬性的資料可能會被放到同一個頁面),以確保程式執行時的安全。正式基於這個原因,PE中才出現了所謂的節的概念。

節就是存放不同型別資料(比如程式碼、資料、常量、資源等)的地方,不同的節具有不同的訪問許可權。節是PE檔案中存放程式碼和資料的基本單元。例如:一個目標檔案中的所有程式碼可以組合成單個節,或者每個函式獨佔一格節(如果編譯器允許)。增加節的數目會增加檔案的開銷,但是連結器在連結程式碼的時候就會有更大的選擇餘地。一個節中的所有原始資料必須被載入到連續的記憶體空間中。

從作業系統載入角度來看,節是相同屬性資料的組合。與資料目錄不同的是,儘管有些資料型別不同,分別屬於不同的資料目錄,但是由於其訪問屬性相同,便被歸類到同一個節中。這個節最終可能會佔用一個或多個頁面;但無論有多少個,所有相關頁面均會被賦予相同的頁面屬性。這些屬性包括只讀、只寫、可讀、可寫等。

Windows作業系統在裝載PE檔案時會對相同和不同型別的節資料執行拋棄、合併、新增、複製等操作。這些不同的操作交叉組合導致了記憶體中的節和檔案中的節會出現很大的不同。例如:”.data”的資料在磁碟中不存在,但是在記憶體中存在,而”.reloc”重定位表資料卻恰恰相反。

2.3. 對齊(Alignment)

對齊這個概念並非只在PE檔案中出現,許多檔案格式都會有對齊的要求。有的對齊是為了美觀,有的對齊則是為了效率。PE中規定了三類的對齊:資料在記憶體中的對齊、資料在檔案中的對齊、資原始檔中資源資料的對齊。

2.3.1. 記憶體對齊(SectionAligment)

PE 檔案頭裡邊的SectionAligment 定義了記憶體中區塊的對齊值。由於Windows作業系統對記憶體屬性的設定以頁為單位,所以通常情況下,PE 檔案被對映到記憶體中時,節在記憶體中的對齊單位必須至少是一個頁的大小。對於32位的windows作業系統來說,這個值是4KB(1000h),而對64位作業系統來說,這個值就是8KB(2000h)。

2.3.2. 檔案對齊(FileAligment)

PE 檔案頭裡邊的FileAligment 定義了磁碟區塊的對齊值。每一個區塊從對齊值的倍數的偏移位置開始存放。而區塊的實際程式碼或資料的大小不一定剛好是這麼多,所以在多餘的地方一般以00h 來填充,這就是區塊間的間隙。為了提高磁碟利用率,對齊單位通常小於記憶體,以一個物理扇區的大小作為對齊粒度,512位元組,200h。

2.3.3. 資源資料對齊

資源位元組碼部分一般要求以雙字(4位元組)方式對齊。

3. PE檔案結構

PE 檔案格式被組織為一個線性的資料流,它由一個MS-DOS 頭部開始,接著是一個是模式的程式殘餘以及一個PE 檔案標誌,這之後緊接著PE檔案頭和可選頭部。這些之後是所有的段頭部,段頭部之後跟隨著所有的段實體。檔案的結束處是一些其它的區域,其中是一些混雜的資訊,包括重分配資訊、符號表資訊、行號資訊以及字串表資料。

如下圖所示,PE檔案結構被劃分為四大部份,包括:DOS部分、PE頭、節表、和節資料。

這裡寫圖片描述

PE檔案至少包含兩個段(節),即資料段和程式碼段。Windows的應用程式PE檔案格式有11個預定義段,這是對Windows應用程式所通用的。例如: .text 、.bss 、.rdata 、.data 、.pdata 和.debug 段,這些段並不是都是必須的,當然,也可以根據需要定義更多的段(比如一些加殼程式)。

在應用程式中最常出現的段有以下6種:

  • 執行程式碼段,通常 .text (Microsoft)或 CODE(Borland)命名;
  • 資料段,通常以 .data
    、.rdata 或 .bss(Microsoft)、DATA(Borland)命名;
  • 資源段,通常以 .rsrc命名;
  • 匯出表,通常以 .edata命名;
  • 匯入表,通常以 .idata命名;
  • 除錯資訊段,通常以 .debug命名;

PE檔案一個方便的特點是磁碟上的資料結構和載入到記憶體中的資料結構是相同的。載入一個可執行檔案到記憶體中 (例如,通過呼叫LoadLibrary)主要就是對映一個PE檔案中的幾個確定的區域到地址空間中。因此,一個數據結構比如IMAGE_NT_HEADERS (稍後我們會研究到)在磁碟上和在記憶體中是一樣的。關鍵的一點是如果你知道怎麼在一個PE檔案中找到一些東西,當這個PE檔案被載入到記憶體中後你幾乎能找到相同的資訊。

要注意到PE檔案並不僅僅是被對映到記憶體中作為一個記憶體對映檔案。代替的,Windows載入器分析這個PE檔案並決定對映這個檔案的哪些部分。當對映到記憶體中時檔案中偏移位置較高的資料對映到較高的記憶體地址處。一個專案在磁碟檔案中的偏移也許不同於它被載入到記憶體中時的偏移。然而,所有被表現出來的資訊都允許你進行從磁碟檔案偏移到記憶體偏移的轉換 (參見下圖)。

Windows裝載器在裝載的時候僅僅建立好虛擬地址和PE檔案之間的對映關係,只有真正執行到某個記憶體頁中的指令或訪問某一頁中的資料時,這個頁才會被從磁碟提交到實體記憶體。但因為裝載可執行檔案時,有些資料在裝入前會被預先處理(如需要重定位的程式碼),裝入以後,資料之間的相對位置也可能發生改變。因此,一個節的偏移和大小在裝入記憶體前後可能是完全不同的。

這裡寫圖片描述

這裡寫圖片描述

4. PE檔案結構分析

4.1. PE檔案準備

為了對PE檔案結構進行更好的分析,我們首先準備一個例子,這次我們通過在WIN10 X64環境下使用VS2015編譯生成的一個X64架構的release程式來進行分析。這個例子是一個在WIN10 X64下通過TLS實現反除錯的小程式。程式清單如下:

#include <Windows.h>
#include <tchar.h>

#pragma comment(lib,"ntdll.lib")

extern "C" NTSTATUS NTAPI NtQueryInformationProcess(HANDLE hProcess, ULONG InfoClass, PVOID Buffer, ULONG Length, PULONG ReturnLength);

#define NtCurrentProcess() (HANDLE)-1


void NTAPI __stdcall TLS_CALLBACK(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    if (IsDebuggerPresent())
    {
        MessageBoxA(NULL, "TLS_CALLBACK: Debugger Detected!", "TLS Callback", MB_OK);
//      ExitProcess(1);
    }
    else
    {
        MessageBoxA(NULL, "TLS_CALLBACK: No Debugger Present!...", "TLS Callback", MB_OK);
    }
}

void NTAPI __stdcall TLS_CALLBACK_2(PVOID DllHandle, DWORD Reason, PVOID Reserved)
{
    HANDLE DebugPort = NULL;
    if (!NtQueryInformationProcess(
        NtCurrentProcess(),
        7,          // ProcessDebugPort
        &DebugPort, // If debugger is present, it will be set to -1 | Otherwise, it is set to NULL
        sizeof(HANDLE),
        NULL))
    {
        if (DebugPort)
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: Debugger detected!", "TLS callback", MB_ICONSTOP);
        }

        else
        {
            MessageBoxA(NULL, "TLS_CALLBACK2: No debugger detected", "TLS callback", MB_ICONINFORMATION);
        }
    }
}

//linker spec通知連結器PE檔案要建立TLS目錄,注意X86和X64的區別
#ifdef _M_IX86
#pragma comment (linker, "/INCLUDE:__tls_used")
#pragma comment (linker, "/INCLUDE:__tls_callback")
#else
#pragma comment (linker, "/INCLUDE:_tls_used")
#pragma comment (linker, "/INCLUDE:_tls_callback")
#endif
//建立TLS段
EXTERN_C
#ifdef _M_X64
#pragma const_seg (".CRT$XLB")
const
#else
#pragma data_seg (".CRT$XLB")
#endif
//end linker

//tls import定義多個回撥函式
PIMAGE_TLS_CALLBACK _tls_callback[] = { TLS_CALLBACK, TLS_CALLBACK_2, 0 };
#pragma data_seg ()
#pragma const_seg ()
//end 

int APIENTRY _tWinMain(HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPTSTR    lpCmdLine,
    int       nCmdShow)
{

    MessageBoxA(NULL, "Hello Wolrd!...:)", "main()", MB_OK);
    return 0;

}

程式是一個簡單的windows小程式,通過使用TLS回撥函式來檢測偵錯程式的存在,並彈出視窗提示檢測到的狀態,主程式只是一個簡單的hello world訊息窗。程式中涉及到PE檔案中幾個重要的節。程式通過在WIN10 X64環境下使用VS2015編譯生成的一個X64架構的release版本tlstest.exe,程式大小為12.0 KB (12,288 位元組)。

這裡寫圖片描述

4.2. MS-DOS 檔案頭

在 image 檔案的最開始處就是 DOS 檔案頭,DOS 檔案頭包含了 DOS stub 小程式。在 WinNT.h 檔案裡定義了一個結構來描述 DOS 檔案頭。

typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
    WORD   e_magic;             // 00000000  4D 5A,Magic number
    WORD   e_cblp;          // 00000002  90 00,Bytes on last page of file
    WORD   e_cp;             // 00000004  03 00,Pages in file
    WORD   e_crlc;            // 00000006  00 00,Relocations
    WORD   e_cparhdr;           // 00000008  04 00,Size of header in paragraphs
    WORD   e_minalloc;        // 0000000A  00 00,Minimum extra paragraphs needed
    WORD   e_maxalloc;          // 0000000C  FF FF,Maximum extra paragraphs needed
    WORD   e_ss;                // 0000000E  00 00,Initial (relative) SS value
    WORD   e_sp;             // 00000010  B8 00,Initial SP value
    WORD   e_csum;          // 00000012  00 00,Checksum
    WORD   e_ip;                // 00000014  00 00,Initial IP value
    WORD   e_cs;                // 00000016  00 00,Initial (relative) CS value
    WORD   e_lfarlc;        // 00000018  40 00,File address of relocation table
    WORD   e_ovno;           // 0000001A  00 00,Overlay number
WORD   e_res[4];            // 0000001C  00 00 00 00,Reserved words
        // 00000020  00 00 00 00
    WORD   e_oemid;             // 00000024  00 00,OEM identifier (for e_oeminfo)
    WORD   e_oeminfo;           // 00000026  00 00,OEM information; e_oemid specific
WORD   e_res2[10];          // 00000028  00 00 00 00,Reserved words
        // 0000002C  00 00 00 00
        // 00000030  00 00 00 00
        // 00000034  00 00 00 00
        // 00000038  00 00 00 00
    LONG    e_lfanew;           // 0000003C  F8 00 00 00,File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

這個結構名叫 IMAGE_DOS_HEADER 共 64 bytes,以 IMAGE_DOS_HEADER 結構描述的 DOS 檔案頭結構從 image 的 0x00000000 - 0x0000003F(64 bytes)

結構的 e_magic 域是 DOS 標頭檔案簽名,它的值是:0x5A4D 代表字元 MZ,它在 WinNT.h 裡定義為:

#define IMAGE_DOS_SIGNATURE                 0x5A4D      // MZ

e_lfanew 域是一個 offset 值,它指出 NT 檔案頭的位置。

下面看看tlstest.exe 的 DOS 檔案頭內容:

這裡寫圖片描述

綠色部分是 DOS 簽名,藍色部分是 PE header offset(NT 檔案頭)值,也就是 IMAGE_DOS_HEADER 裡的 e_lfanew 值,表明 NT 檔案頭在 image 檔案的 0x000000F8 處。

4.2.1. DOS stub 程式

在 DOS 檔案頭下面緊跟著一小段 stub 程式,其內容隨著連結時使用的連結器不同而不同,在PE中並沒有與之對應的相關結構。在本例中從 0x00000040 - 0x0000004D 共 14 bytes是程式碼,其後是顯示資料等內容,這段 dos stub 程式是這樣的:

00000040  0E                push cs
00000041  1F                pop ds
00000042  BA0E00       mov dx,0xe
00000045  B409            mov ah,0x9
00000047  CD21           int 0x21
00000049  B8014C       mov ax,0x4c01
0000004C  CD21          int 0x21

當 windows 的 PE 檔案放在 DOS 上執行時,將會執行這一段 DOS stub 程式,作用是列印資訊:This program cannot be run in DOS mode…. 然後呼叫 int 21 來終止執行返回到 DOS,看看它是怎樣執行的:

00000014  00 00      // ip
00000016  00 00      // cs
00000018  40 00      // e_lfarlc

這個 DOS 執行環境中,CS 和 IP 被初始化為 0(對應值在IMAGE_DOS_HEADER結構中的e_ip、e_cs欄位),e_lfarlc 是 DOS 環境的 relocate 表,它的值是 0x40 ,那麼資訊字串的位置是:0x0040 + 0x000e = 0x4e,在 image 檔案 0x0000004e 正好這字串的位置。

4.3. NT 檔案頭

NT 檔案頭是 PE 檔案頭的核心部分,由 IMAGE_DOS_HEADER 結構的 e_lfanew 域指出它的位置。

同樣 NT 檔案頭部分由一個結構 IMAGE_NT_HEADER 來描述,在 WinNT.h 裡定義如下:

typedef struct _IMAGE_NT_HEADERS64 {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER64 OptionalHeader;
} IMAGE_NT_HEADERS64, *PIMAGE_NT_HEADERS64;
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;
    IMAGE_FILE_HEADER FileHeader;
    IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
#ifdef _WIN64
typedef IMAGE_NT_HEADERS64                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS64                PIMAGE_NT_HEADERS;
#else
typedef IMAGE_NT_HEADERS32                  IMAGE_NT_HEADERS;
typedef PIMAGE_NT_HEADERS32                PIMAGE_NT_HEADERS;
#endif

可見這個結構分為 32 和 64 位版本,IMAGE_NT_HEADER 結構分為三大部分:

  • PE 檔案簽名:Signature
  • IMAGE_FILE_HEADER 檔案頭:FileHeader
  • IMAGE_OPTINAL_HEADER(32/64) 可選頭:OptionalHeader

IMAGE_NT_HEADERS32 和 IMAGE_NT_HEADERS64 的匹別在於 IMAGE_OPTIONAL_HEADER 結構,分別為:IMAGE_OPTIONAL_HEADERS32 和 IMAGE_OPTIONAL_HEADERS64

在 Win32 下 IMAGE_NT_HEADERS32 是 248 bytes,在 Win64 下 IMAGE_NT_HEADERS64 是 264 bytes,因此 tlstest.exe的 NT 檔案頭從 0x000000F8 - 0x000001FF 共 264 bytes

4.3.1. PE 簽名

在 WinNT.h 檔案裡定義了 PE 檔案的簽名,它是:

#define IMAGE_NT_SIGNATURE                  0x00004550  // PE00

這個簽名值是 32 位,值為:0x00004550 即:PE 的 ASCII 碼,下面看看 tlstest.exe 中的 PE 簽名:

這裡寫圖片描述

4.3.2. IMAGE_FILE_HEADER 檔案頭結構

PE 簽名接著是 IMAGE_FILE_HEADER 結構,它在 WinNT.h 中的定義為:

typedef struct _IMAGE_FILE_HEADER {   
    WORD   Machine;               //執行平臺 
    WORD   NumberOfSections;     //塊(section)數目      
      DWORD    TimeDateStamp;        //時間日期標記     
      DWORD   PointerToSymbolTable;    //COFF符號指標,這是程式除錯資訊    
     DWORD   NumberOfSymbols;         //符號數  
      WORD   SizeOfOptionalHeader;    //可選部首長度,是IMAGE_OPTIONAL_HEADER的長度    
     WORD   Characteristics;         //檔案屬性 
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

這個 IMAGE_FILE_HEADER 對 PE 檔案大致的描述,這個結構共 20 bytes,它的域描述如下:

size 描述
Machine WORD IMAGE_FILE_MACHINE_xxx 表示目標平臺 processor 型別,例:IMAGE_FILE_MACHINE_I386
NumberOfSections WORD 節數量 表示映象中有多少個 section
TimeDataStamp DWORD 從1970年1月1日0:00 以來的總秒數 表示檔案建立的時間
PointerToSymbolTable DWORD COFF 符號表偏移量 在 image 檔案中很少見,總是為 0
NumberOfSymbols DWORD COFF 符號表的個數 如果存在的話,表示符號表的個數
SizeOfOptionalHeader WORD IMAGE_OPTIONAL_HEADER 結構大小 該域表示 IMAGE_NT_HEADER 中的 IMAGE_OPTIONAL_HEADER 結構的大小
Characteristics WORD IMAGE_FILE_xxx 表示檔案屬性,例如:IMAGE_FILE_DLL 屬性

IMAGE_FILE_HEADER 結構中比較重要的域是:Machine 和 SizeOfOptionalHeader, Machine 可以用來判斷目標平臺,比如:值為 0x8664 是代表 AMD64(即:x64 平臺)它也適合 Intel64 平臺。SizeOfOptionalHeader 指出 IMAGE_OPTIONAL_HEADER 結構的大小。

WinNT.h 中定義了一些常量值用來描述 Machine,以 IMAGE_FILE_MACHINE_XXX 開頭,下面是一些典型的常量值:

#define IMAGE_FILE_MACHINE_UNKNOWN           0
#define IMAGE_FILE_MACHINE_I386                   0x014c  // Intel 386.
#define IMAGE_FILE_MACHINE_ALPHA                0x0184  // Alpha_AXP
#define IMAGE_FILE_MACHINE_POWERPC            0x01F0  // IBM PowerPC Little-Endian
#define IMAGE_FILE_MACHINE_AMD64                0x8664  // AMD64 (K8)

WinNT.h 中還針對 Characteristics 域定義了一些常量值,以 IMAGE_FILE_XXX 開頭,代表目標 image 檔案的型別,下面是一些常見的值:

#define IMAGE_FILE_RELOCS_STRIPPED              0x0001  // Relocation info stripped from file.
#define IMAGE_FILE_EXECUTABLE_IMAGE          0x0002  // File is executable  (i.e. no unresolved externel references).
#define IMAGE_FILE_LINE_NUMS_STRIPPED        0x0004  // Line nunbers stripped from file.
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED     0x0008  // Local symbols stripped from file.
#define IMAGE_FILE_AGGRESIVE_WS_TRIM         0x0010  // Agressively trim working set
#define IMAGE_FILE_LARGE_ADDRESS_AWARE  0x0020  // App can handle >2gb addresses
#define IMAGE_FILE_BYTES_REVERSED_LO         0x0080  // Bytes of machine word are reversed.
#define IMAGE_FILE_32BIT_MACHINE                 0x0100  // 32 bit word machine.
#define IMAGE_FILE_DEBUG_STRIPPED               0x0200  // Debugging info stripped from file in .DBG file
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP   0x0400  // If Image is on removable media, copy and run from the swap file.
#define IMAGE_FILE_NET_RUN_FROM_SWAP        0x0800  // If Image is on Net, copy and run from the swap file.
#define IMAGE_FILE_SYSTEM                            0x1000  // System File.
#define IMAGE_FILE_DLL                                 0x2000  // File is a DLL.
#define IMAGE_FILE_UP_SYSTEM_ONLY              0x4000  // File should only be run on a UP machine
#define IMAGE_FILE_BYTES_REVERSED_HI         0x8000  // Bytes of machine word are reversed.

可以看出Characteristics是以每一位表示檔案屬性,它的每一個bit代表的含義如下:

         Bit 0 :置1表示檔案中沒有重定向資訊。每個段都有它們自己的重定向資訊。
                 這個標誌在可執行檔案中沒有使用,在可執行檔案中是用一個叫做基址重定向目錄表來表示重定向資訊的,這將在下面介紹。
        Bit 1 :置1表示該檔案是可執行檔案(也就是說不是一個目標檔案或庫檔案)。
     Bit 2 :置1表示沒有行數資訊;在可執行檔案中沒有使用。
     Bit 3 :置1表示沒有區域性符號資訊;在可執行檔案中沒有使用。
     Bit 4Bit 7 
     Bit 8 :表示希望機器為32位機。
     Bit 9 :表示沒有除錯資訊,在可執行檔案中沒有使用。
     Bit 10:置1表示該程式不能運行於可移動介質中(如軟碟機或CD-ROM)。在這    種情況下,OS必須把檔案拷貝到交換檔案中執行。
         Bit 11:置1表示程式不能在網上執行。在這種情況下,OS必須把檔案拷貝到交換檔案中執行。
         Bit 12:置1表示檔案是一個系統檔案例如驅動程式。在可執行檔案中沒有使用。
         Bit 13:置1表示檔案是一個動態連結庫(DLL)。
         Bit 14:表示檔案被設計成不能運行於多處理器系統中。
         Bit 15:表示檔案的位元組順序如果不是機器所期望的,那麼在讀出之前要進行
                 交換。在可執行檔案中它們是不可信的(作業系統期望按正確的位元組順序執行程式)。

NumberOfSections 表示 image 有多個 section,另一個重要的域是:SizeOfOptionalHeader 它指出接下來 IMAGE_OPTIONAL_HEADER 的大小,它有兩個 size:Win32 的 0xDC 和 Win64 的 0xF0.

下面是 tlstest.exe的 IMAGE_FILE_HEADER 結構,從 0x000000F8 - 0x0000010F 共 20 bytes:

這裡寫圖片描述

將這些值分解為:
000000FC  64 86                   // Machine
000000FE  07 00                   // NumberOfSections
00000100  F1 71 1E 57           // TimeDateStamp
00000104  00 00 00 00           // PointerToSymbolTable
00000108  00 00 00 00           // NumberOfSymbols
0000010C  F0 00                   // SizeOfOptionalHeader
0000010E  22 00                   // Characteristics
  • Machine 是 0x0x8664,它的值是 IMAGE_FILE_MACHINE_AMD64,說明這個 image
    檔案的目標平臺是 AMD64,即:X64 平臺
  • NumberOfSections 是 0x08,說明 image 檔案內含有 8 個 sections
  • SizeOfOptionalHeader 是 0xF0,說明接下來的 IMAGE_OPTIONAL_HEADERS64 將是
    0xF0(224 bytes)

它的 Characteristics 是 0x0022 = IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE,說明這個 image 是 64 位可執行的映像。可以在 >2G 地址上,並且指明瞭接下來的 IMAGE_OPTIONAL_HEADER 結構是 0xf0 bytes(240 個位元組)。

4.3.3. IMAGE_OPTIONAL_HEADER64 結構

在 IMAGE_FILE_HEADER 結構裡已經指明瞭 image 是 64 位,並且 IMAGE_OPTIONAL_HEADER 的大小是 240 bytes,那麼這個結構就是 IMAGE_OPTIONAL_HEADER64 結構。
可以根據 IMAGE_FILE_HEAER 結構的 Machine 來判斷 image 是 Win32 還是 Win64 平臺的。但是 Microsoft 官方推薦及認可的方法是從 IMAGE_OPTIONAL_HEADER 裡的 magic 的值來判斷目標平臺 。

在 WinNT.h 裡 IMAGE_OPTIONAL_HEADER64 的定義如下:

typedef struct _IMAGE_OPTIONAL_HEADER64 {

    WORD          Magic;
    BYTE           MajorLinkerVersion;
    BYTE           MinorLinkerVersion;
    DWORD        SizeOfCode;
    DWORD        SizeOfInitializedData;
    DWORD        SizeOfUninitializedData;
    DWORD        AddressOfEntryPoint;
    DWORD        BaseOfCode;
    ULONGLONG   ImageBase;
    DWORD        SectionAlignment;
    DWORD        FileAlignment;
    WORD          MajorOperatingSystemVersion;
    WORD          MinorOperatingSystemVersion;
    WORD          MajorImageVersion;
    WORD          MinorImageVersion;
    WORD          MajorSubsystemVersion;
    WORD          MinorSubsystemVersion;
    DWORD        Win32VersionValue;
    DWORD        SizeOfImage;
    DWORD        SizeOfHeaders;
    DWORD        CheckSum;
    WORD          Subsystem;
    WORD          DllCharacteristics;
    ULONGLONG  SizeOfStackReserve;
    ULONGLONG  SizeOfStackCommit;
    ULONGLONG  SizeOfHeapReserve;
    ULONGLONG  SizeOfHeapCommit;
    DWORD        LoaderFlags;
    DWORD        NumberOfRvaAndSizes;
    IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER64, *PIMAGE_OPTIONAL_HEADER64;

64 位的 IMAGE_OPTIONAL_HEADER64 裡沒有 BaseOfData 域,其它的與 IMAGE_OPTIONAL_HEADER32 結構的域是一樣的,只是一些域擴充套件為 64 位值,它們包括:

  • ImageBase
  • SizeOfStackReserve
  • SizeOfStackCommit
  • SizeOfHeapRerserve
  • SizeOfHeapCommit

這些域在 64 位結構裡被定義為 ULONGLONG 型別。

IMAGE_OPTIONAL_HEADER64 的定義的各個域含義如下:

偏移 大小 欄位 描述
0 2 Magic The unsigned integer that identifies the state of the image file. The most common number is 0x10B, which identifies it as a normal executable file. 0x107 identifies it as a ROM image, and 0x20B identifies it as a PE32+ executable. 確定這是什麼型別的頭。兩個最常用的值是0x10b和0x20b.
2 1 MajorLinkerVersion The linker major version number. 建立可執行檔案的連結器的主版本號。對於Microsoft的連結器生成的PE檔案,這個版本號的Visual Studio的版本號相一致
3 1 MinorLinkerVersion The linker minor version number. 建立可執行檔案的連結器的次版本號。
4 4 SizeOfCode The size of the code (text) section, or the sum of all code sections if there are multiple sections. 所有具有IMAGE_SCN_CNT_CODE屬性的節的總的大小。
8 4 SizeOfInitializedData The size of the initialized data section, or the sum of all such sections if there are multiple data sections. 所有包含已初始資料的節的總的大小。
12 4 SizeOfUninitializedData The size of the uninitialized data section (BSS), or the sum of all such sections if there are multiple BSS sections. 所有包含未初始化資料的節的總的大小。這個域總是0,因為連結器可以把未初始化資料附加到常規資料節的末尾。
16 4 AddressOfEntryPoint The address of the entry point relative to the image base when the executable file is loaded into memory. For program images, this is the starting address. For device drivers, this is the address of the initialization function. An entry point is optional for DLLs. When no entry point is present, this field must be zero. 檔案中將被執行的第一個程式碼位元組的RVA。對於DLL,這個進入點將在程序初始化和關閉時以及執行緒被建立和銷燬時呼叫。在大多數可執行檔案中,這個地址並不直接指向main,WinMain或DllMain函式,而是指向執行時庫程式碼,由執行時庫呼叫前述函式。在DLL中,這個域可以被設為0。連結器選項/NOENTRY可以設定這個域為0。
20 4 BaseOfCode The address that is relative to the image base of the beginning-of-code section when it is loaded into memory. 載入到記憶體後代碼的第一個位元組的RVA
24 4 BaseOfData The address that is relative to the image base of the beginning-of-data section when it is loaded into memory. 理論上,它表示載入到記憶體後資料的第一個位元組的RVA。然而,這個域的值對於不同版本的Microsoft連結器是不一致的。在64位的可執行檔案中這個域不出現。
28/24 4/8 ImageBase The preferred address of the first byte of image when loaded into memory; must be a multiple of 64 K. The default for DLLs is 0x10000000. The default for Windows CE EXEs is 0x00010000. The default for Windows NT, Windows 2000, Windows XP, Windows 95, Windows 98, and Windows Me is 0x00400000. 檔案在記憶體中的首選載入地址。載入器儘可能地把PE檔案載入到這個地址(就是說,如果當前這塊記憶體沒有被佔用,它是對齊的並且是一個合法的地址,等等)。如果可執行檔案被載入到這個地址,載入器就可以跳過進行基址重定位這一步。在Win32下,對於EXE,預設的ImageBase是0x400000。對於DLL,預設是0x10000000。在連結時可以通過/BASE 選項來指定ImageBase,或者以後用REBASE工具重新設定。
32/32 4 SectionAlignment The alignment (in bytes) of sections when they are loaded into memory. It must be greater than or equal to FileAlignment. The default is the page size for the architecture. 載入到記憶體後節的對齊大小。這個值必須大於等於FileAlignment(下一個域)。預設的對齊值是目標CPU的頁大上。對於執行在Windows 9x或Windows Me下的使用者模式可執行檔案,最小對齊大小是一頁(4KB)。這個域可以通過連結器選項/ALIGN來設定。
36/36 4 FileAlignment The alignment factor (in bytes) that is used to align the raw data of sections in the image file. The value should be a power of 2 between 512 and 64 K, inclusive. The default is 512. If the SectionAlignment is less than the architecture’s page size, then FileAlignment must match SectionAlignment. 在PE檔案中節的對齊大小。對於x86下的可執行檔案,這個值通常是0x200或0x1000。不同版本的Microsoft連結器預設值不同。這個值必須是2的冪,並且如果SectionAlignment小於CPU的頁大小,這個域必須和SectionAlignment相匹配。連結器選項/OPT:WIN98可設定x86可執行檔案的檔案對齊為0x1000,/OPT:NOWIN98設定檔案對齊為0x200。
40/40 2 MajorOperatingSystemVersion The major version number of the required operating system. 所要求的作業系統的主版本號。隨著那麼多版本Windows的出現,這個域的值就變得很不確切。
42/42 2 MinorOperatingSystemVersion The minor version number of the required operating system. 所要求的作業系統的次版本號。
44/44 2 MajorImageVersion The major version number of the image. 這個檔案的主版本號。不被系統使用並可設為0。可以通過連結器選項/VERSION來設定。
46/46 2 MinorImageVersion The minor version number of the image. 這個檔案的次版本號。
48/48 2 MajorSubsystemVersion The major version number of the subsystem. 可執行檔案所要求的操作子系統的主版本號。它曾經被用來表示需要較新的Windows 95或Windows NT使用者介面,而不是老版本的Windows NT介面。今天隨著各種不同版本Windows的出現,這個域已不被系統使用,並且通常被設為4。可通過連結器選項/SUBSYSTEM設定這個域的值。
50/50 2 MinorSubsystemVersion The minor version number of the subsystem. 可執行檔案所要求的操作子系統的次版本號。
52/52 4 Win32VersionValue Reserved, must be zero. 另一個不被使用的域,通常設為0。
56/56 4 SizeOfImage The size (in bytes) of the image, including all headers, as the image is loaded in memory. It must be a multiple of SectionAlignment. 映像的大小。它表示了載入檔案到記憶體中時系統必須保留的記憶體的數量。這個域的值必須是SectionAlignmnet的倍數。
60/60 4 SizeOfHeaders The combined size of an MS-DOS stub, PE header, and section headers rounded up to a multiple of FileAlignment. MS-DOS頭,PE頭和節表的總的大小。PE檔案中所有這些專案出現在任何程式碼或資料節之前。這個域的值被調整為檔案對齊大小的整數倍。
64/64 4 CheckSum The image file checksum. The algorithm for computing the checksum is incorporated into IMAGHELP.DLL. The following are checked for validation at load time: all drivers, any DLL loaded at boot time, and any DLL that is loaded into a critical Windows process. 映像的校驗和。IMAGEHLP.DLL中的CheckSumMappedFile函式可以計算出這個值。校驗和用於核心模式的驅動和一些系統DLL。對於其它的,這個域可以為0。當使用連結器選項/RELEASE時校驗和被放入檔案中。
68/68 2 Subsystem The subsystem that is required to run this image. For more information, see “Windows Subsystem” later in this specification. 指示可執行檔案期望的子系統(使用者介面型別)的列舉值。這個域只用於EXE。一些重要的值包括:
IMAGE_SUBSYSTEM_NATIVE // 映像不需要子系統
IMAGE_SUBSYSTEM_WINDOWS_GUI // 使用Windows GUI IMAGE_SUBSYSTEM_WINDOWS_CUI // 作為控制檯程式執行。
// 執行時,作業系統建立一個控制檯
// 視窗並提供stdin,stdout和stderr
// 檔案控制代碼。
70/70 2 DllCharacteristics For more information, see “DLL Characteristics” later in this specification. 標記DLL的特性。對應於IMAGE_DLLCHARACTERISTICS_xxx定義。當前的值是:
IMAGE_DLLCHARACTERISTICS_NO_BIND // 不要繫結這個映像
IMAGE_DLLCHARACTERISTICS_WDM_DRIVER // WDM模式的驅動程式
IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE // 當終端服務載入一個不是
// Terminal- Services-aware 的應用程
// 序時,它也載入一個包含相容程式碼
// 的DLL。
72/72 4/8 SizeOfStackReserve The size of the stack to reserve. Only SizeOfStackCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE檔案中,為執行緒保留的堆疊大小。預設是1MB,但並不是所有的記憶體一開始都被提交。
76/80 4/8 SizeOfStackCommit The size of the stack to commit. 在EXE檔案中,為堆疊初始提交的記憶體數量。預設情況下,這個域是4KB。
80/88 4/8 SizeOfHeapReserve The size of the local heap space to reserve. Only SizeOfHeapCommit is committed; the rest is made available one page at a time until the reserve size is reached. 在EXE檔案中,為預設程序堆初始保留的記憶體大小。預設是1MB。然而在當前版本的Windows中,堆不經過使用者干涉就能超出這裡指定的大小。
84/96 4/8 SizeOfHeapCommit The size of the local heap space to commit. 在EXE檔案中,提交到堆的記憶體大小。預設情況下,這裡的值是4KB。
88/104 4 LoaderFlags Reserved, must be zero. 不使用。
92/108 4 NumberOfRvaAndSizes The number of data-directory entries in the remainder of the optional header. Each describes a location and size. 在IMAGE_NT_HEADERS結構的末尾是一個IMAGE_DATA_DIRECTORY結構陣列。此域包含了這個陣列的元素個數。自從最早的Windows NT釋出以來這個域的值一直是16。

上面表格中的 offset 值兩個,前面的是 IMAGE_OPTIONAL_HEADER32 的 offset 值,後面的是 IMAGE_OPTIONAL_HEADER64,這是因為在 64 位版本中一些域被擴充套件為 64 位值,而 BaseOfData 域在 64 位版中是不存在的。
Magic 域是一個幻數值,在 WinNT.h 裡定義了一些常量值:

#define IMAGE_NT_OPTIONAL_HDR32_MAGIC      0x10b
#define IMAGE_NT_OPTIONAL_HDR64_MAGIC      0x20b
#define IMAGE_ROM_OPTIONAL_HDR_MAGIC       0x107
  • 值 0x10b 說明這個 image 是 32 位的,PE 檔案格式是 PE32
  • 值 0x20b 說明這個 image 是 64 位的,PE 檔案格式是 PE32+

PE32+ 代表的擴充套件的 PE 檔案格式,擴充套件為 64 位。在 PE 檔案規範中並沒有 PE64 這種檔案格式,Microsoft 官方的判斷 image 檔案是 32 位還是 64 位的方法就是通過 Magic 的值來確定。

在這些基本的域裡可以獲得 linker 的版本,text 節,data 節以及 bss 節的大小,下面看一看tlstest.exe 的 IMAGE_OPTIONAL_HEADER64 結構,從 0x00000110 - 0x000001FF

這裡寫圖片描述

這裡寫圖片描述

Magic 是 0x020B 表明這個 image 檔案是 64 位的 PE32+格式,這裡看出 linker 的版本是 14.00
.text 節的 size 是 0x00001000 bytes,.data 節的 size 是 0x00022000 bytes,還有一個重要的資訊,程式碼的 RVA 入口在 0x00001338,它是基於 ImageBase 的 RVA 值。tlstest.exe 的 ImageBase 是0x0000000140000000,那麼 tlstest.exe 映象的入口在:ImageBase + AddressOfEntryPoint = 0x0000000140000000 + 0x00001338 = 0x0000000140001338,這個地址是 __scrt_common_main() 的入口。

上面的 SectionAlinment 域值為 0x1000 是表示映象被載入到 virtual address 以是 0x1000(4K byte)為單位的倍數,也就是載入在 virtual address 的 4K 邊界上。例如:tlstest.exe映象的 .text 節被載入到以 ImageBase(virtual address 為 0x00000001_40000000)為基址的第 1 個 4K 邊界上(即:0x00000001_40001000 處),.rdata 節載入到第 2 個 4K 邊界上(即:0x00000001_40002000 處)。FileAlinment 域的值為 0x200 表示執行映象從 0x200 為邊界開始載入到 virtual address 上。例如,t.exe 映象中 code 位於檔案映象的 0x400 處(0x200 邊界上),因此,t.exe 檔案映象 code 從 0x400 處開始載入到 virtual address。

4.3.3.1. IMAGE_DATA_DIRECTORY表格

這裡寫圖片描述

在 IMAGE_OPTIONAL_HEADER64 未端是一組 IMAGE_DATA_DIRECTORY 結構的陣列,上圖所示:從 0x00000180 到 0x000001FF。在 WinNT.h 裡定義為:
//
// Directory format.
//
typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD   VirtualAddress;
    DWORD   Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES    16

這個結構十分重要,它用來描述 windows 執行映象中所使用的各種表格的位置和大小。VirtualAddress 域是一個 RVA(Relative Virtual Address)值,更明白一點就是:它是一個偏移量(基於 PE 檔案頭),Size 域表示這個表格有多大。IMAGE_NUMBEROF_DIRECTORY_ENTRIES 的值為 16,因此有 16 個 Directory,也就是表示,在執行映象中最多可以使用 16 個表格。

這 16 個 Driectory 指引出 16 不同格式的 table,實際上這 16 個表格是固定的,對於這些表格 Microsoft 都作了統一的規定,在 WinNT.h 裡都作了定義:

// Directory Entries
#define IMAGE_DIRECTORY_ENTRY_EXPORT              0   // Export Directory
#define IMAGE_DIRECTORY_ENTRY_IMPORT              1   // Import Directory
#define IMAGE_DIRECTORY_ENTRY_RESOURCE          2   // Resource Directory
#define IMAGE_DIRECTORY_ENTRY_EXCEPTION         3   // Exception Directory
#define IMAGE_DIRECTORY_ENTRY_SECURITY           4   // Security Directory
#define IMAGE_DIRECTORY_ENTRY_BASERELOC         5   // Base Relocation Table
#define IMAGE_DIRECTORY_ENTRY_DEBUG                6   // Debug Directory
//      IMAGE_DIRECTORY_ENTRY_COPYRIGHT           7   // (X86 usage)
#define IMAGE_DIRECTORY_ENTRY_ARCHITECTURE    7   // Architecture Specific Data
#define IMAGE_DIRECTORY_ENTRY_GLOBALPTR         8   // RVA of GP
#define IMAGE_DIRECTORY_ENTRY_TLS