这篇是PE结构系列笔记的第3篇。前两篇我们把PE文件是什么、它和进程加载之间的关系捋清楚之后,接下来就得正面碰这些头部结构了。DOS头、DOS体、NT头、文件头、可选头,这五个名词围绕着一串不算很长的二进制数据,第一次接触PE的人很容易被各种结构体缩写劝退。但这部分真不难,只要想明白一件事:Windows系统兜兜转转加载一个EXE,靠的就是这一串引导数据,一步一步找到真正要执行的代码。这篇我会先建立全局结构图,再逐个拆开讲字段含义,最后带你用十六进制编辑器对着真实文件实操一把,把纸面上那些IMAGE_XXX定义落地成你眼睛能看到、手能指到的字节。
1. PE文件的全局结构图:先把头尾关系摆清楚
1.1 为什么PE头这么“臃肿”?这是一笔兼容性历史债
PE文件头部长得啰嗦,核心原因在于历史兼容性。上世纪八九十年代,DOS下通用的可执行文件格式是MZ格式,以文件最开头的“MZ”两个字符为标志。后来Windows 3.x时代引入了NE格式(New Executable)。等到真正面向32位系统的PE格式出现时,设计者没有粗暴地把旧格式一脚踢开,而是在文件最前面保留了DOS相关的结构,再在后面藏入真正的新格式头部。于是,一个现代PE文件就自然分成了前后两段:前面是旧时代遗产(DOS头加DOS体),后面是真正管事的部分(NT头)。
记住这段来历,后面所有字段设计都能对上号。DOS头里那些看起来没用的字段、DOS体里那段老掉牙的16位代码,都不是毫无理由的占用空间,而是在延续一个兼容性设计:老系统看到文件开头是MZ,会按DOS程序处理;新系统则继续往下读,找真正属于它的NT头。理解了这一点,你再看e_lfanew字段就不会疑惑——因为NT头在文件里没有固定位置,所以必须留一个指针来告诉系统“新格式头部藏在哪”。
1.2 一个PE文件从磁盘到内存,头部结构是怎么排布的
先看磁盘上的线性布局。从文件偏移0开始,从左到右依次是:
- DOS头,固定64字节,字段定义叫IMAGE_DOS_HEADER
- DOS体又称DOS Stub,长度不固定,通常是16字节、32字节或更大
- NT头签名,4字节“PE\0\0”
- 文件头,固定20字节,字段定义叫IMAGE_FILE_HEADER
- 可选头,32位下224字节,64位下240字节
- 节表,每个节区条目40字节,数量由文件头里的NumberOfSections决定
- 节区数据,也就是.text、.data、.rsrc这些真正的代码与全局数据
注意系统加载时,第3到第5项会被当成一个整体IMAGE_NT_HEADERS看待:签名在最前面,后面紧跟着文件头和可选头两个子结构。很多人容易把“文件头”理解成“文件最开头的部分”,实际上PE语境里的文件头特指IMAGE_FILE_HEADER,它躲在DOS头和DOS Stub后面几十字节的位置。文件头负责描述文件的机器属性和总体元信息,可选头负责描述进程加载时的内存布局参数。可选头名字里带“可选”,但在正常PE文件里它必须存在,没有它,系统根本不知道程序入口在哪、该给代码分配多大空间。
提示:DOS头固定64字节,也就是十六进制的0x40。e_lfanew字段位于这个64字节结构的偏移0x3C处,正好在第60字节,后面还剩4字节。这个位置设计是算好的,方便解析程序固定读取。
2. DOS头:整个文件第一个脚印MZ
2.1 e_magic与MZ标记:第一道身份验证
用十六进制编辑器打开任意一个正常PE文件,文件最开头的两字节几乎都是4D 5A,ASCII码写作“MZ”。这是IMAGE_DOS_HEADER结构第一个成员e_magic的值,也是判断一个文件是不是PE格式的第一道门槛。系统加载文件时,会先读这2字节,如果等于0x5A4D才继续往下走,否则直接拒绝加载,弹出“不是有效的Win32应用程序”。很多PE解析脚本做的第一件事也是检查这个值,它就是整个文件的身份证号前两位。
MZ这两个字母本身是有来历的,取自早期DOS系统一位关键开发者的姓名缩写。不过对现代人来说,记住它是标志就够用了。真正想理解PE解析,你只需要形成一个条件反射:看到文件头两字节不是MZ,后面基本不用看了,八成是个纯数据文件或者已经损坏的EXE。
2.2 真正决定命运的是e_lfanew:偏移0x3C处的宝藏指针
DOS头64字节里,绝大部分字段从现代角度讲都是“历史包袱”,只有e_lfanew是新时代的关键所在。它位于DOS头相对偏移0x3C处,占4字节,是一个32位偏移量,指向文件里NT头签名的起点。系统读取这个值之后,才能跳过长度不固定的DOS Stub,准确跳到“PE\0\0”所在位置。
为什么必须靠它定位?因为DOS Stub的长度不是固定值,不同编译器生成的Stub内容可能不同,有的16字节,有的几十字节。如果没有一个固定指针,系统想找NT头就得在文件开头区域暴力扫描“PE\0\0”模式,又慢又容易误判。有了e_lfanew,整个定位过程变成三步:读MZ确认身份,去偏移0x3C读指针,再跳到指针位置校验PE签名。干净利落。
实操时,你解析任何PE文件都按这个固定套路来。以后写代码解析,也是同样的逻辑:打开文件、读头两字节、读偏移0x3C、跳到NT头。这个三步定位的套路在手工逆向、脚本批量扫描、调试器插件里,到处都是。
2.3 DOS头里的其他字段:知道它们在占位比背含义更有用
IMAGE_DOS_HEADER的完整字段有十几个,比如e_cblp、e_cp、e_cs、e_ip、e_ss、e_sp等。这些字段对应DOS加载器使用的内存分配数量、初始栈地址、初始IP等,在纯DOS环境里执行MZ程序时才起作用。现在几乎没有程序还会真去跑这套旧逻辑,所以不需要死记每个字段的定义,但要清楚两件事。
第一,它们占据了DOS头从偏移0到0x3B的区间,加起来正好0x3C字节,e_lfanew才能稳稳当当待在偏移0x3C。第二,很多加壳工具和手工修改器会利用DOS头里的空白区域填充自定义数据,因为这段空间即便被改动,也不会影响NT头定位,更不会影响代码执行。分析未知样本时,如果看到DOS头里有异常字符串或可疑字节,先别急着认为是文件损坏,很可能是有人刻意在里面藏了东西。
3. DOS体(DOS Stub):一段古老代码的现代意义
3.1 DOS Stub的组成与典型内容
紧跟在DOS头后面的就是DOS Stub,也叫DOS体。它是一段真正的16位DOS可执行代码,通常很小,作用是在老DOS环境下运行这个文件时,在屏幕上打印一行提示,然后退出。最经典的提示是“This program cannot be run in DOS mode.”,这句话伴随了无数PE文件。具体内容取决于链接器实现,有的编译工具生成的Stub非常短,有的会附带版权信息字符串,长度和样式都有差异。
为什么叫“Stub”?英文里stub有“短小碎片、存根”的意思。在Windows加载流程中,系统读到DOS头后根本不会执行它,而是直接按e_lfanew跳到NT头。这段代码更像一个占位填充物,让整个文件在旧系统眼里仍然像一个可执行的DOS程序,方便旧系统给出合理的错误提示而不是直接崩溃。
3.2 为什么现在做PE分析还要关心DOS Stub
你可能会想,这段代码既然不被Windows执行,分析它有什么用?这里有两个现实原因。
第一,它是识别工具链的一个辅助线索。DOS Stub的字节内容往往带有编译工具的特征。某些知名编译器的Stub内容会包含独特的代码模式或版权字符串,这在样本同源性比对、加壳器识别时有一定参考价值。虽然不能仅凭它锁定某个工具,但可以缩小判断范围。
第二,它是天然的“藏数据”区域。加壳工具、版本信息的额外数据、甚至捆绑器塞进去的小载荷,经常会被放在DOS Stub区域。因为它不影响NT头定位,也不影响后续节区映射,改动起来非常安全。我分析样本时遇到过有些文件在DOS Stub位置放了一段密钥常量,跟着它追下去才看懂整个解密流程。这类非标准用途的Stub内容,恰恰是手工逆向时需要留意的点。
3.3 从十六进制里快速识别DOS Stub的范围
DOS Stub没有专门的长度字段,但它的结束位置可以通过e_lfanew间接算出来。公式很简单:DOS Stub大小 = e_lfanew值 - 64。比如在偏移0x3C处读到的e_lfanew值是0x80(128),那么DOS Stub就是128减64等于64字节,也就是说文件开头0到0x7F是DOS头加DOS Stub的组合体,从0x80开始才进入NT头。
我建议你真正想理解PE,就去找个正常的小程序,用这个公式手动算一遍。算完之后再在十六进制编辑器里对照着看,你会对“DOS头后面到底跟了多长内容”有个非常直观的感觉。这个感觉比背一百遍结构体定义都有用。
4. NT头:真正管理进程的“身份证”
4.1 签名“PE\0\0”的含义与校验方法
跳到e_lfanew指向的位置,看到的前4字节通常是50 45 00 00,ASCII显示为“PE”后面跟着两个空字节,规范上称之为IMAGE_NT_SIGNATURE。这个签名的作用和DOS头里的MZ类似,就是再次确认“这确实是PE格式”,相当于给文件换发一张新身份证。系统校验MZ能过、校验PE签名也能过,才会继续解析后面的文件头和可选头。
需要注意,签名是50 45 00 00,后两字节必须为0。有些PE解析脚本如果只判断ASCII“PE”,很容易出错,因为文件正文的字符串区域里可能多处出现“PE”这两个字母。正确做法永远是根据e_lfanew跳转后再校验,而不是用文本扫描的方式去找“PE”。
4.2 IMAGE_NT_HEADERS的整体包裹结构
在代码层面,NT头通常被定义为一个大的结构体,里面包含三个成员。示意代码如下:
typedef struct _IMAGE_NT_HEADERS { DWORD Signature; // 4字节,PE\0\0 IMAGE_FILE_HEADER FileHeader; // 20字节文件头 IMAGE_OPTIONAL_HEADER OptionalHeader; // 可选头 } IMAGE_NT_HEADERS;从这里能清楚看到文件头和可选头是并列的两个分支。文件头固定20字节,可选头长度根据架构变化:32位程序为224字节,64位为240字节。三者加起来,32位PE里NT头总长度通常是4加20加224等于248字节;64位PE里则是4加20加240等于264字节。
理解了这个包裹结构,你再看各种PE解析工具打印出来的信息,会发现它们的输出顺序正是照着这个结构来的:先是签名校验结果,再是Machine、NumberOfSections这些文件头字段,然后是ImageBase、AddressOfEntryPoint这些可选头字段。结构清晰,不容易乱。
5. 文件头(IMAGE_FILE_HEADER):机器的硬件档案
5.1 Machine字段:目标CPU平台
文件头第一个成员是Machine,占2字节,描述这个可执行文件面向哪种CPU架构。最常见的两个值必须记熟:0x014C表示32位x86平台,0x8664表示x64平台。看文件头时要注意字节序,x86文件里Machine字段显示为“4C 01”,x64文件显示为“64 86”,直接读十六进制会觉得和资料里的数值对不上,其实只是小端序存储。
| Machine值 | 平台说明 |
|---|---|
| 0x014C | x86(32位) |
| 0x8664 | x64(64位) |
| 0x01C0 | ARM |
| 0xAA64 | ARM64 |
判断一个PE是32位还是64位,除了看可选头的Magic字段,先扫一眼Machine往往更快。两个字段一起看还能交叉验证,防止单改一个字段造成的伪装。
5.2 NumberOfSections:决定节表规模的关键数字
这个2字节字段告诉系统:文件里到底有多少个节区。正常可执行文件的节区数量常见在4到10之间,像.text、.rdata、.data、.reloc、.rsrc这些都是典型节区。系统解析节表时,会根据这个数字决定读取多少个IMAGE_SECTION_HEADER,每个条目40字节。对PE解析程序来说,遍历节区就靠它控制次数:从可选头结束的位置开始,连续读取NumberOfSections乘以40个字节,就得到了全部节表信息。
如果你手动修改PE文件时发现程序加载后节区数量不对,十有八九是因为这里填的数字和实际节表条目数不一致。这个字段既是数量的声明,也是解析循环的终止条件,填错会引发连锁问题。
5.3 TimeDateStamp:链接时间戳的可信度
TimeDateStamp占4字节,是从1970年1月1日0时算起的秒数,记录的是程序被链接(Link)的时间,不是源码编译时间。这个细微区分在样本分析里很有用。看到某个样本的TimeDateStamp指向好几年前,但文件行为明显是近期出现的,就要怀疑时间戳是否被人为修改过,或者整个文件是从旧模板重新打包生成的。
这个字段也常见一些“怪现象”。很多实现可重复构建的工具会把时间戳固定成一个特定值,导致不同时间生成的文件时间戳完全相同。分析时若把时间戳当成唯一证据,很容易得出错误判断。正确姿势是把它和文件内部的其他线索(比如导入表函数特征、编译工具字符串)结合着看。
5.4 SizeOfOptionalHeader:可选头的真实尺寸
2字节字段,记录可选头实际占用的字节数。32位PE这里通常填0xE0(224),64位PE填0xF0(240)。系统加载时把这个值作为跳过节表定位的依据:找到NT头之后,先读取签名4字节,再读文件头20字节,然后跳过SizeOfOptionalHeader这么多字节,就到节表位置了。所以它必须和真实可选头大小一致。
手工修改PE时这个字段特别容易出错。有人把可选头结构改了长度却忘记同步更新这个值,系统就会在错误的位置去找节表,最后整个文件根本无法加载。动手前先记录原始值,改动后要及时复查。
5.5 Characteristics:特征位小宇宙
文件头最后一个成员Characteristics,2字节,是一堆特征位的集合。每一位代表一个文件属性。实际用得多的几个:
- 0x0001:文件里没有重定位信息
- 0x0002:文件是32位程序
- 0x0020:文件是可执行文件
- 0x0100:小端序格式
- 0x2000:这是一个DLL而不是EXE
判断一个样本是DLL还是EXE,直接看这个字段的0x2000位,比看扩展名可靠得多。不少恶意程序喜欢把DLL伪装成.dat文件或者其他扩展名,扩展名骗得了人,骗不了Characteristics字段。分析样本时先读这里,能避免走弯路。
6. 可选头(IMAGE_OPTIONAL_HEADER):进程加载的灵魂参数
6.1 Magic字段:32位和64位的分水岭
可选头的第一个成员Magic,占2字节。32位PE填0x010B,64位PE填0x020B。和Machine字段一起,可以双重确认文件架构。在系统内部,加载器会根据Magic值决定剩余可选头区域按哪种布局解析。一个64位PE如果被当成32位结构去读,后面所有字段都会错位,所以解析脚本的第一步一定是先读Magic分支处理。
6.2 AddressOfEntryPoint:入口点RVA,调试器停下的第一站
AddressOfEntryPoint占4字节,是程序入口点(常说的OEP)相对于模块基址的RVA。这个值极其关键:调试器附加进程后,CPU停下来的第一条指令地址,正常情况下就是ImageBase加上这个RVA。很多加壳程序和反调试技术都会修改这个字段,让程序入口指向壳的解密代码,而不是真正的原始代码。所以脱壳时要找OEP,本质上就是在想办法恢复这个字段的原始值。
补充一个非常容易混淆的点:AddressOfEntryPoint是RVA,不是文件偏移。入口点在文件里到底位于哪个字节位置,需要根据节区信息换算,直接用十六进制在文件里搜索入口地址值常常找不到。这个坑我见过很多次,后面实操部分细说。
6.3 ImageBase、SectionAlignment、FileAlignment:三巨头组成对齐规则
这块是理解PE加载的关键。
ImageBase是程序在内存中希望加载的基地址。32位EXE默认0x00400000,64位EXE默认0x0000000140000000。DLL的默认基址通常不同,32位下常见0x10000000,64位下常见0x180000000。现代系统开启ASLR后,实际加载地址会随机化,但Original ImageBase仍是重定位计算的重要基准。
SectionAlignment是内存中每个节区的对齐粒度,常见值是0x1000,对应内存页大小4KB。FileAlignment是磁盘上每个节区数据的对齐粒度,常见值是0x200(512字节)或0x1000。这两个值决定了同一份节数据在磁盘上的偏移和在内存中的偏移往往不一样。比如某个节区在磁盘上从文件偏移0x400开始,映射到内存后可能从0x1000开始,中间相差一个对齐量。
解析节区时,偏移换算不能靠直觉直接减,必须逐节计算:文件偏移 = 入口RVA - 节区VirtualAddress + 节区PointerToRawData。这条公式会反复用到,建议直接刻进脑子里。
6.4 SizeOfImage与SizeOfHeaders:给加载器的两个容量数字
SizeOfImage占4字节,表示加载整个PE到内存后,从ImageBase开始算起的内存总大小。这个值必须是对SectionAlignment的整数倍。SizeOfHeaders是文件头、DOS头、Stub、NT头、节表加起来的总大小,通常等于第一个节区在文件中的起始偏移,常见值是0x200、0x400或0x1000。加载器要把文件开头的这一整块原样映射到内存开头,再逐个映射后面的节区,所以这两个值填错任何一个,进程内存布局都会乱套。
6.5 Subsystem与DllCharacteristics:看清程序的身份标签
Subsystem字段2字节,告诉系统这个程序适合哪种运行环境。0x0002表示Windows图形界面程序(GUI),0x0003表示Windows命令行程序(CUI)。编译时如果选错参数,GUI程序会莫名其妙弹出一个黑窗,或者命令行程序运行后没有窗口,都是Subsystem配置和实际模式不匹配造成的。
DllCharacteristics字段2字节,里面藏着一组安全特征位。0x0020表示启用ASLR地址随机化,0x0040表示启用DEP数据执行保护,0x0080表示强制使用完整性策略,0x0400表示支持SEH异常处理。分析现代编译的程序,看一眼这几位的组合,就能大概知道它开了哪些安全缓解措施。恶意样本通常会刻意关闭或保持这些标记,便于排查异常行为。
6.6 数据目录(DataDirectory):通往各类表的16扇门
可选头最后一部分是数据目录数组,固定16项,每项由RVA和Size构成,共8字节。它是PE结构里的“目录表”,系统通过它找到导入表、导出表、重定位表、异常表、资源表、证书表等。这几项最常用:
- 第0项:导出表,主要出现在DLL文件里,记录对外导出的函数列表
- 第1项:导入表,记录EXE依赖的其他DLL及函数名
- 第2项:资源表,图标、菜单、版本信息、对话框模板都在这
- 第5项:重定位表,ASLR开启或ImageBase被占用时的地址修正依据
手工逆向时不需要把16项全部背下来,但你得知道顺序和每项的8字节布局。分析一个DLL插件,优先看导出表;分析一个加载恶意载荷的EXE,优先看导入表和重定位表。数据目录数组一旦被破坏,程序往往直接报“无法定位程序输入点”之类的错误。
7. 实操:用十六进制视角手动走通一个PE的头部解析
7.1 准备一个合法的小型PE文件
建议找一个正常的命令行小工具,几十KB最好,千万别一开始就拿大型安装包练手。我自己演示用的是随手编译的一个控制台小程序,只有几KB,正好足够观察基础布局。核心思路是:文件越简单,头部字段越典型,越容易验证你的理解。
找不到合适文件的话,用记事本写一段简单代码,然后拿常见的C编译器编一个不含界面的控制台程序即可。注意编译时尽量别加额外优化参数,生成的PE会更接近教科书结构。
7.2 第一步:验证MZ并读取e_lfanew
用十六进制编辑器打开文件,第一行开头是4D 5A,确认DOS头无误。接着把视图定位到偏移0x3C(也就是文件开头第60字节),读取4字节内容。正常小工具的e_lfanew值很常见,比如80 00 00 00,按小端读法是0x80(128)。也就是说,NT头从文件偏移0x80处开始。
这一步看着简单,但它是整个手工解析流程的锚点。如果0x3C处读出来的值明显异常(比如超大或者不是常见值),就要怀疑文件是不是被改过,或者你打开的根本不是PE文件。
7.3 第二步:跳到NT头校验“PE\0\0”
既然e_lfanew是0x80,那就把十六进制编辑器的光标移到偏移0x80。正常情况下看到的是50 45 00 00,也就是ASCII“PE”加两个空字节。看到这三个字节,才算真正确认了PE格式身份。
接着,从0x84开始就是文件头区域。依次往下读:2字节Machine、2字节NumberOfSections、4字节TimeDateStamp、4字节PointerToSymbolTable、4字节NumberOfSymbols、2字节SizeOfOptionalHeader、2字节Characteristics。手动把这些字段逐个标出来,你会感受到结构体定义和真实字节之间的对应关系,比看任何教程都直观。
7.4 第三步:进入可选头,读出入口点RVA
文件头占20字节,所以可选头从0x84加20等于0x98开始。继续读可选头,先看Magic字段,如果看到0B 01,按小端就是0x010B,确认是32位PE。然后从可选头偏移0x10处(实际文件偏移0x98加0x10等于0xA8)读4字节,那就是AddressOfEntryPoint。假设读到的值是00 10 03 00,小端即0x00031000,这就是入口点RVA。
接下来要把它换算成文件偏移,就必须进入节表了。节表在可选头之后,32位下可选头224字节,所以从0x98加224等于0x178处开始。每个节表条目40字节,根据文件头里NumberOfSections的值,你可以逐个读出节区名、VirtualAddress、PointerToRawData、SizeOfRawData等关键参数。找到入口RVA落在哪个节区范围内,用公式一换算,就能定位入口指令在文件里的实际字节位置。
7.5 实践心得:手推一遍胜过背十遍
我强烈建议每个想学PE结构的人,完整手推一遍这个流程。先用十六进制编辑器把DOS头、NT头、节表都标出来,再写个十几行的解析脚本自动读一遍,最后对照现成的PE结构工具验证自己的解析结果。整个过程花不了一小时,但对结构理解的深度远超看十篇教程。我第一次手推时连续看错三个字段的偏移,把自己蠢到了,但错完就再也不会忘。踩坑有时候是最好的学习方式。
8. 常见问题与排查技巧实录
8.1 入口点地址在文件里搜不到怎么办
原因基本是把RVA当文件偏移去搜了。正确做法是先根据入口RVA找到所属节区,再用“文件偏移 = 入口RVA - 节区VirtualAddress + 节区PointerToRawData”这个公式换算。如果入口点在第一个节区之前,也就是落在头部区域,那文件偏移就等于RVA,这种情况一般出现在特殊压缩的壳里。总之遇到搜不到的情况,先检查是不是没做节区换算。
8.2 为什么PE工具显示的时间戳“不对”
不是工具算错,而是TimeDateStamp是UTC秒数,工具显示时可能按本地时区转换,也可能直接显示UTC。另外很多编译工具为了可重复构建,会把时间戳固定成一个固定值,导致大量文件的链接时间完全相同。判断样本新旧时,时间戳只能当辅助参考,不能当唯一证据。真要判断时间,可以结合文件内部字符串、编译工具版本间接推断。
8.3 32位和64位的可选头千万别互相套用
不少解析脚本偷懒只按32位结构体解析,遇到64位文件就会错位。正确做法是先读Magic字段,判断是0x010B还是0x020B,再决定按哪种布局解析后续字段。虽然二者在Subsystem之前的布局基本一致,但从数据目录开始,64位版本在某些字段的尺寸和排列上会有区别,强行套用必然读错。
8.4 文件头里NumberOfSections与节区表对不上
如果NumberOfSections填得比实际节表项大,系统就会尝试解析不存在的节表条目,轻则加载失败,重则被恶意软件利用来隐藏数据。异常样本中常见“伪节区”手法:实际数据被塞在正常节表范围之外,再通过篡改字段掩盖。分析时不要只看节区名,要结合PointerToRawData、SizeOfRawData和文件真实长度综合判断。发现文件末尾有多出来的数据区域,并且文件头字段指向可疑位置,就得提高警惕。
8.5 手工修改PE头之后打不开的几种典型原因
最常见的是SizeOfOptionalHeader没跟着改、数据目录项没清零、修改了入口点却没有同步更新SizeOfImage。我调试自写加载器时就犯过改完AddressOfEntryPoint忘了更新SizeOfImage的错,程序直接无法加载,排查了半天才发现是两个字段联动的问题。建议任何修改前先备份原始文件,改动后立刻用PE结构工具复查一遍头部字段,别嫌麻烦,这个习惯能省很多时间。
这套头部结构,网上文章一大把,但真正能让人记住的还是自己动手的那一次。我个人的习惯是每接触一个新的PE相关工具链,都会先写一个二三十行的解析脚本把头部字段打出来,跟现成的PE结构查看工具逐项核对。坚持几次以后,那些IMAGE_XXX结构体就不再是纸上定义,而是一个个有实际偏移、有字节特征的实体。下一篇可以顺着入口点往下走,把节区表映射文件到内存的那几行机制讲透。如果你卡在任何偏移计算上,拿不准就回到十六进制编辑器慢慢数,这是我试过最笨也最有效的办法。