可移植可執行

可移植性可執行文件英語:Portable Executable,縮寫為PE)是一種用於可執行文件目標文件動態鏈接庫文件格式,主要使用在32位和64位的Windows操作系統上。「可移植的」是指該文件格式的通用性,可用於許多種不同的操作系統和體系結構中。PE文件格式封裝了Windows操作系統加載可執行程序代碼時所必需的一些信息。這些信息包括動態鏈接庫API導入和導出表、資源管理數據和線程局部存儲數據。在Windows NT操作系統中,PE文件格式主要用於EXE文件、DLL文件、.sys(驅動程序)和其他文件類型。可擴展固件接口(EFI)技術規範書中說明PE格式是EFI環境中的標準可執行文件格式。開頭為DOS頭部

可移植性可執行文件
副檔名
.acm, .ax, .cpl, .dll, .drv, .efi, .exe, .mui, .ocx, .scr, .sys, .tsp
網路媒體型式
application/vnd.microsoft.portable-executable
開發者Microsoft
格式類型二進制可執行文件目標代碼函式庫
延伸自DOS MZ可執行文件
COFF

PE格式是由Unix中的COFF格式修改而來的。在Windows開發環境中,PE格式也稱為PE/COFF格式。

在Windows NT操作系統中,PE格式目前支持IA-32IA-64x86-64(AMD64/Intel64)的指令系統。在Windows 2000之前,Windows NT還支持MIPSAlphaPowerPC的指令系統。由於Windows CE也在使用PE文件格式,因此PE仍然支持幾種不同型號的MIPS、ARM(包括Thumb)和SuperH指令系統。

PE文件格式的主要競爭對手是可執行與可鏈接格式(ELF)(使用於Linux和大多數Unix版本中)和Mach-O(使用於Mac OS X中)。

布局結構

文件頭部

MS-DOS頭與MS-DOS stub

MS-DOS頭和MS-DOS Stub只存在於映像文件中。在MS-DOS下運行該應用程序,默認的Stub會打印出消息"This program cannot be run in DOS mode"。 MS-DOS頭的偏移位置0x3C處包括指向PE簽名的文件指針,用於定位PE的開始位置。

COFF file header

PE簽名是一個4字節的項:字符P和E,隨後是2個空字節。

在Winnt.h中定義的標準COFF頭的結構_IMAGE_FILE_HEADER

optional header

Object文件不含這部分,所以稱為「可選頭」。

在Winnt.h中定義的optional header的結構_IMAGE_OPTIONAL_HEADER

Standard COFF Fields
Windows Specific Fields
Data Directories
  • Export Table: .edata Section
  • Import Table: .idata Section.
  • Resource Table: .rsrc Section.
  • Exception Table: .pdata Section.
  • Certificate Table: 指向Attribute Certificate Table(由用於文件驗證的屬性證書組成的表). 屬性證書不會作為映像文件的一部分加載到內存。同樣,這個地址項的第一個字段是文件指針而不是RVA。屬性證書表的每個項都包括了一個4字節的文件指針,指向各自的屬性證書,並具有4字節的大小。
  • Base Relocation Table: .reloc Section
  • Debug: .debug Section. 調試數據輸出到PDB文件中,因此這個Data directory要麼全都是0,或者只指向一個類型為2(IMAGE_DEBUG_TYPE_CODEVIEW)、30個字節的調試目錄項,而這個項又指向一個包括PDB文件路徑在內的、CodeReview風格的頭。
  • Architecture: 保留,必須為0
  • Global Ptr: 在全局指針寄存器中存儲的RVA值。該結構的大小必須設置為0。如果目標架構沒有使用全局指針的概念,這個數據目錄就全都設置為0(例如I386或AMD64)。
  • TLS Table: .tls Section.
  • Load Config Table: Load Configuration Structure. 特定於Window NT家族操作系統的數據(例如,GlobalFlag值)。
  • Bound Import: 由綁定導入描述符組成的數組,其中的每個描述符都描述了一個DLL。在創建映像的時候,該映像與DLL綁定在一起。描述符中還攜帶這些綁定的時間戳,如果這些綁定是最新的,那麼操作系統加載程序就會使用這些綁定以作為API導入的"快捷方式"。否則,加載程序就會忽視這些綁定並通過導入表解析這些導入API。
  • IAT: Import Address Table. 這個表(IAT)會在導出目錄表(第1個數據目錄)中被引用。
  • Delay Import Descriptor: Delay-Load Import Tables. 包括一個由32位ImgDelayDescr結構體組成的數組,每個結構體都描述了延遲加載的導入。延遲加載(delay load)的導入是這樣一些DLL,它們被描述為隱式的導入,而作為顯式的導入進行加載(通過對LoadLibrary這個API的調用)。動態庫的延遲加載是根據需要--在第一次調用這樣一個DLL的時候執行的。這與隱式的導入不同,後者在導入的可執行體初始化的時候就立即被加載。
  • CLR Runtime Header: .cormeta Section (Object Only).
  • 保留: 必須全為0

