# 可移植可执行

可移植性可执行文件| 扩展名 | .acm, .ax, .cpl, .dll, .drv, .efi, .exe, .mui, .ocx, .scr, .sys, .tsp |
| 互联网媒体类型 |
application/vnd.microsoft.portable-executable |
| 开发者 | Microsoft |
| 格式类型 | 二进制可执行文件、目标代码、函式庫 |
| 扩展自 | DOS MZ可执行文件 COFF |

**可移植性可执行文件**（英语：Portable Executable，缩写为PE）是一种用于可执行文件、目标文件和动态链接库的文件格式，主要使用在32位和64位的Windows操作系统上。“可移植的”是指该文件格式的通用性，可用于许多种不同的操作系统和体系结构中。PE文件格式封装了Windows操作系统加载可执行程序代码时所必需的一些信息。这些信息包括动态链接库、API导入和导出表、资源管理数据和线程局部存储数据。在Windows NT操作系统中，PE文件格式主要用于EXE文件、DLL文件、.sys（驱动程序）和其他文件类型。可扩展固件接口（EFI）技术规范书中说明PE格式是EFI环境中的标准可执行文件格式。开头为DOS头部。

PE格式是由Unix中的COFF格式修改而来的。在Windows开发环境中，PE格式也称为**PE/COFF**格式。

在Windows NT操作系统中，PE格式目前支持IA-32、IA-64和x86-64（AMD64/Intel64）的指令系统。在Windows 2000之前，Windows NT还支持MIPS、Alpha和PowerPC的指令系统。由于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)为该函数的绝对行号,然后加上函数内的相对行号.

* PE infection
* EXE格式
* 可執行與可鏈接格式
* Mach-O
* A.out
* 可执行文件格式比较
* 可执行文件压缩
* ar (Unix)
* 应用程序虚拟化

* Microsoft Portable Executable and Common Object File Format Specification（页面存档备份，存于互联网档案馆） (latest edition, OOXML format)
* Microsoft Portable Executable and Common Object File Format Specification (1999 edition, .doc format)
* The original Portable Executable article （页面存档备份，存于互联网档案馆） by Matt Pietrek (MSDN Magazine, March 1994)
* Part I. An In-Depth Look into the Win32 Portable Executable File Format（页面存档备份，存于互联网档案馆） by Matt Pietrek (MSDN Magazine, February 2002)
* Part II. An In-Depth Look into the Win32 Portable Executable File Format by Matt Pietrek (MSDN Magazine, March 2002)
* The .NET File Format by Daniel PistelliArchive.today的存檔，存档日期2013-01-30
* Ero Carrera's blog describing the PE header and how to walk through（页面存档备份，存于互联网档案馆）
* PE Internals provides an easy way to learn the Portable Executable File Format （页面存档备份，存于互联网档案馆）