第 5 章 进程与线程 --- 5.5 Windows 的可执行程序映像
概述:为什么 PE 文件要这样布局?
Windows 可执行文件采用 PE(Portable Executable)格式,这是一种经过精心设计的文件结构。PE 格式的布局并非随意安排,而是基于操作系统加载器、内存管理器、安全子系统等多个内核组件的协同工作需求。理解 PE 文件的布局设计,有助于我们深入理解 Windows 的进程创建、代码加载、地址空间管理等核心机制。
PE 格式的核心设计目标
PE 格式的设计围绕以下几个核心目标:
1. 支持直接内存映射
PE 文件的最关键特性是它可以被操作系统直接映射到内存中执行。为了实现这一点,PE 文件的布局必须与内存布局高度一致:
- 节区对齐:SectionAlignment 和 FileAlignment 确保文件中的节区可以被直接映射到内存页上,无需重新组织数据。
- 相对虚拟地址(RVA):PE 文件中的所有内部指针都使用相对虚拟地址,即相对于映像基地址的偏移量。这使得整个文件是"位置无关"的------无论被加载到哪个地址,内部引用仍然有效。
- 段属性分离:代码段(.text)标记为可执行但不可写,数据段(.data)标记为可读写,只读数据段(.rdata)标记为只读。这种设计直接对应 CPU 的内存保护机制,防止代码被意外修改。
2. 高效的资源组织
PE 文件将不同类型的数据组织到不同的节区中,每个节区有明确的用途和属性:
- 代码与数据分离:将可执行代码(.text)与数据(.data, .rdata)分离,支持代码共享(多个进程可以共享同一份只读代码页面)和写时复制(Copy-On-Write)。
- 资源独立管理:图标、字符串、对话框等资源存储在独立的 .rsrc 节区,可以被系统资源加载器独立访问,无需加载整个程序。
- 重定位信息:当映像无法加载到预期的基地址时,.reloc 节区提供了需要调整的指针列表,使得加载器可以快速完成地址重定位。
3. 模块化与扩展性
PE 格式通过数据目录(DataDirectory)机制实现了高度的模块化和可扩展性:
- 数据目录数组:16 个数据目录项提供了对导出表、导入表、资源表、异常表、TLS 表等重要数据结构的索引。新增功能只需增加新的数据目录项,无需修改整体结构。
- 延迟加载支持:导入表的设计支持延迟加载 DLL,减少程序启动时间和内存占用。
- 导出表:DLL 的导出函数通过导出表管理,支持按名称或序号导出,便于外部调用。
4. 安全性与完整性
PE 格式在设计上考虑了安全性需求:
- 校验和(Checksum):文件头中的校验和可以验证文件完整性,防止文件被篡改。
- DLL 特性标志(DllCharacteristics):指定 DEP(数据执行保护)、ASLR(地址空间布局随机化)、SafeSEH(安全结构化异常处理)等安全特性。
- 数字签名:通过 Authenticode 数字签名机制,可以验证文件的来源和完整性。
5. 向后兼容性
PE 格式保留了 DOS 头(DOS Header)和 DOS Stub,以确保在 DOS 系统上尝试运行 Windows 程序时能够显示一条提示信息(通常是"This program cannot be run in DOS mode")。这虽然在现代系统中几乎没有实际用途,但体现了 Windows 对向后兼容性的重视。
这种设计的优势
优势 1:加载速度快
由于 PE 文件与内存布局一致,操作系统加载器只需将文件的各个节区映射到对应的内存页即可,无需复杂的数据解析和重组。相比之下,某些脚本语言(如 Python、JavaScript)需要在运行时逐行解析,启动开销显著更大。
优势 2:内存效率高
- 多个进程可以共享同一份 DLL 的代码页面(.text 节区通常是只读且可共享的)。
- 未初始化数据(.bss)不占用文件空间,只在内存中分配,减少文件大小。
- 资源按需加载,只有实际需要的资源才会被读入内存。
优势 3:安全性强
通过节区属性分离和现代安全特性(DEP、ASLR、SafeSEH),PE 格式在架构层面支持操作系统的安全机制。恶意代码难以修改只读的代码段,溢出攻击需要绕过多层保护。
优势 4:灵活可扩展
数据目录机制使得 PE 格式可以轻松支持新功能,如 .NET 元数据、清单文件(Manifest)、调试信息等。这些扩展通过新的数据目录项实现,不影响旧版加载器的兼容性。
优势 5:调试与诊断友好
PE 文件中的调试信息目录(DataDirectory6)可以指向 PDB 文件,使得调试器能够将机器码与源代码关联。导出表、导入表等信息也便于诊断工具分析程序的依赖关系。
小结
PE 格式的设计是操作系统工程的典范:它将文件结构与内存结构统一考虑,将性能需求与安全需求平衡设计,在保证效率的同时提供了强大的扩展能力。理解 PE 文件的布局,是理解 Windows 进程创建、模块加载、地址空间管理等核心机制的关键一步。
5.5.0 框架图
┌──────────────────────────────────────────────────────────────────────────────────┐
│ Windows 可执行程序映像结构 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ PE 文件结构 │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ DOS Header (IMAGE_DOS_HEADER) │ │
│ │ │ │ │
│ │ └─► DOS Stub (MZ 标记 + 跳转指令) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ PE Header (IMAGE_NT_HEADERS) │ │
│ │ │ │ │
│ │ ├─► Signature ("PE\0\0") │ │
│ │ ├─► FileHeader (IMAGE_FILE_HEADER) │ │
│ │ │ ├─► Machine (CPU 类型) │ │
│ │ │ ├─► NumberOfSections (节区数量) │ │
│ │ │ ├─► TimeDateStamp │ │
│ │ │ └─► Characteristics (特性标志) │ │
│ │ │ │ │
│ │ └─► OptionalHeader (IMAGE_OPTIONAL_HEADER) │ │
│ │ ├─► Magic (PE32/PE32+) │ │
│ │ ├─► AddressOfEntryPoint (入口点) │ │
│ │ ├─► ImageBase (基地址) │ │
│ │ ├─► SectionAlignment (节区对齐) │ │
│ │ ├─► FileAlignment (文件对齐) │ │
│ │ ├─► SizeOfImage (映像大小) │ │
│ │ ├─► SizeOfHeaders (头部大小) │ │
│ │ ├─► Subsystem (子系统) │ │
│ │ ├─► DllCharacteristics (DLL 特性) │ │
│ │ └─► DataDirectory[16] (数据目录) │ │
│ │ ├─► [0] Export Table │ │
│ │ ├─► [1] Import Table │ │
│ │ ├─► [2] Resource Table │ │
│ │ ├─► [3] Exception Table │ │
│ │ ├─► [5] Base Relocation Table │ │
│ │ └─► [13] TLS Table │ │
│ │ │ │
│ │ Section Headers (IMAGE_SECTION_HEADER[]) │ │
│ │ ├─► .text (代码段) │ │
│ │ ├─► .data (数据段) │ │
│ │ ├─► .rdata (只读数据) │ │
│ │ ├─► .bss (未初始化数据) │ │
│ │ ├─► .rsrc (资源) │ │
│ │ └─► .reloc (重定位) │ │
│ │ │ │
│ │ Section Data (节区数据) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.5.0.1 设计意图
核心问题
Windows 可执行文件是什么格式?它包含哪些部分?操作系统如何加载和执行它?
设计哲学 :「统一的可执行格式」
想象一下,Windows 可执行文件就像一个打包好的软件套件:
- 文件头:套件的外包装标签,说明里面是什么内容
- 节区(Sections) :套件中的各个组件包
- .text:核心代码组件
- .data:配置数据
- .rsrc:资源文件(图片、图标等)
- 数据目录:套件的索引目录,告诉系统如何使用各个组件
操作系统加载可执行文件就像组装套件:
- 读取外包装标签(PE Header)
- 按照目录索引(DataDirectory)找到各个组件
- 将组件放到合适的位置(内存映射)
- 按照说明书(AddressOfEntryPoint)开始运行
本节定位
本节深入分析 Windows 可执行程序的 PE(Portable Executable)格式,包括文件结构、节区组织、数据目录等。读完本节后,读者应当能够:
- 理解 PE 文件的整体结构
- 掌握各个节区的作用
- 理解数据目录的用途
- 理解重定位机制
- 理解导入/导出表
5.5.1 PE 文件格式概述
PE 格式:Windows 的统一可执行格式
PE(Portable Executable)是 Windows 操作系统使用的可执行文件格式。它是一种灵活的、可扩展的格式,支持 32 位和 64 位程序。
PE 文件的类型:
| 文件类型 | 扩展名 | 说明 |
|---|---|---|
| 应用程序 | .exe | 可直接执行的程序 |
| 动态链接库 | .dll | 可被其他程序调用的库 |
| 驱动程序 | .sys | 内核模式驱动程序 |
| 控件 | .ocx | ActiveX 控件 |
| 屏幕保护 | .scr | 屏幕保护程序 |
PE 格式的历史:
- MS-DOS:使用 MZ 格式(DOS 可执行文件)
- Windows 3.x:使用 NE(New Executable)格式
- Windows NT:引入 PE 格式,延续至今
- Windows 64 位:扩展为 PE32+ 格式
PE 文件的布局:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ PE 文件内存布局 │
│ │
│ 虚拟地址空间 │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ 高地址 ─────────────────────────────────────────────────────────► │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 内核空间 (通常不映射到用户进程) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 栈 (Stack) │ │ │
│ │ │ - 向下增长 │ │ │
│ │ │ - 存放局部变量、函数调用信息 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 堆 (Heap) │ │ │
│ │ │ - 向上增长 │ │ │
│ │ │ - 动态分配的内存 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PE 映像 (Image) │ │ │
│ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
│ │ │ │ .text (代码段) │ │ │ │
│ │ │ │ .data (数据段) │ │ │ │
│ │ │ │ .rdata (只读数据) │ │ │ │
│ │ │ │ .rsrc (资源) │ │ │ │
│ │ │ │ .reloc (重定位) │ │ │ │
│ │ │ └─────────────────────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ PEB / TEB (进程/线程环境块) │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ 低地址 ─────────────────────────────────────────────────► │ │ │
│ │ │ - 通常为空或存放特殊数据 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────────────┘
5.5.2 DOS Header 和 DOS Stub
DOS Header:兼容旧系统的入口
DOS Header(IMAGE_DOS_HEADER)是 PE 文件的开头部分,用于兼容 MS-DOS 系统。
结构定义:
c
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; // 初始 SS 值
WORD e_sp; // 初始 SP 值
WORD e_csum; // 校验和
WORD e_ip; // 初始 IP 值
WORD e_cs; // 初始 CS 值
WORD e_lfarlc; // 重定位表偏移
WORD e_ovno; // 覆盖号
WORD e_res[4]; // 保留
WORD e_oemid; // OEM 标识符
WORD e_oeminfo; // OEM 信息
WORD e_res2[10]; // 保留
LONG e_lfanew; // PE Header 的偏移!
} IMAGE_DOS_HEADER;
关键字段:
| 字段 | 说明 |
|---|---|
e_magic |
"MZ"(0x5A4D),标识 DOS 可执行文件 |
e_lfanew |
PE Header 在文件中的偏移量,这是关键! |
DOS Stub:兼容性代码
DOS Stub 是一段短小的 DOS 程序,当在 DOS 系统上运行 PE 文件时,它会执行并显示一条消息。
This program cannot be run in DOS mode.
e_lfanew 的重要性:
e_lfanew 字段指向 PE Header 的位置。加载器通过这个字段跳过 DOS 部分,直接找到真正的 PE Header。
源码位置:ntimage.h(file:///d:/reactos/sdk/include/ddk/ntimage.h#L51-L71)
5.5.3 PE Header
PE Header:文件的核心描述
PE Header(IMAGE_NT_HEADERS)包含了文件的核心信息,是加载器最关心的部分。
结构定义:
c
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; // "PE\0\0" (0x00004550)
IMAGE_FILE_HEADER FileHeader; // 文件头
IMAGE_OPTIONAL_HEADER OptionalHeader; // 可选头
} IMAGE_NT_HEADERS;
FileHeader(IMAGE_FILE_HEADER):
c
typedef struct _IMAGE_FILE_HEADER {
WORD Machine; // CPU 类型
WORD NumberOfSections; // 节区数量
DWORD TimeDateStamp; // 时间戳
DWORD PointerToSymbolTable; // 符号表偏移
DWORD NumberOfSymbols; // 符号数量
WORD SizeOfOptionalHeader; // 可选头大小
WORD Characteristics; // 特性标志
} IMAGE_FILE_HEADER;
Machine 字段:
| 值 | 说明 |
|---|---|
| 0x014C | Intel 386 |
| 0x0200 | Intel Itanium |
| 0x8664 | AMD64 |
Characteristics 字段:
| 标志 | 值 | 说明 |
|---|---|---|
| IMAGE_FILE_RELOCS_STRIPPED | 0x0001 | 无重定位信息 |
| IMAGE_FILE_EXECUTABLE_IMAGE | 0x0002 | 可执行文件 |
| IMAGE_FILE_LINE_NUMS_STRIPPED | 0x0004 | 无行号信息 |
| IMAGE_FILE_LOCAL_SYMS_STRIPPED | 0x0008 | 无局部符号 |
| IMAGE_FILE_DLL | 0x2000 | DLL 文件 |
OptionalHeader(IMAGE_OPTIONAL_HEADER):
c
typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic; // PE32=0x10B, PE32+=0x20B
BYTE MajorLinkerVersion; // 链接器主版本
BYTE MinorLinkerVersion; // 链接器次版本
DWORD SizeOfCode; // 代码大小
DWORD SizeOfInitializedData; // 已初始化数据大小
DWORD SizeOfUninitializedData; // 未初始化数据大小
DWORD AddressOfEntryPoint; // 入口点地址!
DWORD BaseOfCode; // 代码基地址
DWORD BaseOfData; // 数据基地址(仅 PE32)
// PE32+ 没有 BaseOfData,而是直接有 ImageBase
DWORD ImageBase; // 建议的加载基地址
DWORD SectionAlignment; // 内存中的节区对齐
DWORD FileAlignment; // 文件中的节区对齐
WORD MajorOperatingSystemVersion;// OS 主版本
WORD MinorOperatingSystemVersion;// OS 次版本
WORD MajorImageVersion; // 映像主版本
WORD MinorImageVersion; // 映像次版本
WORD MajorSubsystemVersion; // 子系统主版本
WORD MinorSubsystemVersion; // 子系统次版本
DWORD Win32VersionValue; // Win32 版本
DWORD SizeOfImage; // 映像总大小
DWORD SizeOfHeaders; // 头部总大小
DWORD CheckSum; // 校验和
WORD Subsystem; // 子系统
WORD DllCharacteristics; // DLL 特性
DWORD SizeOfStackReserve; // 保留栈大小
DWORD SizeOfStackCommit; // 提交栈大小
DWORD SizeOfHeapReserve; // 保留堆大小
DWORD SizeOfHeapCommit; // 提交堆大小
DWORD LoaderFlags; // 加载器标志
DWORD NumberOfRvaAndSizes; // 数据目录数量
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER;
关键字段:
| 字段 | 说明 |
|---|---|
Magic |
PE32(0x10B)或 PE32+(0x20B) |
AddressOfEntryPoint |
程序入口点的 RVA |
ImageBase |
建议的内存加载地址 |
SectionAlignment |
节区在内存中的对齐方式 |
FileAlignment |
节区在文件中的对齐方式 |
Subsystem |
目标子系统(控制台/GUI) |
DataDirectory |
数据目录数组(16 个条目) |
Subsystem 字段:
| 值 | 说明 |
|---|---|
| 1 | 本机(设备驱动) |
| 2 | Windows GUI |
| 3 | Windows 控制台 |
| 5 | OS/2 |
| 7 | POSIX |
源码位置:
- IMAGE_NT_HEADERS(file:///d:/reactos/sdk/include/ddk/ntimage.h#L399-L403)
- IMAGE_FILE_HEADER(file:///d:/reactos/sdk/include/ddk/ntimage.h#L248-L256)
- IMAGE_OPTIONAL_HEADER32(file:///d:/reactos/sdk/include/ddk/ntimage.h#L290-L322)
- IMAGE_OPTIONAL_HEADER64(file:///d:/reactos/sdk/include/ddk/ntimage.h#L340-L371)
5.5.4 节区(Sections)
节区:程序的组成部分
节区是 PE 文件的实际内容区域,每个节区有特定的用途和属性。
节区头结构(IMAGE_SECTION_HEADER):
c
typedef struct _IMAGE_SECTION_HEADER {
BYTE Name[IMAGE_SIZEOF_SHORT_NAME]; // 节区名称
union {
DWORD PhysicalAddress;
DWORD VirtualSize;
} Misc;
DWORD VirtualAddress; // 节区的 RVA
DWORD SizeOfRawData; // 文件中的大小
DWORD PointerToRawData; // 文件中的偏移
DWORD PointerToRelocations; // 重定位表偏移
DWORD PointerToLinenumbers; // 行号表偏移
WORD NumberOfRelocations; // 重定位数量
WORD NumberOfLinenumbers; // 行号数量
DWORD Characteristics; // 节区特性
} IMAGE_SECTION_HEADER;
常见节区:
| 节区名 | 说明 | 特性 |
|---|---|---|
.text |
代码段,存放可执行指令 | 可执行、可读 |
.data |
数据段,存放已初始化数据 | 可读写 |
.rdata |
只读数据,存放常量、字符串 | 只读 |
.bss |
未初始化数据段 | 可读写 |
.rsrc |
资源段,存放图标、对话框等 | 只读 |
.reloc |
重定位信息 | 只读 |
.tls |
TLS(线程本地存储) | 可读写 |
.edata |
导出表 | 只读 |
.idata |
导入表 | 只读 |
节区特性标志:
| 标志 | 值 | 说明 |
|---|---|---|
| IMAGE_SCN_CNT_CODE | 0x00000020 | 包含代码 |
| IMAGE_SCN_CNT_INITIALIZED_DATA | 0x00000040 | 包含已初始化数据 |
| IMAGE_SCN_CNT_UNINITIALIZED_DATA | 0x00000080 | 包含未初始化数据 |
| IMAGE_SCN_MEM_EXECUTE | 0x20000000 | 可执行 |
| IMAGE_SCN_MEM_READ | 0x40000000 | 可读 |
| IMAGE_SCN_MEM_WRITE | 0x80000000 | 可写 |
节区的内存映射:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 节区的文件与内存映射 │
│ │
│ 文件中的节区布局: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Header (DOS + PE) │ │
│ │ .text (FileAlignment = 0x200) │ │
│ │ .data (FileAlignment = 0x200) │ │
│ │ .rdata (FileAlignment = 0x200) │ │
│ │ .rsrc (FileAlignment = 0x200) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 内存中的节区布局: │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ImageBase + VirtualAddress │ │
│ │ .text (SectionAlignment = 0x1000) │ │
│ │ .data (SectionAlignment = 0x1000) │ │
│ │ .rdata (SectionAlignment = 0x1000) │ │
│ │ .rsrc (SectionAlignment = 0x1000) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 对齐差异: │
│ - FileAlignment: 通常 0x200 (512 字节) │ │
│ - SectionAlignment: 通常 0x1000 (4KB) │ │
│ - 内存中的节区大小会向上对齐到 SectionAlignment │ │
└──────────────────────────────────────────────────────────────────────────────────┘
源码位置:IMAGE_SECTION_HEADER(file:///d:/reactos/sdk/include/ddk/ntimage.h#L211-L225)
5.5.5 数据目录(Data Directory)
数据目录:文件内容的索引
数据目录是一个数组,包含了 PE 文件中各种重要数据结构的位置和大小信息。
数据目录数组:
c
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; // RVA
DWORD Size; // 大小
} IMAGE_DATA_DIRECTORY;
#define IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16
数据目录条目:
| 索引 | 名称 | 说明 |
|---|---|---|
| 0 | Export Table | 导出表,列出 DLL 导出的函数 |
| 1 | Import Table | 导入表,列出需要导入的函数 |
| 2 | Resource Table | 资源表,列出图标、对话框等资源 |
| 3 | Exception Table | 异常表,用于结构化异常处理 |
| 4 | Certificate Table | 证书表,用于数字签名 |
| 5 | Base Relocation Table | 重定位表,用于地址重定位 |
| 6 | Debug | 调试信息 |
| 7 | Architecture | 架构特定数据 |
| 8 | Global Pointer Table | GP 表(用于某些架构) |
| 9 | TLS Table | TLS 表 |
| 10 | Load Configuration | 加载配置 |
| 11 | Bound Import | 绑定导入表 |
| 12 | IAT | 导入地址表 |
| 13 | Delay Import Descriptor | 延迟导入描述符 |
| 14 | CLR Runtime Header | .NET 运行时头 |
| 15 | Reserved | 保留 |
源码位置:IMAGE_DATA_DIRECTORY(file:///d:/reactos/sdk/include/ddk/ntimage.h#L282-L285)
5.5.6 导入表与导出表
导入表:程序依赖的外部函数
导入表(Import Table)列出了程序需要从其他 DLL 导入的函数。
导入表结构:
c
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics; // 0(未使用)
DWORD OriginalFirstThunk; // 指向 IMAGE_THUNK_DATA(原始名称表)
};
DWORD TimeDateStamp; // 时间戳
DWORD ForwarderChain; // 转发链
DWORD Name; // DLL 名称的 RVA
DWORD FirstThunk; // 指向 IMAGE_THUNK_DATA(IAT)
} IMAGE_IMPORT_DESCRIPTOR;
导入过程:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 导入表解析过程 │
│ │
│ 1. 加载器读取 Import Table │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ ImportDescriptor[0]: kernel32.dll │ │ │
│ │ ImportDescriptor[1]: user32.dll │ │ │
│ │ ImportDescriptor[2]: ntdll.dll │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 2. 对于每个 DLL,读取名称表 │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ OriginalFirstThunk -> IMAGE_THUNK_DATA[] │ │ │
│ │ [0]: CreateFileA │ │ │
│ │ [1]: WriteFile │ │ │
│ │ [2]: CloseHandle │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 3. 加载器加载对应的 DLL │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ LoadLibrary("kernel32.dll") │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 4. 获取函数地址并填充 IAT │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ FirstThunk (IAT) -> 实际函数地址 │ │ │
│ │ [0]: 0x7C801D77 (CreateFileA 的实际地址) │ │ │
│ │ [1]: 0x7C801E02 (WriteFile 的实际地址) │ │ │
│ │ [2]: 0x7C801F05 (CloseHandle 的实际地址) │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
└──────────────────────────────────────────────────────────────────────────────────┘
导出表:DLL 提供的函数
导出表(Export Table)列出了 DLL 导出的函数,供其他程序调用。
导出表结构:
c
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics; // 0(未使用)
DWORD TimeDateStamp; // 时间戳
WORD MajorVersion; // 主版本
WORD MinorVersion; // 次版本
DWORD Name; // DLL 名称的 RVA
DWORD Base; // 导出序号的基数
DWORD NumberOfFunctions; // 导出函数数量
DWORD NumberOfNames; // 命名函数数量
DWORD AddressOfFunctions; // 函数地址表的 RVA
DWORD AddressOfNames; // 函数名称表的 RVA
DWORD AddressOfNameOrdinals; // 序号表的 RVA
} IMAGE_EXPORT_DIRECTORY;
导出方式:
| 方式 | 说明 |
|---|---|
| 按名称导出 | 通过函数名调用(如 CreateFileA) |
| 按序号导出 | 通过序号调用(如序号 1) |
| 按名称和序号导出 | 两种方式都支持 |
源码位置:
- IMAGE_IMPORT_DESCRIPTOR(file:///d:/reactos/sdk/include/ddk/ntimage.h#L572-L581)
- IMAGE_EXPORT_DIRECTORY(file:///d:/reactos/sdk/include/ddk/ntimage.h#L78-L90)
5.5.7 重定位
重定位:地址无关的关键
当 PE 文件加载到非首选基地址时,需要进行重定位。
为什么需要重定位?
- PE 文件编译时假设会加载到
ImageBase(通常是 0x00400000) - 如果该地址已被其他模块占用,加载器会选择其他地址
- 需要修改代码中的绝对地址引用,使其指向正确的位置
重定位表结构:
c
typedef struct _IMAGE_BASE_RELOCATION {
DWORD VirtualAddress; // 重定位块的 RVA
DWORD SizeOfBlock; // 块大小
// WORD TypeOffset[1]; // 类型和偏移对
} IMAGE_BASE_RELOCATION;
重定位类型:
| 类型 | 说明 |
|---|---|
| 0x00 | 无重定位 |
| 0x03 | HIGHLOW(32 位地址) |
| 0x0A | DIR64(64 位地址,仅 PE32+) |
重定位过程:
┌──────────────────────────────────────────────────────────────────────────────────┐
│ 重定位过程 │
│ │
│ 1. 计算重定位偏移 │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ Delta = ActualBase - ImageBase │ │ │
│ │ = 0x00500000 - 0x00400000 │ │ │
│ │ = 0x00100000 │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 2. 遍历重定位表 │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ BaseRelocation.VirtualAddress = 0x1000 │ │ │
│ │ BaseRelocation.SizeOfBlock = 0x20 │ │ │
│ │ TypeOffset[0] = 0x0305 (TYPE_HIGHLOW + 0x0005) │ │ │
│ │ TypeOffset[1] = 0x0310 (TYPE_HIGHLOW + 0x0010) │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │
│ ▼ │
│ 3. 修改每个需要重定位的地址 │
│ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ Address = ImageBase + VirtualAddress + Offset │ │ │
│ │ = 0x00500000 + 0x1000 + 0x0005 │ │ │
│ │ = 0x00501005 │ │ │
│ │ *Address += Delta │ │ │
│ │ *Address = 原值 + 0x00100000 │ │ │
│ └─────────────────────────────────────────────────────────────┘ │ │
└──────────────────────────────────────────────────────────────────────────────────┘
源码位置:IMAGE_BASE_RELOCATION(file:///d:/reactos/sdk/include/ddk/ntimage.h#L162-L165)
5.5.8 资源
资源:程序的非代码数据
资源包含程序使用的非代码数据,如图标、对话框模板、字符串表等。
资源类型:
| 类型 | 说明 |
|---|---|
| RT_CURSOR | 光标 |
| RT_ICON | 图标 |
| RT_BITMAP | 位图 |
| RT_MENU | 菜单 |
| RT_DIALOG | 对话框 |
| RT_STRING | 字符串表 |
| RT_FONTDIR | 字体目录 |
| RT_FONT | 字体 |
| RT_ACCELERATOR | 快捷键 |
| RT_RCDATA | 原始数据 |
| RT_MESSAGETABLE | 消息表 |
资源的访问:
c
// 加载图标资源
HICON hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_MYICON));
// 加载字符串资源
WCHAR szBuffer[256];
LoadString(hInstance, IDS_MYSTRING, szBuffer, sizeof(szBuffer)/sizeof(WCHAR));
// 加载对话框资源
DialogBox(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), hParent, DialogProc);
5.5.9 小结
5.5.9.1 关键知识点
| 主题 | 关键点 |
|---|---|
| PE 格式 | Windows 统一的可执行文件格式 |
| DOS Header | 兼容 DOS,包含 PE Header 偏移 |
| PE Header | 包含文件的核心信息 |
| 节区 | 程序的组成部分(.text, .data, .rsrc 等) |
| 数据目录 | 16 个条目,索引各种数据结构 |
| 导入表 | 程序依赖的外部函数 |
| 导出表 | DLL 提供的函数 |
| 重定位 | 当加载地址变化时调整地址引用 |
5.5.9.2 设计原则
- 兼容性:保留 DOS Header 支持旧系统
- 模块化:节区划分清晰,职责明确
- 可扩展性:数据目录支持扩展
- 地址无关:通过重定位支持任意加载地址
5.5.9.3 常见陷阱
- 基地址冲突:多个模块可能竞争相同的基地址
- 缺少重定位信息:如果 IMAGE_FILE_RELOCS_STRIPPED 被设置,无法重定位
- 导入函数不存在:DLL 版本不兼容可能导致函数缺失
- 资源损坏:资源格式错误会导致程序崩溃
5.5.9.4 后续学习路径
- PE 文件加载过程
- DLL 加载和初始化
- 延迟加载(Delay Load)
- .NET 程序集格式(CLR Header)
参考资料:Microsoft PE/COFF Specification
PE 格式结构体代码位置索引:
所有 PE 格式核心结构体定义均位于 ReactOS 的 `ntimage.h`(file:///d:/reactos/sdk/include/ddk/ntimage.h) 头文件中:
| 结构体 | 说明 | 行号 |
|---|---|---|
IMAGE_DOS_HEADER |
DOS 文件头 | L51-L71(file:///d:/reactos/sdk/include/ddk/ntimage.h#L51-L71) |
IMAGE_NT_HEADERS32 |
32 位 PE 头 | L399-L403(file:///d:/reactos/sdk/include/ddk/ntimage.h#L399-L403) |
IMAGE_NT_HEADERS64 |
64 位 PE 头 | L393-L397(file:///d:/reactos/sdk/include/ddk/ntimage.h#L393-L397) |
IMAGE_FILE_HEADER |
文件头 | L248-L256(file:///d:/reactos/sdk/include/ddk/ntimage.h#L248-L256) |
IMAGE_OPTIONAL_HEADER32 |
32 位可选头 | L290-L322(file:///d:/reactos/sdk/include/ddk/ntimage.h#L290-L322) |
IMAGE_OPTIONAL_HEADER64 |
64 位可选头 | L340-L371(file:///d:/reactos/sdk/include/ddk/ntimage.h#L340-L371) |
IMAGE_DATA_DIRECTORY |
数据目录 | L282-L285(file:///d:/reactos/sdk/include/ddk/ntimage.h#L282-L285) |
IMAGE_SECTION_HEADER |
节区头 | L211-L225(file:///d:/reactos/sdk/include/ddk/ntimage.h#L211-L225) |
IMAGE_IMPORT_DESCRIPTOR |
导入表描述符 | L572-L581(file:///d:/reactos/sdk/include/ddk/ntimage.h#L572-L581) |
IMAGE_EXPORT_DIRECTORY |
导出表目录 | L78-L90(file:///d:/reactos/sdk/include/ddk/ntimage.h#L78-L90) |
IMAGE_BASE_RELOCATION |
重定位表 | L162-L165(file:///d:/reactos/sdk/include/ddk/ntimage.h#L162-L165) |
IMAGE_RESOURCE_DATA_ENTRY |
资源数据入口 | L95-L100(file:///d:/reactos/sdk/include/ddk/ntimage.h#L95-L100) |
其他相关头文件:
- pecoff.h(file:///d:/reactos/sdk/include/host/pecoff.h) --- PE/COFF 格式辅助定义
- winnt_old.h(file:///d:/reactos/sdk/include/xdk/winnt_old.h) --- 部分 Windows 兼容定义
核心代码位置:
- ntoskrnl/ps/process.c(file:///d:/reactos/ntoskrnl/ps/process.c) --- 进程创建相关
- ntoskrnl/ps/thread.c(file:///d:/reactos/ntoskrnl/ps/thread.c) --- 线程创建相关