可移植可执行

(重定向自PE文件

可移植性可执行文件英语: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)为该函数的绝对行号,然后加上函数内的相对行号.

参见

外部链接