Skip to content

Day 85: std.pe模块:PE格式解析

PE(Portable Executable)格式是Windows NT及其所有后续版本(包括XP, 7, 10, 11)使用的标准可执行文件格式。它不仅用于.exe文件,也用于.dll(动态链接库)、.sys(驱动程序)和.obj(对象文件)等。

PE格式源自于更早的COFF(Common Object File Format)格式,并进行了一系列扩展以支持现代操作系统的特性,如虚拟内存、动态链接和安全功能。Zig的std.pe模块提供了一个完整的解析器,用于深入探索PE文件的内部结构。

为了保持与旧的MS-DOS系统的兼容性,每个PE文件都以一个DOS头部和一个小的DOS存根程序(stub program)开始。这个存根程序通常只会打印一条消息,如“This program cannot be run in DOS mode.”。

DOS头部之后,才是在现代Windows上真正起作用的PE头部。std.pe.parse函数会自动处理这个结构。

const std = @import("std");
const pe = std.pe;
pub fn main() !void {
const allocator = std.testing.allocator;
// 注意:在非Windows系统上,你需要有一个PE文件用于测试
const file_bytes = try std.fs.cwd().readFileAlloc(allocator, "kernel32.dll", 1 * 1024 * 1024);
defer allocator.free(file_bytes);
var file = try pe.parse(file_bytes, allocator, .{});
defer file.deinit();
std.debug.print("Machine Type: {any}\n", .{file.header.machine});
std.debug.print("Number of Sections: {d}\n", .{file.header.number_of_sections});
}

PE文件最重要的部分之一是导入表。它列出了该文件从其他DLL中导入的所有函数。动态链接器在加载程序时,会读取这个表,找到所需的DLL,并将导入函数的地址“填入”到程序的内存空间中。

std.pe可以解析这个表,让你能够看到一个.exe或.dll依赖了哪些其他库的哪些函数。

4. 示例:列出DLL依赖和导入的函数

Section titled “4. 示例:列出DLL依赖和导入的函数”
// ...接上文
const imports = try file.getImports();
for (imports) |imp| {
std.debug.print("Imports from DLL: {s}\n", .{imp.name});
for (imp.functions) |func| {
if (func.name) |name| {
std.debug.print(" - Function: {s}\n", .{name});
} else {
std.debug.print(" - Ordinal: {d}\n", .{func.ordinal});
}
}
}

PE文件有一个专门的.rsrc节,用于嵌入各种资源,如图标、位图、字符串表和对话框模板。尝试解析这个资源节,找到并打印出文件中嵌入的图标或版本信息。

  • RVA(Relative Virtual Address) PE格式中广泛使用RVA。它是一个相对于文件被加载到内存中的基地址的偏移量。std.pe解析器会为你处理RVA到文件偏移量的转换,让你能够轻松地从文件字节中定位数据。

  • 重定位表(Relocation Table) 对于DLL和位置无关的可执行文件(ASLR),重定位表记录了代码中所有需要根据加载基地址进行调整的硬编码地址。链接器和加载器使用这个表来“修复”代码。

  • 数字证书 PE文件可以在其末尾附加Authenticode数字证书,以验证其发布者并确保文件未被篡改。std.pe可以定位这个证书数据,但本身不执行加密验证。

PE格式的设计与Windows加载器紧密相关,反映了其从DOS和16位Windows演化而来的历史。它的结构比ELF或Mach-O在某些方面更为复杂和特殊。

通过提供对PE、ELF和Mach-O这三种主流可执行格式的统一、高质量的原生解析支持,Zig展示了其作为构建下一代开发工具的强大潜力。一名开发者可以使用Zig编写一个单一的工具(例如,一个符号查看器或依赖分析器),该工具能够在所有主流操作系统上本地运行,并能解析所有这些平台上的二进制文件,而无需依赖外部的C库或复杂的构建设置。这在系统编程语言中是独一無二的。