PE格式解析
PE格式解析
注意点
本文主要参考:PE可执行文件格式详解 - 知乎
可以直接看winnt.h
,里面有PE的定义.这里以32位为主,因为这个知乎文章就是以32位为主,因为这位博主都有翻译就不直接复制winnt.h
的结构体了.所以和实际的结构体是有区别的,需要区分.
实际上更建议看这位博主文章,非常好,非常细,我则就是他文章的简化方便我自己理解罢了.
不过我自己用010editor查看了实际的二进制文件,所以也加了些不影响理解的小东西.
这是源码,建议用Mingw/gcc生成,微软的会去符号:
void foo(){}
void fuck(){}
int a = 0xaaaa;
int main() {
int b = 0xbbbb;
foo();
return 0;
}
事前须知
地址
-
虚拟内存地址(Virtual Address, VA)PE文件中的指令被装入内存后的地址。
-
相对虚拟内存地址(Reverse Virtual Address, RVA相对虚拟地址是内存地址相对于映射基址的偏移量。
-
文件偏移地址(File Offset Address, FOA)数据在PE文件中的地址叫文件偏移地址,这是文件在磁盘上存放时相对于文件开头的偏移。
-
装载基址(Image base)PE装入内存时的基地址。默认情况下,EXE文件在内存中的基地址时0x00400000, DLL文件是0x10000000。这些位置可以通过修改编译选项更改。如果已经被占用,则操作系统会选择一个新的地址,无论选没选择,这个转入后的地址都是VA.
VA = Image base + RVA
多余的概念特别是链接相关的可以看程序员的自我修养.
PE总结构
ai生成的
PE 文件结构
├─ DOS 头 (IMAGE_DOS_HEADER)
│ ├─ e_magic: DOS 标识(固定为 0x5A4D,即 "MZ")
│ ├─ e_lfanew: 指向 NT 头的文件偏移量(核心字段,用于定位 PE 真正结构)
│ └─ 其他 DOS 兼容字段(如 e_cblp、e_cp 等,现代系统加载时基本忽略)
│
├─ NT 头 (IMAGE_NT_HEADERS)
│ ├─ Signature: PE 标识(固定为 0x00004550,即 "PE00",32/64 位通用)
│ │
│ ├─ 标准头 (IMAGE_FILE_HEADER)
│ │ ├─ Machine: 目标机器架构(如 0x014C=x86、0x8664=x64)
│ │ ├─ NumberOfSections: 节的数量
│ │ ├─ TimeDateStamp: 文件创建时间戳
│ │ ├─ PointerToSymbolTable: 指向符号表的偏移(调试用,通常为 0)
│ │ ├─ NumberOfSymbols: 符号表中的符号数量(调试用,通常为 0)
│ │ ├─ SizeOfOptionalHeader: 可选头的大小
│ │ └─ Characteristics: 文件属性(如 0x2000=DLL、0x0002=可执行文件)
│ │
│ └─ 可选头 (IMAGE_OPTIONAL_HEADER)
│ ├─ 基础信息(32/64 位通用)
│ │ ├─ Magic: 格式标识(0x10B=PE32、0x20B=PE32+)
│ │ ├─ MajorLinkerVersion/MinorLinkerVersion: 链接器版本
│ │ ├─ SizeOfCode: 代码节总大小
│ │ ├─ SizeOfInitializedData: 已初始化数据节总大小
│ │ ├─ SizeOfUninitializedData: 未初始化数据节总大小
│ │ ├─ AddressOfEntryPoint: 程序执行入口 RVA
│ │ ├─ BaseOfCode: 代码节起始 RVA(PE32+ 无此字段)
│ │ └─ BaseOfData: 数据节起始 RVA(PE32+ 无此字段)
│ │
│ ├─ 内存布局信息
│ │ ├─ ImageBase: 建议加载地址(PE32=DWORD、PE32+=QWORD)
│ │ ├─ SectionAlignment: 内存中节的对齐粒度
│ │ ├─ FileAlignment: 磁盘中节的对齐粒度
│ │ ├─ SizeOfImage: 内存中 PE 镜像总大小
│ │ └─ SizeOfHeaders: 所有头(含节表)的总大小
│ │
│ ├─ 版本与兼容性
│ │ ├─ MajorOperatingSystemVersion/MinorOperatingSystemVersion: 最低系统版本
│ │ ├─ MajorImageVersion/MinorImageVersion: 程序自身版本
│ │ ├─ MajorSubsystemVersion/MinorSubsystemVersion: 所需子系统版本
│ │ └─ Win32VersionValue: 保留字段(通常为 0)
│ │
│ ├─ 运行特性
│ │ ├─ CheckSum: 文件校验和(防篡改)
│ │ ├─ Subsystem: 运行子系统(0x02=GUI、0x03=CUI)
│ │ ├─ DllCharacteristics: DLL 特性(如 ASLR 兼容、SEH 支持)
│ │ └─ LoaderFlags: 调试相关保留字段(通常为 0)
│ │
│ ├─ 栈与堆配置
│ │ ├─ SizeOfStackReserve/SizeOfStackCommit: 线程栈保留/提交大小
│ │ └─ SizeOfHeapReserve/SizeOfHeapCommit: 默认堆保留/提交大小
│ │
│ └─ 数据目录(核心扩展信息)
│ ├─ NumberOfRvaAndSizes: 数据目录条目数(固定为 16)
│ └─ DataDirectory[16]: 16 个数据块描述(每项含 VirtualAddress 和 Size)
│ ├─ 第 0 项:导出表(Export Table)
│ ├─ 第 1 项:导入表(Import Table)
│ ├─ 第 2 项:资源表(Resource Table)
│ ├─ 第 3 项:异常表(Exception Table)
│ └─ 其他:安全表、重定位表、调试表等(共 16 项)
│
├─ 节表 (IMAGE_SECTION_HEADER 数组)
│ ├─ 数量 = IMAGE_FILE_HEADER.NumberOfSections
│ └─ 每个节表项包含:
│ ├─ Name: 节名称(如 ".text"、".data",8 字节)
│ ├─ VirtualSize: 节在内存中的实际大小
│ ├─ VirtualAddress: 节的起始 RVA
│ ├─ SizeOfRawData: 节在磁盘上的大小(按 FileAlignment 对齐)
│ ├─ PointerToRawData: 节在磁盘上的起始偏移
│ ├─ PointerToRelocations: 重定位信息偏移(通常为 0)
│ ├─ PointerToLinenumbers: 行号信息偏移(调试用,通常为 0)
│ ├─ NumberOfRelocations: 重定位项数量(通常为 0)
│ ├─ NumberOfLinenumbers: 行号项数量(通常为 0)
│ └─ Characteristics: 节属性(如 0x60000020=代码节、0xC0000040=数据节)
│
└─ 节数据(实际代码/数据存储区)
├─ 数量 = 节表中的节数量
└─ 常见节类型:
├─ .text: 可执行代码(对应代码节)
├─ .data: 已初始化数据(全局变量、静态变量)
├─ .bss: 未初始化数据(磁盘上不占空间,内存中分配)
├─ .rdata: 只读数据(如字符串常量、常量数据)
├─ .rsrc: 资源数据(图标、对话框、字符串等)
└─ .reloc: 重定位信息(ImageBase 被占用时修正地址)
DOS头(可以统称DOS Stub)
IMAGE_DOS_HEADER {
WORD e_magic; // +0000h - EXE标志,“MZ”
WORD e_cblp; // +0002h - 最后(部分)页中的字节数
WORD e_cp; // +0004h - 文件中的全部和部分页数
WORD e_crlc; // +0006h - 重定位表中的指针数
WORD e_cparhdr; // +0008h - 头部尺寸,以段落为单位
WORD e_minalloc; // +000ah - 所需的最小附加段
WORD e_maxalloc; // +000ch - 所需的最大附加段
WORD e_ss; // +000eh - 初始的SS值(相对偏移量)
WORD e_sp; // +0010h - 初始的SP值
WORD e_csum; // +0012h - 补码校验值
WORD e_ip; // +0014h - 初始的IP值
WORD e_cs; // +0016h - 初始的CS值
WORD e_lfarlc; // +0018h - 重定位表的字节偏移量
WORD e_ovno; // +001ah - 覆盖号
WORD e_res[4]; // +001ch - 保留字00
WORD e_oemid; // +0024h - OEM标识符
WORD e_oeminfo; // +0026h - OEM信息
WORD e_res2[10]; // +0028h - 保留字
LONG e_lfanew; // +003ch - PE头相对于文件的偏移地址
}
其中有用的就是e_magic
和e_lfanew
.
DOS头下面是DOS stub,存放data
(用来在dos环境运行时会输出的话,可以用来整活),如果用微软工具链生成里面会用个加密数据,存放相应版本号.
PE头IMAGE_NT_HEADERS
IMAGE_NT_HEADERS {
DWORD Signature; // +0000h - PE文件标识,“PE\0\0”
IMAGE_FILE_HEADER FileHeader; // +0004h - PE标准头
IMAGE_OPTIONAL_HEADER32 OptionalHeader; // +0018h - PE扩展头,这里32位和64位不一样
}
广义的头,含有标准头和扩展头(或者叫可选头)
标准PE头IMAGE_FILE_HEADER
IMAGE_FILE_HEADER {
WORD Machine; // +0004h - 运行平台
WORD NumberOfSections; // +0006h - PE中节的数量
DWORD TimeDateStamp; // +0008h - 文件创建日期和时间
DWORD PointerToSymbolTable; // +000ch - 指向符号表
DWORD NumberOfSymbols; // +0010h - 符号表中的符号数量
WORD SizeOfOptionalHeader; // +0014h - 扩展头结构的长度
WORD Characteristics; // +0016h - 文件属性,用来区分dll和pe,还有七七八八
}
扩展PE头IMAGE_OPTIONAL_HEADER
IMAGE_OPTIONAL_HEADER {
WORD Magic; // +0018h - 魔术字107h = ROM Image,10bh = PE32, 20bh = PE32+(就是64位)
BYTE MajorLinkerVersion; // +001ah - 链接器主版本号
BYTE MinorLinkerVersion; // +001bh - 链接器次版本号
DWORD SizeOfCode; // +001ch - 所有含代码的节的总大小
DWORD SizeOfInitializedData; // +0020h - 所有含已初始化数据的节的总大小(如.data,.rodata)
DWORD SizeOfUninitializedData; // +0024h - 所有含未初始化数据的节的大小(如.bss)
DWORD AddressOfEntryPoint; // +0028h - 程序执行入口RVA
DWORD BaseOfCode; // +002ch - 代码的节的起始RVA
DWORD BaseOfData; // +0030h - 数据的节的起始RVA
DWORD ImageBase; // +0034h - 程序的建议装载地址
DWORD SectionAlignment; // +0038h - 内存中的节的对齐粒度
DWORD FileAlignment; // +003ch - 文件中的节的对齐粒度
WORD MajorOperatingSystemVersion; // +0040h - 操作系统版本号
WORD MinorOperatingSystemVersion; // +0042h -
WORD MajorImageVersion; // +0044h - 该PE的版本号
WORD MinorImageVersion; // +0046h -
WORD MajorSubsystemVersion; // +0048h - 所需子系统的版本号
WORD MinorSubsystemVersion; // +004ah -
DWORD Win32VersionValue; // +004ch - 未用
DWORD SizeOfImage; // +0050h - 内存中的整个PE映象尺寸
DWORD SizeOfHeaders; // +0054h - 所有头+节表的大小
DWORD CheckSum; // +0058h - 校验和,不符合加载器会拒绝加载
WORD Subsystem; // +005ch - 文件的子系统
WORD DllCharacteristics; // +005eh - DLL文件特性
DWORD SizeOfStackReserve; // +0060h - 初始化时的栈大小
DWORD SizeOfStackCommit; // +0064h - 初始化时实际提交的栈大小
DWORD SizeOfHeapReserve; // +0068h - 初始化时保留的堆大小
DWORD SizeOfHeapCommit; // +006ch - 初始化时实际提交的堆大小
DWORD LoaderFlags; // +0070h - 与调试有关
DWORD NumberOfRvaAndSizes; // +0074h - 下面的数据目录结构的项目数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; // 0078h - 数据目录,IMAGE_NUMBEROF_DIRECTORY_ENTRIES为16
}
数据目录项IMAGE_DATA_DIRECTORY
数据目录数组,包含 16 个 IMAGE_DATA_DIRECTORY
结构,每个结构描述一个关键数据块(如导入表、导出表、资源表等)。
IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // +0000h - 数据的起始RVA
DWORD Size; // +0004h - 数据块的长度
}
16个分别为:
- 导出表地址和大小
- 导入表地址和大小
- 资源表地址和大小
- 异常表地址和大小
- 属性证书数据地址和大小
- 基地址重定位表地址和大小
- 调试信息地址和大小
- 预留,数据都为0
- 指向全局指针寄存器的值
- 线程局部存储地址和大小
- 加载配置表地址和大小
- 绑定导入表地址和大小
- 导入函数地址表地址和大小
- 延迟导入表地址和大小
- CLR运行时头部数据地址和大小
- 系统保留
节表头IMAGE_SECTION_HEADER
IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // +0000h - 8个字节节名,如.text,.data这些
union {
DWORD PhysicalAddress; //物理地址(仅在某些系统中使用,现代 PE 文件中很少用到)
DWORD VirtualSize; //节在内存中的实际大小(未按内存对齐粒度扩展前的真实大小)
} Misc; // +0008h - 节区的尺寸
DWORD VirtualAddress; // +000ch - 节区的RVA地址
DWORD SizeOfRawData; // +0010h - 节在磁盘文件中的大小(按 FileAlignment 对齐后的尺寸)
DWORD PointerToRawData; // +0014h - 节在磁盘文件中的起始偏移量(从文件开头计算)
DWORD PointerToRelocations; // +0018h - 在OBJ文件中使用,指向节的重定位信息在文件中的偏移量
DWORD PointerToLinenumbers; // +001ch - 行号表的位置(供调试用)
WORD NumberOfRelocations; // +0020h - 在OBJ文件中使用,节的重定位项数量
WORD NumberOfLinenumbers; // +0022h - 行号表中行号的数量
DWORD Characteristics; // +0024h - 节的属性,可读可写可执行,可丢弃
}
节(或者说块,和上面的数据目录项有重合):
- .text
- .data
- .rdata
- idata:导入表
- edata:导出表
- rsrc:资源,比如图标菜单等
- .bss:现在少用了,往往是data段扩大到可以存放未初始化变量
- .crt
- .tls
- .reloc:重定位表
- .sdata
- .srdata
- .pdata:异常表
- .debug:obj用的
- .drectve:obj用的
- .didat:非Release下能找到
在编译的时候可以用编译器指令修改节的情况,比如用#pragma
指令
各类表
重定位表
[数据目录表项] → 指向重定位表起始地址
↓
[重定位块1]
VirtualAddress: 0x1000(对应页:默认基址 + 0x1000)
SizeOfBlock: 0x10(包含 (0x10-8)/2 = 4 个项)
[重定位项1]:0x3200 → 类型0x3,偏移0x200 → 需调整地址:默认基址+0x1000+0x200
[重定位项2]:0x3300 → 类型0x3,偏移0x300 → 需调整地址:默认基址+0x1000+0x300
[重定位项3]:0x3400 → ...
[重定位项4]:0x0000 → 无效项(块结束)
[重定位块2]
VirtualAddress: 0x2000(对应页:默认基址 + 0x2000)
SizeOfBlock: 0x08(包含0个有效项,仅结构本身)
...
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 该块对应的内存页的起始虚拟地址(基于默认基址)
DWORD SizeOfBlock; // 整个块的大小(包括本结构和后面的重定位项)
// 后面紧跟着多个重定位项(IMAGE_RELOCATION),每个都是这个结构.
} IMAGE_BASE_RELOCATION;
导出表
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; //一般为0,没啥用
DWORD TimeDateStamp; //导出表生成的时间
WORD MajorVersion; //版本,也是0没啥用
WORD MinorVersion; //也是没啥用的版本信息一般为0
DWORD Name; //当前导出表的模块名字
DWORD Base; //序号表中序号的基数
DWORD NumberOfFunctions; //导出函数数量
DWORD NumberOfNames; //按名字导出函数的数量
DWORD AddressOfFunctions; // 地址表,指向函数RVA
DWORD AddressOfNames; // 名称表,指向函数名称
DWORD AddressOfNameOrdinals; // 序号表,指向AddressOfNameOrdinals的序号
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;
根据程序的自我修养中的说明,以前是采取序号来找RVA的,用来节省空间.后续发现如果dll删除了一个函数或者加一个函数,序号就要变,就不如直接找符号.
但是为了向后兼容,序号表得到保留,事实上,名称表是可选的.
导入表
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 为0
DWORD OriginalFirstThunk; // 指向 导入名称表指针 INT,实际上就是未动态链接前的IAT
} DUMMYUNIONNAME;
DWORD TimeDateStamp; // 时间日期戳,与绑定技术有关,现在不常用,无需了解
DWORD ForwarderChain; // -1 if no forwarders
DWORD Name; // dll的名字
DWORD FirstThunk; // 指向 导入地址数组(Import Address Table IAT),链接后存放外部函数地址
} IMAGE_IMPORT_DESCRIPTOR;
typedef IMAGE_IMPORT_DESCRIPTOR UNALIGNED *PIMAGE_IMPORT_DESCRIPTOR;
也有输入绑定的技术,就是在确保dll加载在相应基址上,在确保dll导出表符号位置不变的情况下,不过这个不重要,了解一下即可
资源表
它采取类似磁盘目录结构的方式保存,通常有3层.
typedef struct _IMAGE_RESOURCE_DIRECTORY {
DWORD Characteristics; //通常为0
DWORD TimeDateStamp; //资源创建时间
WORD MajorVersion; //通常为0
WORD MinorVersion;
WORD NumberOfNamedEntries;//使用名字的资源条目个数
WORD NumberOfIdEntries;//使用ID数字资源条目的个数
// IMAGE_RESOURCE_DIRECTORY_ENTRY DirectoryEntries[];
} IMAGE_RESOURCE_DIRECTORY, *PIMAGE_RESOURCE_DIRECTORY;
//下一层
typedef struct _IMAGE_RESOURCE_DIRECTORY_ENTRY {
union {
struct {
DWORD NameOffset:31;
DWORD NameIsString:1;
} DUMMYSTRUCTNAME;
DWORD Name;
WORD Id;
} DUMMYUNIONNAME;
//如果最高位为 0,则剩下的31位表示一个整数ID(例如,图标的ID)。
//如果最高位为 1,则剩下的31位是一个偏移量,指向一个IMAGE_RESOURCE_DIR_STRING_U结构,该结构包含资源的Unicode名称。
//第一层定义资源类型,第二层定义资源名称,第三层定义代码页编号
union {
DWORD OffsetToData; //资源数据偏移地址或者子目录偏移地址
struct {
DWORD OffsetToDirectory:31;
DWORD DataIsDirectory:1;
} DUMMYSTRUCTNAME2;
} DUMMYUNIONNAME2;
//如果最高位为 1,则剩下的31位是一个偏移量,指向下一级的IMAGE_RESOURCE_DIRECTORY(即子目录)。
//如果最高位为 0,则剩下的31位是一个偏移量,指向一个IMAGE_RESOURCE_DATA_ENTRY结构(即最终的资源数据信息)。
//这是从资源区块开始计算偏移量,不是根目录.
} IMAGE_RESOURCE_DIRECTORY_ENTRY, *PIMAGE_RESOURCE_DIRECTORY_ENTRY;
//最终的资源结构
typedef struct _IMAGE_RESOURCE_DATA_ENTRY {
DWORD OffsetToData; // 资源的RVA (相对虚拟地址)
DWORD Size; // 资源数据的大小
DWORD CodePage; // 代码页,通常用于文本资源
DWORD Reserved; // 保留字段
} IMAGE_RESOURCE_DATA_ENTRY, *PIMAGE_RESOURCE_DATA_ENTRY;
资源类型(第一层) | 类型ID值 | 资源类型 | 类型ID值 |
---|---|---|---|
光标 (Cursor) | 01h | 字体 (Font) | 08h |
位图 (Bitmap) | 02h | 加速键 (Accelerators) | 09h |
图标 (Icon) | 03h | 未格式化资源 (Unformatted) | 0Ah |
菜单 (Menu) | 04h | 消息表 (MessageTable) | 0Bh |
对话框 (Dialog) | 05h | 光标组 (Group Cursor) | 0Ch |
字符串 (String) | 06h | 图标组 (Group Icon) | 0Eh |
字体目录 (Font Directory) | 07h | 版本信息 (Version Information) | 10h |
TLS表
我放在专门讲TLS的地方了