節(section)是PE文件中存儲數據的劃分。通常,加載到內存後,同一節的數據具有相同的內存訪問屬性(可讀/可寫/可執行等)。

節表緊隨文件頭部。由於沒有文件頭具有直接指向節表的指針,所以節表的位置被計算為文件頭的大小再加上1。

COFF頭的NumberOfSections字段,定義了節表中節的數量。在節頭表中,節的索引是基於1,並且節的順序是由鏈接器確定。節按照節表中定義的順序連續存放,起始RVA根據PE頭的SectionAlignment字段值進行對齊。

節頭是一個定義在Winnt.h中的40字節的結構IMAGE_SECTION_HEADER:

  • Name: 8字節的ASCII字符串。表示節的名稱。節名稱開始於一個點號(例如,.reloc)。如果節名稱正好包含8個字符,就會省略null終結符。如果節名稱小於8個字符,就會使用null字符來填充數組Name。映像文件不能使用超過8個字符的節名稱。然而,在對象文件中,節名稱可以更長一些, 在這種情況下,名稱被放置在字符串表中,並且字段的第一個字節中包括了字符"/",隨後是一個ASCII字符串,包含有字符串表中相應偏移量的十進制表示。
  • VirtualSize: 4字節無符號整型的Union。在映像文件中,這個字段保存了節中的代碼或數據裝入內存的實際(未對齊的)字節大小。如果改制大於本節的SizeOfRawData, 本節由0填充. 對於object文件該節為0.
  • VirtualAddress: 4字節無符號整型。在映像文件中,為本節裝入內存後相對於image base的偏移.
  • SizeOfRawData: 4字節無符號整型。在映像文件中,這個字段保存了磁盤上需要初始化的數據的字節大小,向上捨入為FileAlignment的倍數。如果SizeOfRawData小於VirtualSize,那麼裝入內存時使用0來填充節的剩餘部分。對於不需要初始化的數據,這個字段的值為0.
  • PointerToRawData: 4字節無符號整型。保存了指向節的第一頁的文件指針。在映像文件中,向上捨入為FileAlignment的倍數。 對於不需要初始化的數據,這個字段的值為0.
  • PointerToRelocations: 4字節無符號整型。這是一個文件指針,指向了節的重定位項的起始位置。在映像文件中,不使用這個字段,應該設置為0。
  • PointerToLinenumbers: 4字節無符號整型。這個字段保存了一個文件指針,指向節的行號項的起始位置。在映像文件中, 該字段已經過時了,應設置為0
  • NumberOfRelocations: 2字節無符號整型。節的重定位項的數量. 在映像文件中,設置為0。
  • NumberOfLinenumbers: 2字節無符號整型. 節的行號條目的數量. 在映像文件中, 該字段已經過時了,應設置為0
  • Characteristics: 4字節無符號整型。這個字段指定了映像文件的特徵,並保存了這些二進制標誌的位或運算值。

