必须先读取DOS头获取e_lfanew值定位PE签名,再根据Magic字段区分32/64位解析可选头,节表数量由NumberOfSections决定,结构体需[StructLayout(Sequential, Pack=1)]且字段类型严格匹配,RVA需转文件偏移。

用 System.IO.FileStream 读取 PE 头前必须跳过 DOS 头
PE 文件开头是 DOS 头(IMAGE_DOS_HEADER),它占 64 字节,但真正关键的 PE 签名("PE\0\0")在偏移 0x3C 处的 e_lfanew 字段指向的位置。直接从文件头开始解析会失败。
- 先读取前 64 字节,解析出
e_lfanew字段(4 字节小端整数) - 用该值作为偏移,定位到 PE 签名位置;若此处不是
0x00004550(即 "PE\0\0"),说明不是合法 PE 文件 - DOS stub(DOS 占位程序)可能很长,但 PE 头永远从
e_lfanew指向的位置开始,不是固定偏移0x40
解析 IMAGE_FILE_HEADER 和 IMAGE_OPTIONAL_HEADER 要区分 32/64 位
PE 文件可选头有两种:32 位用 IMAGE_OPTIONAL_HEADER32(共 224 字节),64 位用 IMAGE_OPTIONAL_HEADER64(共 240 字节)。仅靠文件扩展名(.exe/.dll)无法判断,必须读 IMAGE_FILE_HEADER.Machine 和可选头中的 Magic 字段。
-
IMAGE_FILE_HEADER.Machine是0x014C(I386)或0x8664(AMD64)等,但不决定可选头大小 - 真正决定用哪个可选头的是紧接在
IMAGE_FILE_HEADER后的 Magic 值:0x010B表示 32 位,0x020B表示 64 位 - 忽略这个判断,硬按 32 位读 64 位文件,会导致后续所有字段错位(比如
AddressOfEntryPoint解析成错误地址)
节表(Section Headers)数量由 NumberOfSections 决定,不能硬写死
节表紧跟在可选头之后,每个节头固定 40 字节(IMAGE_SECTION_HEADER),但节数量由 IMAGE_FILE_HEADER.NumberOfSections 给出——这个值通常为 3~6,但某些加壳或手工构造的 PE 可能只有 1 节或多达 10+ 节。
- 用
NumberOfSections * 40计算节表总长度,再用FileStream.Seek()跳转到对应位置 - 常见错误:假设只有 .text/.rdata/.data 三节,漏读 .reloc 或 .rsrc,导致资源或重定位信息丢失
- 节名是 8 字节 ASCII(如
".text\0\0\0"),需用Encoding.ASCII.GetString()并截断 \0,不能直接ToString()
用 Marshal.PtrToStructure 解析结构体时注意字节对齐和字段顺序
C# 默认结构体布局是 Auto,会重排字段顺序并插入填充,而 PE 结构体是 C 风格的 Sequential 布局、1 字节对齐。不显式声明,解析结果全是错的。
- 每个结构体必须加
[StructLayout(LayoutKind.Sequential, Pack = 1)] - 字段类型要严格匹配:比如
WORD→UInt16,DWORD→UInt32,PVOID在 64 位 PE 中是UInt64,不是IntPtr - 避免用
string直接映射字符数组;对IMAGE_DOS_HEADER.e_lfanew这种 4 字节字段,必须用UInt32,不能用Int32(符号扩展会污染高位)
最易被忽略的是:PE 头里大量字段是相对虚拟地址(RVA),不是文件偏移。比如 OptionalHeader.AddressOfEntryPoint 是 RVA,要查节表才能换算成实际文件位置——这个转换逻辑一旦写错,整个反汇编或资源提取就全偏了。








