Reactos 第 5 章 进程与线程 — 5.5 Windows 的可执行程序映像

第 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:资源文件(图片、图标等)
  • 数据目录:套件的索引目录,告诉系统如何使用各个组件

操作系统加载可执行文件就像组装套件

  1. 读取外包装标签(PE Header)
  2. 按照目录索引(DataDirectory)找到各个组件
  3. 将组件放到合适的位置(内存映射)
  4. 按照说明书(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 设计原则

  1. 兼容性:保留 DOS Header 支持旧系统
  2. 模块化:节区划分清晰,职责明确
  3. 可扩展性:数据目录支持扩展
  4. 地址无关:通过重定位支持任意加载地址

5.5.9.3 常见陷阱

  1. 基地址冲突:多个模块可能竞争相同的基地址
  2. 缺少重定位信息:如果 IMAGE_FILE_RELOCS_STRIPPED 被设置,无法重定位
  3. 导入函数不存在:DLL 版本不兼容可能导致函数缺失
  4. 资源损坏:资源格式错误会导致程序崩溃

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) --- 线程创建相关
相关推荐
caimouse3 小时前
Reactos 第 5 章 进程与线程 — 5.2 Windows 进程的用户空间
windows·架构
莫逸风3 小时前
【AgentScope】6.文件系统(Filesystem)详解
开发语言·windows·springai·agentscope·agnet
超级无敌zhq3 小时前
内网权限维持实战:打造持久化后门与隐蔽通道
网络·windows·安全·网络安全
shandianchengzi4 小时前
【记录】VSCode|Windows 下 VS Code 配置 Git Bash 为默认终端完整教程
windows·git·vscode·bash
caimouse4 小时前
Reactos 第 5 章 进程与线程 — 5.6 Windows 的进程创建和映像装入
windows
泡^泡4 小时前
Python数据类型与运算符
开发语言·windows·python
caimouse16 小时前
Reactos 第 4 章 对象管理 — 4.5 几个常用的内核函数
c语言·windows·架构
caimouse16 小时前
Reactos 第 4 章 对象管理 — 4.3 句柄和句柄表(Handle & Handle Table)
c语言·windows·架构
Chase_______17 小时前
【Java基础 | 15】集合框架(中):Set、HashSet、TreeSet 与哈希表
java·windows·散列表