1. PE文件格式简介
PE文件全称为Portable Executable,表示为“可移植的可执行文件”,专为Windows操作系统使用的可执行文件格式,我们平时常见的exe、dll、sys等都是PE格式。
具体可查看详细资料:PE文件格式
2. PE文件格式布局
PE文件格式在X86平台下称为PE32,PE文件结构=DOS头+PE头+节表+.text/.data/.rdata,布局图如下:
| MS-DOS Header |
| MS-DOS Stub(DOS残留) |
| PE Signature(PE签名或标识) |
| IMAGE_FILE_HEADER |
| IMAGE_OPTIONAL_HEADER |
| section header(节表1,节表数组的元素1) |
| section header(节表2,节表数组的元素2) |
| section header(节表3,节表数组的元素3) |
| ………… |
| .text(代码段,节表数据区1) |
| .data(数据段,节表数据区2) |
| .bss(未初始化数据段,节表数据区3) |
学习PE文件格式之前,先要了解几个重要的概念:
- 映像文件:PE文件被加载到内存中执行时的状态,它是一个程序编译和链接后的最终二进制文件,注意:这是在运行之前的状态;
- VA:虚拟地址,表示程序加载到内存后的地址,即进程的虚拟地址空间使用的内存地址,用于在运行时访问内存中的数据和代码,VA是相对于进程基址的偏移量;
- RVA:相对虚拟地址,相对于模块基址的偏移量,用于定位模块内部的代码和数据;
- FOA:文件偏移地址,映像文件在磁盘中相对于文件起始处的偏移,用于定位可执行文件中的代码和数据在文件中的位置;
总结:通常在没有使用ASLR机制的环境下,一个模块的实际加载虚拟地址就是它的映像基址。但是实际环境下由于ASLR的机制VA和映像基址往往不同。FOA对应文件,RVA对应内存中的模块基址,VA则对应进程在内存中的最终地址,这三个地址可以进行互相转换:
- RVA互相转换VA:VA=ImageBase+RVA,RVA=VA-ImageBase;
- RVA转换为FOA:(1)确定目标RVA所在的节:VA<=目标RVA<VA+VirtualSize,(2)计算FOA:FOA=(RVA-VA)+PointerToRawData;
- FOA转换为RVA:(1)确定目标FOA所在的节:PointerToRawData<=目标FOA<PointerToRawData+SizeOfRawData(2)计算RVA:RVA=(FOA-PointerToRawData+VA;
3. MS-DOS头
MS-DOS头通常被称为DOS MZ Header,是PE文件格式的起始部分,之所以保留了传统的DOS头,是为了确保文件的向后兼容性,即兼容DOS系统,PE文件都可以在DOS环境下运行。
MS-DOS头由IMAGE_DOS_HEADER和DOS Stub两部分组成。IMAGE_DOS_HEADER的结构体如下:
typedef struct _IMAGE_DOS_HEADER
{
WORD e_magic; // "MZ",用于标识是否是可执行文件
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
LONG e_lfanew; //PE头相对于文件起始处的偏移量
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;
通过结构体成员发现,DOS头中声明的寄存器都是16位的,32位和64位模式下就只有e_magic和e_lfanew两个结构体成员:
- e_magic:DOS签名,值必须为4D5A,转换为ASCII码就是MZ,确定其是不是一个PE文件;
- e_lfanew:IMAGE_NT_HEADER相对于文件起始处的偏移,用于找到PE头;
IMAGE_DOS_HEADER后面紧跟的就是DOS Stub,它是DOS残留的一段程序,主要作用是:当系统环境为MS-DOS时,输出“This program cannotbe run in DOS mode”,并退出程序,说明该程序不能在DOS环境下运行,相当于两个环境:16位DOS代码在DOS系统运行,32位/64位程序在WIndows环境下运行。
打开010 Editor程序随机打开一个exe文件就可以看到PE文件的结构信息:

4. PE头
PE文件头(PE File Header)是PE文件的核心标识部分,也被称为NT头,由PE签名、标准PE头和可选PE头三部分组成,NT头跟在DOS Stub的后面,在_IMAGE_DOS_HEADER结构的e_lfanew元素(0x3C位置处),存了PE头的偏移量。

PE头的结构体是IMAGE_NT_HEADERS,定义如下:
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //0x00:PE标识
IMAGE_FILE_HEADER FileHeader; //0x04:PE标准头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //0x18:PE可选头
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
4.1 PE Signature
首先第一个成员是Signature,它是一个4字节的值为“PE\0\0”的签名,用于标识PE文件,位置通常是0x3C。
4.2 IMAGE_FILE_HEADER
Signature后面是FileHeader,也就是标准头,它是一个IMAGE_FILE_HEADER结构体,也被称为“COFF文件标头”:
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; //0x04:目标机器类型
WORD NumberOfSections; //0x06:PE中节的数量
DWORD TimeDateStamp; //0x08:时间戳,连接器产生此文件的时间差
DWORD PointerToSymbolTable; //0x0C:指向符号表的指针,COFF 符号表的文件偏移量
DWORD NumberOfSymbols; //0x10:符号表中符号数量
WORD SizeOfOptionalHeader; //0x12:可选头的大小,也就是IMAGE_OPTIONAL_HEADER的大小
WORD Characteristics; //0x14:文件属性标志
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;
这里为什么使用0x前缀表示十六进制,而不是使用h后缀呢?个人习惯吧,一般情况下地址使用0x前缀表示十六进制,数据(值)则使用h后缀表示十六进制;具体的成员的功能在上面的注释中已经说明了。
SizeOfOptionalHeader这个成员,定义了IMAGE_OPTIONAL_HEADER的大小,32位系统下的值默认是E0h,而64位系统下默认则是F0h,是可以进行修改的。
Characteristics成员是按位来看的,成员值是多个属性的组合,每一位的文件属性标志定义:
// 文件属性标志
#define IMAGE_FILE_RELOCS_STRIPPED 0x0001 //文件不包含重定位信息,只能在原定的基址加载。如果原定基址不可用,加载器会报出错误
#define IMAGE_FILE_EXECUTABLE_IMAGE 0x0002 //文件可执行,如果该位未设置,意味着存在链接器错误
#define IMAGE_FILE_LINE_NUMS_STRIPPED 0x0004 //不存在行信息
#define IMAGE_FILE_LOCAL_SYMS_STRIPPED 0x0008 //不存在符号信息
#define IMAGE_FILE_AGGRESSIVE_WS_TRIM 0x0010 //已废弃,主动调整工作区
#define IMAGE_FILE_LARGE_ADDRESS_AWARE 0x0020 //高地址警告,应用可处理大于 2GB 的地址
#define IMAGE_FILE_BYTES_REVERSED_LO 0x0080 //小尾存储(已废弃)
#define IMAGE_FILE_32BIT_MACHINE 0x0100 //基于32位体系结构
#define IMAGE_FILE_DEBUG_STRIPPED 0x0200 //不存在调试信息
#define IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP 0x0400 //如果映像文件在可移动介质上,完全加载并复制到内存交换文件中
#define IMAGE_FILE_NET_RUN_FROM_SWAP 0x0800 //如果映像文件在网络介质上,完全加载并复制到内存交换文件中
#define IMAGE_FILE_SYSTEM 0x1000 //映像文件是系统文件
#define IMAGE_FILE_DLL 0x2000 //映像文件是DLL文件
#define IMAGE_FILE_UP_SYSTEM_ONLY 0x4000 //文件只能在单处理器机器上运行
#define IMAGE_FILE_BYTES_REVERSED_HI 0x8000 //大尾存储(已废弃)
4.3 IMAGE_OPTIONAL_HEADER
PE标准头后面就是可选头,它提供了必要的程序运行时的加载信息,IMAGE_OPTIONAL_HEADER结构体定义如下:
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; //0x18:魔数
BYTE MajorLinkerVersion; //0x1A:链接器主版本号
BYTE MinorLinkerVersion; //0x1B:链接器次版本号
DWORD SizeOfCode; //0x1C:所有代码节的总大小
DWORD SizeOfInitializedData; //0x20:所有已初始化数据节的总大小
DWORD SizeOfUninitializedData; //0x24:所有未初始化数据节的总大小
DWORD AddressOfEntryPoint; //0x28:程序入口地址OEP,就是一个RVA
DWORD BaseOfCode; //0x2C:代码节起始地址RVA
DWORD BaseOfData; //0x30:数据节起始地址RVA
DWORD ImageBase; //0x34:映像文件默认装入的起始地址(4000H)
DWORD SectionAlignment; //0x38:内存中节对齐粒度
DWORD FileAlignment; //0x3C:文件中节对齐粒度(1000H)
WORD MajorOperatingSystemVersion; //0x40:操作系统主版本号
WORD MinorOperatingSystemVersion; //0x42:操作系统次版本号
WORD MajorImageVersion; //0x44:映像文件主版本号
WORD MinorImageVersion; //0x46:映像文件次版本号
WORD MajorSubsystemVersion; //0x48:子系统主版本号
WORD MinorSubsystemVersion; //0x4A:子系统次版本号
DWORD Win32VersionValue; //0x4C:保留。总是为0
DWORD SizeOfImage; //0x50:PE文件在内存中文件的总大小
DWORD SizeOfHeaders; //0x54:所有头+节表的总大小,按照文件对齐
DWORD CheckSum; //0x58:映像文件CRC校验和,判断文件是否被修改
WORD Subsystem; //0x5c:用户运行映像的子系统类型
WORD DllCharacteristics; //0x5e:映像文件的DLL属性,总是0
DWORD SizeOfStackReserve; //0x60:初始化时保留的栈大小
DWORD SizeOfStackCommit; //0x64:初始化时实际提交的线程栈大小
DWORD SizeOfHeapReserve; //0x68:初始化时保留的堆大小
DWORD SizeOfHeapCommit; //0x6c:初始化时实际提交的堆大小
DWORD LoaderFlags; //0x70:已废弃
DWORD NumberOfRvaAndSizes; //0x74:数据目录项的总数
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; //0x78:指向数据目录中第一个IMAGE_DATA_DIRECTORY结构体指针
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;
具体看一下每个成员的作用,注释中说明的这里不再提及:
- 第1项:Magic:表明映像文件的类型,107H表示ROM映像,10BH表示PE32,20BH表示PE32+,即64位的PE文件;
- 第4项:SizeOfCode:表示代码节的总大小,指的是文件对齐后的总大小;
- 第7项:AddressOfEntryPoint:表示程序的入口地址,相对于映像文件基址的偏移量,对于exe文件这就是程序的开始地址,对于DLL文件是可选的,对于驱动文件是初始化函数的地址;
- 第8项:BaseOfCode:表示代码节的RVA,也就是代码段的基址,相对于映像文件加载基址的偏移量,代码节通常跟在PE头后面,以“.text”表示代码节;
- 第9项:BaseOfData:表示数据节的RVA,也就是数据段的基址,相对于映像文件加载基址的偏移量,数据节通常在文件末尾,以“.data”表示数据节;
- 第10项:ImageBase: 映像文件默认装入的起始地址,必须是64KB的整数倍,exe文件默认是400000H,DLL文件默认是10000000H;
- 第11项:SectionAlignment:内存中的节对齐粒度,默认与页大小相等,即4096字节;
- 第12项:FileAlignment:映像文件中数据对齐粒度,默认为200H,即512个字节,如果要设置值,范围必须为512-64K范围内的2的幂,如果SectionAlignment成员的值小于系统页大小,则FileAlignment与SectionAlignment两者成员的值必须相同;
- 第20项:SizeOfImage:PE(映像)文件在内存中的总大小,必须是SectionAlignment的整数倍;
- 第21项:SizeOfHeaders:DOS头+PE签名+PE标准头+PE可选头+节表按照FileAlignment对齐后的大小;
- 第22项:CheckSum:映像文件的CRC检验值,校验装载时的所有驱动、DLL等;
- 第25项:SizeOfHeapReserve:初始化时保留的栈虚拟内存大小,默认为100000H,即1MB;
- 第26项:SizeOfStackCommit:初始化时实际提交的栈虚拟内存大小;
- 第27项:SizeOfHeapReserve:初始化时保留的堆虚拟内存大小,默认为100000H,即1MB;
- 第28项:SizeOfHeapCommit:初始化时实际提交的栈虚拟内存大小,默认为页大小,即4KB;
- 第30项:NumberOfRvaAndSizes:数据目录项总数量,默认为10H,即16个;
- 第31项:DataDirectory:IMAGE_DATA_DIRECTORY结构体数组;
IMAGE_DATA_DIRECTORY结构体定义如下:
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据目录的 RVA
DWORD Size; //数据目录的大小
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;
数组各项的定义如下:
DataDirectory[0] = EXPORT Directory //导入表RVA和大小
DataDirectory[1] = IMPORT Directory //导入表RVA和大小
DataDirectory[2] = RESOURCE Directory //资源表RVA和大小
DataDirectory[3] = EXCEPTION Directory //异常表RVA和大小
DataDirectory[4] = CERTIFICATE Directory //证书表FOA和大小
DataDirectory[5] = BASE RELOCATION Directory //基址重定位表RVA和大小
DataDirectory[6] = DEBUG Directory //调试信息RVA和大小
DataDirectory[7] = ARCH DATA Directory //指定架构信息RVA和大小
DataDirectory[8] = GLOBALPTR Directory //全局指针寄存器RVA
DataDirectory[9] = TLS Directory //线程私有存储表RVA和大小
DataDirectory[10] = LOAD CONFIG Directory //加载配置表RVA和大小
DataDirectory[11] = BOUND IMPORT Directory //绑定导入表RVA和大小
DataDirectory[12] = `IAT` Directory //导入地址表RVA和大小
DataDirectory[13] = DELAY IMPORT Directory //延迟导入描述符RVA和大小
DataDirectory[14] = CLR Directory //CLR数据RVA和大小
DataDirectory[15] = Reserverd //保留
5. 节表(Section Header)
PE可选头后面就是节头信息,也称为“节表”。节表中的每个成员都是IMAGE_SECTION_HEADER结构体,也就是说节表是一个结构体数组类型,记录了各个节的属性信息,大小均为40个字节。
IMAGE_SECTION_HEADER结构体定义如下:
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; //0x00:节区名,比如.text、.data、.rdata等,长度最多8个字节
union {
DWORD PhysicalAddress; //0x08:物理地址
DWORD VirtualSize; //虚拟内存中节区大小
} Misc;//存储的是当前节在没有对齐前的真实大小
DWORD VirtualAddress; //0x0c:虚拟内存中节区RVA(相对虚拟地址)
DWORD SizeOfRawData; //0x10:当前节在文件中对齐后的大小,值必须为 FileAlignment的整数倍
DWORD PointerToRawData; //0x14:当前节在文件中的偏移量FOA
DWORD PointerToRelocations; //0x18:指向重定位表的指针,重定位偏移(用于obj文件)
DWORD PointerToLinenumbers; //0x1c:指向行号表的指针,行号表偏移(调试用)
WORD NumberOfRelocations; //0x20:重定位项目数量(用于obj文件)
WORD NumberOfLinenumbers; //0x22:行号数量
DWORD Characteristics; //0x24:节区属性(按位设置)
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;
Characteristics节区属性按位设置,有以下标志位:
// 节区属性
#define IMAGE_SCN_CNT_CODE 0x00000020 //节区包含代码
#define IMAGE_SCN_CNT_INITIALIZED_DATA 0x00000040 //节区包含已初始化数据
#define IMAGE_SCN_CNT_UNINITIALIZED_DATA 0x00000080 //节区包含未初始化数据
#define IMAGE_SCN_ALIGN_1BYTES 0x00100000 //1字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_2BYTES 0x00200000 //2字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_4BYTES 0x00300000 //4字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_8BYTES 0x00400000 //8字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_16BYTES 0x00500000 //16字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_32BYTES 0x00600000 //32字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_64BYTES 0x00700000 //64字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_128BYTES 0x00800000 //128字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_256BYTES 0x00900000 //256字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_512BYTES 0x00A00000 //512字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_1024BYTES 0x00B00000 //1024字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_2048BYTES 0x00C00000 //2048字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_4096BYTES 0x00D00000 //4096字节对齐,仅用于目标文件
#define IMAGE_SCN_ALIGN_8192BYTES 0x00E00000 //8192字节对齐,仅用于目标文件
#define IMAGE_SCN_LNK_NRELOC_OVFL 0x01000000 //节区包含扩展的重定位项
#define IMAGE_SCN_MEM_DISCARDABLE 0x02000000 //节区可根据需要丢弃,比如.reloc在进程开始后被丢弃
#define IMAGE_SCN_MEM_NOT_CACHED 0x04000000 //节区不会被缓存
#define IMAGE_SCN_MEM_NOT_PAGED 0x08000000 //节区不可分页
#define IMAGE_SCN_MEM_SHARED 0x10000000 //节区可共享给不同进程
#define IMAGE_SCN_MEM_EXECUTE 0x20000000 //节区可作为代码可执行
#define IMAGE_SCN_MEM_READ 0x40000000 //节区可读
#define IMAGE_SCN_MEM_WRITE 0x80000000 //节区可写
6. 节区Sections
节表后面就是节了,就是节表中存储的每个节,PE文件格式要求至少有两个节区:.text节和.data节,当然实际环境下肯定会更多,以下是常见的节区:
- .text节:默认的代码节区,用于保存可执行代码;
- .data节:默认的读/写数据节区,用于保存已初始化的变量等信息;
- .rdata节:默认的只读数据节区;
- .bss节:用于保存未初始化数据节区;
- .idata节:用于保存导入表信息;
- .edata节:用于保存导出表信息;
- .reloc节:重定位表节,用于保存重定位表信息;
7. 导出表结构
导出表是Windows系统可执行文件的一个重要数据结构,记录了当前模块自身提供的函数和数据(名称和地址),以供其他程序调用。区别于导入表,导入表是记录了当前模块所依赖的外部函数和数据。
导出表的结构是一个IMAGE_EXPORT_DIRECTORY,定义如下:
typedef struct _IMAGE_EXPORT_DIRECTORY
{
DWORD Characteristics; //0x00,保留,恒为0x00000000
DWORD TimeDateStamp; //0x04,文件的产生时间戳
WORD MajorVersion; //0x08,主版本号
WORD MinorVersion; //0xA,次版本号
DWORD Name; //0xC:指向文件名的RVA
DWORD Base; //0x10:导出函数的起始序号
DWORD NumberOfFunctions; //0x14:导出函数总数
DWORD NumberOfNames; //0x18:以名称导出函数的总数
DWORD AddressOfFunctions; //0x1C:导出函数地址表的RVA
DWORD AddressOfNames; //0x20:函数名称地址表的RVA
DWORD AddressOfNameOrdinals; //0x24:函数名序号表的RVA
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
关键成员解析:
- AddressOfFunctions:导出函数地址表的RVA,这是一个RVA数组,每个元素指向一个导出函数的实际代码位置;
- AddressOfNames:函数名称地址表的RVA,这也是一个RVA数组,每个元素指向一个导出函数名称的字符串;
- AddressOfNameOrdinals:函数名序号表的RVA,这是一个WORD数组,用于将名称索引映射到地址索引。
8. PE32+结构
64位PE文件格式称为PE+或PE32+,它是PE32的扩展,并没有本质上的区别,而是PE规范的两个版本。PE32+可以看做是对PE32的扩展,使其能够处理64位架构所需的更大地址和内存管理需求。
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。