可移植可执行
可移植性可执行文件(英语: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-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
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字节无符号整型。这个字段指定了映像文件的特征,并保存了这些二进制标志的位或运算值。
常见的节: (页面存档备份,存于)
- .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)为该函数的绝对行号,然后加上函数内的相对行号.
外部链接
- 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.is的存檔,存档日期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 (页面存档备份,存于)