C++之《程序员自我修养》读书总结(5)

《程序员自我修养》读书总结(五)

Author: Once Day Date: 2026年2月12日

一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦...

漫漫长路,有人对你微笑过嘛...

全系列文章可参考专栏: 书籍阅读_Once-Day的博客-CSDN博客

参考文章:


文章目录

  • 《程序员自我修养》读书总结(五)
        • [5. Windows PE/COFF 格式](#5. Windows PE/COFF 格式)
          • [5.1 发展历史](#5.1 发展历史)
          • [5.2 mingw-w64 工具链](#5.2 mingw-w64 工具链)
          • [5.3 COFF 文件结构](#5.3 COFF 文件结构)
          • [5.4 COFF 符号表](#5.4 COFF 符号表)
          • [5.5 PE 文件格式](#5.5 PE 文件格式)
5. Windows PE/COFF 格式
5.1 发展历史

在 Windows 平台上,PE/COFF 是核心的二进制文件格式体系,其历史可以追溯到早期的 COFF(Common Object File Format)。COFF 最初用于 Unix 系统目标文件,强调可重定位目标文件的结构化组织。微软在此基础上进行扩展,形成适用于 Windows 平台的 PE(Portable Executable)格式,用于可执行文件、动态链接库以及驱动程序等。

在 Win32 时代,目标文件通常采用 COFF 格式,而最终生成的可执行文件或 DLL 则采用 PE 格式。PE 文件本质上是在 COFF 结构之上增加了装载信息与 Windows 特有的数据目录,例如导入表、导出表、资源表和重定位表等。这种分层设计使得编译器与链接器可以延续 COFF 的目标文件结构,同时满足操作系统加载器的需求。

随着 Win64 平台的出现,微软对 PE 格式进行了扩展,形成 PE32+。其核心差异在于支持 64 位地址空间,例如可选头中的 ImageBase、栈和堆大小字段从 32 位扩展为 64 位,而某些与 16 位兼容相关的字段被移除。PE32+ 保持了整体结构的一致性,从而保证工具链和加载机制的平滑过渡。

在结构组织上,PE/COFF 同样以段(Section)为核心。常见段包括 .text(代码)、.data(已初始化数据)、.rdata(只读数据)、.bss(未初始化数据)等。链接器会根据段属性为其分配不同的访问权限,例如可执行、可读或可写。这种段级别的权限划分,与操作系统的页级内存保护机制紧密配合,增强了程序运行时的安全性。

一个典型的 PE 文件加载流程可以概括如下:
读取 DOS 头
定位 PE Header
解析 Section Table
映射各 Section 到内存
处理重定位与导入表
跳转到入口点执行

通过 dumpbin /headersobjdump -x 等工具,可以直观查看这些头部与段表信息。理解 PE/COFF 的历史与结构,不仅有助于掌握 Windows 平台的程序装载机制,也为分析崩溃转储、逆向工程以及底层调试奠定了坚实基础。

5.2 mingw-w64 工具链

mingw-w64 是在原始 MinGW 基础上发展而来的 Windows 原生工具链,其核心目标是在不依赖微软编译器的前提下,使用 GCC 生成可在 Windows 上直接运行的本地程序。相较早期仅支持 32 位的 MinGW,mingw-w64 增强了对 64 位架构的支持,并补全了大量 Windows API 头文件与运行库接口,使其在现代 Windows 平台上具备更好的兼容性与可维护性。

从组成上看,mingw-w64 并非单一编译器,而是一整套工具集合。核心包括 gcc/g++ 编译器前端、binutils(如 ldasobjdump)、mingw-w64 运行时库(CRT)、Windows API 头文件与导入库,以及可选的 gdb 调试器。编译过程与类 Unix 系统一致,例如:

bash 复制代码
x86_64-w64-mingw32-g++ main.cpp -o app.exe

这里的三元组 x86_64-w64-mingw32 表明目标平台为 64 位 Windows,体现了交叉编译工具链的命名规范。

在用途上,mingw-w64 既可以作为 Windows 本地开发环境,也可以在 Linux 或 macOS 上进行交叉编译。例如在 Linux 上构建 Windows 可执行文件:

bash 复制代码
sudo apt install mingw-w64
x86_64-w64-mingw32-gcc hello.c -o hello.exe

这种能力在持续集成与跨平台发布场景中尤为重要,能够避免频繁切换操作系统环境。

常见发行版本主要包括 MSYS2WinLibsMingw-w64-builds 等。MSYS2 提供基于 pacman 的包管理系统,便于安装和升级;WinLibs 则提供预编译的压缩包,适合快速部署;部分 IDE(如 Code::Blocks)也内置特定版本工具链。不同发行版本在默认线程模型(posixwin32)以及异常处理机制(sehsjljdwarf)上可能存在差异,选择时需结合目标平台与性能需求。

安装方式通常分为三类:下载安装包直接解压配置环境变量,通过 MSYS2 包管理安装,或在类 Unix 系统中通过系统仓库获取交叉编译版本。

配置完成后,将 bin 目录加入 PATH,即可在命令行或 IDE 中调用 gccg++ 等工具。掌握 mingw-w64 的工具链结构与变种差异,有助于在跨平台构建和 ABI 兼容问题上做出更合理的技术决策。

5.3 COFF 文件结构

PE 文件结构中,映像头(Image Header)承担着描述整体布局与装载属性的职责。它位于 DOS Header 之后,由 File HeaderOptional Header 组成。File Header 包含机器类型、段数量、时间戳等基本信息,而 Optional Header(在可执行文件中实际上是必选的)则定义入口点地址、映像基址 ImageBase、节对齐方式以及数据目录表等关键字段。操作系统加载器正是依据这些信息,判断文件类型并完成内存映射。

从语义上看,映像头更像是"全局配置中心"。例如 AddressOfEntryPoint 决定程序从何处开始执行,Subsystem 指明是控制台程序还是 GUI 程序,而数据目录中的导入表、导出表、重定位表等条目,则提供后续解析所需的偏移位置。使用 dumpbin /headers 可以清晰查看这些字段,它们共同决定了映像文件的运行方式与系统交互模式。

段表(Section Table)紧随映像头之后,是对各个段(Section)的结构化描述。每个段表项记录段名、虚拟地址、文件偏移、大小以及访问属性等信息。加载器根据这些描述,将文件中的数据按页对齐映射到进程虚拟地址空间。例如 .text 通常标记为可执行和只读,而 .data 则具有读写权限。

_IMAGE_FILE_HEADERPE/COFF 文件结构中的核心组成部分,位于 IMAGE_NT_HEADERS 中的 File Header 区域。它用于描述目标文件或可执行文件的基本属性,例如目标架构、节数量以及符号表信息。该结构在目标文件(.obj)与可执行文件(.exe/.dll)中均存在,是链接器与加载器识别文件类型的重要依据。

其典型定义如下(来自 Windows SDK):

c 复制代码
typedef struct _IMAGE_FILE_HEADER {
    WORD  Machine;
    WORD  NumberOfSections;
    DWORD TimeDateStamp;
    DWORD PointerToSymbolTable;
    DWORD NumberOfSymbols;
    WORD  SizeOfOptionalHeader;
    WORD  Characteristics;
} IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER;

各字段含义如下:

字段名 类型 含义说明
Machine WORD 指定目标 CPU 架构,如 IMAGE_FILE_MACHINE_I386(0x014c)表示 x86,IMAGE_FILE_MACHINE_AMD64(0x8664)表示 x64。加载器据此判断是否与当前系统架构匹配。
NumberOfSections WORD 文件中节(Section)的数量,对应后续 Section Table 中的条目数。
TimeDateStamp DWORD 文件创建时间戳,通常为自 1970 年 1 月 1 日以来的秒数,可用于调试与版本追踪。
PointerToSymbolTable DWORD 指向 COFF 符号表的文件偏移,仅在目标文件中有效;在可执行文件中通常为 0。
NumberOfSymbols DWORD 符号表中的符号数量,主要用于 .obj 文件,供链接器解析。
SizeOfOptionalHeader WORD 后续 Optional Header 的大小。对于可执行文件,该值非 0;而纯 COFF 目标文件中通常为 0。
Characteristics WORD 文件属性标志位,如是否为可执行文件、是否为 DLL、是否支持大地址空间等,使用按位组合的宏定义表示。

_IMAGE_SECTION_HEADER 用于描述 PE 文件中的每一个段(Section),是段表(Section Table)的基本单元。链接器在生成可执行文件时,会为每个段生成一个对应的段头;操作系统加载器则依据这些信息,将文件中的段正确映射到进程虚拟地址空间,并设置相应的访问权限。

其典型结构定义如下(来自 Windows SDK):

c 复制代码
typedef struct _IMAGE_SECTION_HEADER {
    BYTE  Name[8];
    union {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;
    DWORD VirtualAddress;
    DWORD SizeOfRawData;
    DWORD PointerToRawData;
    DWORD PointerToRelocations;
    DWORD PointerToLinenumbers;
    WORD  NumberOfRelocations;
    WORD  NumberOfLinenumbers;
    DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

各字段含义如下:

字段名 类型 含义说明
Name BYTE[8] 段名,最长 8 字节,如 .text.data.rdata 等,不足部分以 \0 填充。
Misc.VirtualSize DWORD 段在内存中的实际大小(字节数)。若大于 SizeOfRawData,多余部分在内存中补零。
VirtualAddress DWORD 段在内存中的相对虚拟地址(RVA),基于 ImageBase 计算实际加载地址。
SizeOfRawData DWORD 段在文件中的大小,通常按文件对齐值对齐。
PointerToRawData DWORD 段内容在文件中的偏移位置。
PointerToRelocations DWORD 指向重定位表的文件偏移,主要用于 COFF 目标文件,在可执行文件中通常为 0。
PointerToLinenumbers DWORD 指向行号信息表的偏移,已较少使用,多数情况下为 0。
NumberOfRelocations WORD 重定位项数量,仅在目标文件中有效。
NumberOfLinenumbers WORD 行号条目数量,现代编译环境中通常为 0。
Characteristics DWORD 段属性标志,如是否包含代码、是否可执行、是否可读写等,以位标志形式组合。

在运行时,加载器依据 VirtualAddressVirtualSize 将段映射到内存,并根据 Characteristics 设置页面权限。例如 .text 通常具有 IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ 属性,而 .data 则具有 IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_WRITE。通过理解该结构,可以清晰区分文件布局与内存布局之间的映射关系。

5.4 COFF 符号表

COFF 目标文件中,符号表(Symbol Table)是链接阶段的核心数据结构,用于描述函数、全局变量及外部引用等符号信息。编译器在生成 .obj 文件时,会为每个可见符号生成一条记录;链接器则遍历这些记录,完成符号解析与重定位。与可执行文件不同,COFF 符号表主要存在于目标文件中,最终链接生成的 PE 文件通常不再保留完整符号信息。

一个典型的 COFF 符号表项结构如下:

c 复制代码
typedef struct _IMAGE_SYMBOL {
    union {
        BYTE  ShortName[8];
        struct {
            DWORD Short;     // 若为0,则使用Long
            DWORD Long;      // 指向字符串表的偏移
        } Name;
    } N;
    DWORD Value;
    SHORT SectionNumber;
    WORD  Type;
    BYTE  StorageClass;
    BYTE  NumberOfAuxSymbols;
} IMAGE_SYMBOL;

各字段含义可概括如下:

字段 含义说明
Name 符号名。若长度不超过 8 字节,直接存储;否则通过偏移索引到字符串表。
Value 符号在所属段中的偏移地址。对于函数或变量,表示相对段起始的偏移。
SectionNumber 符号所属段编号;为 0 表示未定义(外部符号),为负值表示特殊符号。
Type 符号类型,通常低位表示基本类型,高位表示派生类型(如函数)。
StorageClass 存储类别,如 IMAGE_SYM_CLASS_EXTERNAL 表示外部符号。
NumberOfAuxSymbols 后续附加符号数量,用于补充信息(如函数大小等)。

结合一个简单示例进行说明。假设存在如下代码:

c 复制代码
int global_var = 10;

int add(int a, int b) {
    return a + b + global_var;
}

编译为 .obj 后,可通过 dumpbin /symbols 查看符号表。global_var 通常会显示为已定义外部符号,SectionNumber 指向 .data 段;add 位于 .text 段,其 Value 表示函数在代码段内的偏移。若另一个文件中声明 extern int global_var;,则在该文件的符号表中会出现同名但 SectionNumber 为 0 的未定义符号,等待链接器解析。

从链接器视角看,符号解析流程大致如下:
读取目标文件符号表
建立全局符号映射
匹配未定义符号
执行重定位修正

因此,COFF 符号表不仅是编译产物,更是模块化构建机制的基础。理解其结构与字段语义,有助于分析"未定义符号""重复定义"等常见链接错误,并深入掌握 Windows 平台目标文件的内部组织方式。

5.5 PE 文件格式

PE 文件格式在设计之初就承载了向后兼容的历史使命,因此其文件开头并不是直接出现 COFF 文件头,而是以一个 DOS MZ 头部开始。该头部结构源自早期的 MS-DOS 可执行文件格式,其前两个字节为 MZ 签名。紧随其后的,是一段称为 DOS Stub 的桩代码。这种布局并非偶然,而是为了保证在 DOS 环境下执行时不会导致系统崩溃。

DOS Stub 的典型功能是在 DOS 系统中输出一段提示信息,例如 "This program cannot be run in DOS mode.",随后正常退出。其本质是一段合法的 16 位 DOS 程序。当 DOS 加载器发现该文件时,会按照 MZ 格式执行这段代码;而在 Windows 环境中,加载器会读取 e_lfanew 字段,跳转到真正的 PE 头部位置,忽略 Stub 内容。这种双重结构体现了早期操作系统演进过程中的兼容策略。

Image Header(即 COFF File Header)基础之上,PE 引入了更为复杂的 Optional Header 结构。尽管名为"可选",但对于可执行文件和 DLL 来说它是必不可少的。

该结构包含入口点地址 AddressOfEntryPoint、映像基址 ImageBase、段对齐参数、子系统类型以及数据目录数组等信息。尤其是数据目录表,定义了导入表、导出表、资源表、重定位表等关键数据结构的定位方式,是 Windows 加载机制的重要支撑。

从整体结构上看,PE 文件可以抽象为如下层次:
DOS Header + DOS Stub
PE Signature
COFF File Header
PE Optional Header
Section Table
Section Data

这种结构体现了历史与现实的叠加。虽然 DOS 早已退出主流舞台,但 MZ 头部与桩代码依然保留在每一个 Windows 可执行文件中,成为 PE 格式的历史印记。这种兼容性设计既增加了文件结构的复杂度,也展示了操作系统生态在长期演进中对稳定性的高度重视。

相关推荐
会周易的程序员8 小时前
cNetgate物联网网关内存数据表和数据视图模块架构
c语言·c++·物联网·架构·lua·iot
爱编码的小八嘎8 小时前
第3章 Windows运行机理-3.1 内核分析(6)
c语言
宇木灵8 小时前
C语言基础-十、文件操作
c语言·开发语言·学习
云泽8089 小时前
C++ 多态入门:虚函数、重写、虚析构及 override/final 实战指南(附腾讯面试题)
开发语言·c++
仰泳的熊猫9 小时前
题目1535:蓝桥杯算法提高VIP-最小乘积(提高型)
数据结构·c++·算法·蓝桥杯
闻缺陷则喜何志丹10 小时前
【前后缀分解】P9255 [PA 2022] Podwyżki|普及+
数据结构·c++·算法·前后缀分解
消失的旧时光-194311 小时前
智能指针(二):机制篇 —— 移动语义与所有权转移
c++·智能指针
风吹乱了我的头发~12 小时前
Day31:2026年2月21日打卡
开发语言·c++·算法
mjhcsp12 小时前
C++ 后缀平衡树解析
android·java·c++