常見的節:[1]頁面存檔備份,存於網際網路檔案館

  • .text: 只讀的節,包括了CLR頭、元數據、IL代碼、託管異常處理信息以及資源。
  • .sdata: 可讀寫的節,與GP相關的已初始化數據
  • .reloc: 只讀的節,基址重定位表包含了鏡像中所有需要重定位的內容。NT頭中的數據目錄中的Base Relocation Table(基址重定位表)域給出了基址重定位表所占的字節數。基址重定位表被劃分成許多塊,每一塊表示一個4K頁面範圍內的基址重定位信息,它必須從32位邊界開始。
  • .rsrc: 只讀的節,包括了非託管的資源目錄。
  • .tls: 可讀寫的節,包括了TLS數據。
  • .bss(Block Start with Symbol):未初始化全局變量
  • .textbss:未初始化的可執行代碼節。也即這個節具有可執行屬性,在PE文件中未實際占用硬盤文件空間,在加載到內存時未填充數據。這用於支持Visual Studio在Debug模式下動態編譯代碼功能,也即「Edit and Continue」功能。例如,一個函數在Visual Studio中設斷點或單步調試,這時該函數在.text節中;修改源代碼後繼續執行這個函數,Visual Studio會重新編譯這個函數並把它加載到.textbss節中的未利用地址空間(原為padding的部分),並修改對這個函數調用的跳轉(jmp)表條目以及當前EIP寄存器值。
  • .data 代碼節
  • .edata 導出表
  • .idata 導入表
  • .idlsym 包含已註冊的SEH,它們用以支持IDL屬性
  • .pdata 64位程序的異常處理器的地址表 NT頭中的Exception Table(異常表)域指向它。
  • .rdata 只讀的已初始化數據(用於常量)
  • .sbss 與GP相關的未初始化數據
  • .srdata 與GP相關的只讀數據
  • .text 默認代碼節

符號表

COFF File Header中的字段PointerToSymbolTable給出了符號表地址,字段NumberOfSymbols給出了符號表條目數量。對於映象文件,COFF調試信息是過時的,因此該字段為空。

typedef struct {
  union {
    char e_name[E_SYMNMLEN];
    struct {
      unsigned long e_zeroes;
      unsigned long e_offset;
    } e;
  } e;
  unsigned long e_value;
  short e_scnum;
  unsigned short e_type;
  unsigned char e_sclass;
  unsigned char e_numaux;
} SYMENT;

符號表包含了所有符號與元符號的條目。

  • e.e_name - 內聯的符號名字(小於等於8字節)。
  • e.e.e_zeroes - flag,用於判斷是內聯符號名字還是在字符串表中的符號名字
  • e.e.e_offset - 字符串表中的符號名字的偏移值
  • e_value - 符號的值。如表示函數的符號,值為函數的地址。還有相對於%ebp的變量地址、寄存器變量的寄存器號、結構成員相對偏移值、枚舉成員值、struct/union/enum的尺寸
  • e_scnum - 符號所屬的節的編號。節表中的節從1開始編號。節號0表示未定義(外部)符號;-1表示絕對符號(e_value是個常量而非地址);-2表示調試符號。
  • e_type - 符號類型。由基類型與派生類型組成,如「指針到整型」。
  • e_sclass - 存儲類。C_FCN,值101,".bf"或".ef" - 函數的開始/結束。C_FILE,值103,表示函數名。
  • e_numaux - 輔助條目(18個字節長)的數量(通常為0或1)。符號表中的符號允許緊隨其後有額外的輔助條目。

字符串表

字符串表保存長度大於8的符號。字符串表緊隨符號表。首先讀出4個字節,為符號表的字節長度。隨後的4個字節總為0。到符號表的引用地址總是從這4個0字節開始。示例代碼如下:

    int i;
    char *s;
    read(fd, &i, 4);
    s = (char *)malloc(i);
    memset(s, 0, 4);
    read(fd, s+4, i-4);

行號

typedef struct {
  union {
    unsigned long l_symndx;  /* function name symbol index */
    unsigned long l_paddr;   /* address of line number     */
  } l_addr;
  unsigned short l_lnno;     /* line number                */
} LINENO;

每個可執行的節有自己的行號表. 節中的每個函數獨立編號,函數的首行(有左花括號的行)編號為1. 每個函數在行號表中有一個條目, 其l_lnno為0, l_symndx為符號表中該函數. 其後是該函數每一行的條目, l_lnno甚至為該函數內的行號(1..N), l_paddr設置為該行的第一條匯編代碼的地址.

為得到絕對行號, 需要在符號表中找到該函數的"beginning of function" symbol (類型C_FCN)為該函數的絕對行號,然後加上函數內的相對行號.

參見

外部連結