PE 文件寻址基石:彻底理解 FOA 与 RVA 的转换
文章目录PE 文件寻址基石彻底理解 FOA 与 RVA 的转换一、先搞清楚这两个概念是干什么用的二、核心规则你唯一需要记住的物理规律三、符号定义简单清楚四、RVA → FOA 的推导从“搬家”看公式第一步数据在内存中离区段起点有多远第二步这个距离在文件里也是一样的第三步建立等式解出 data_foa五、具体例子手算一次就懂六、反向转换FOA → RVA七、直观图解从文件到内存的完整映射大图含所有偏移数据图中的关键数据解读为什么文件头是个特例八、弹簧比喻区段之间被拉伸但弹簧本身不变九、为什么不能直接让 RVA FOA十、一句话总结你可以永远记住PE 文件寻址基石彻底理解 FOA 与 RVA 的转换如果你正在学习 PE 文件结构、逆向工程或恶意软件分析你一定见过这两个词FOA和RVA。很多教程一上来就甩出公式让你死记硬背。但如果我们不理解背后的物理原因很快就会混淆。这篇博客的目标是彻底丢掉笔记从零开始建立正确的直觉。我们将用“搬书”、“搬乐高”和“弹簧拉伸”这三个比喻让你再也不会忘记 FOA 和 RVA 的关系。一、先搞清楚这两个概念是干什么用的FOAFile Offset Address数据在文件中的位置。从文件第一个字节开始数第几个字节。RVARelative Virtual Address数据在内存中的位置。从模块基址ImageBase开始算第几个字节的相对偏移。为什么同一份数据需要两套坐标因为可执行文件就像是压缩包在硬盘上为了节省空间各部分紧凑排列但当程序加载到内存中运行时CPU 要求数据必须按页对齐通常是 4KB 边界就像图书馆书架上的书必须按格子放好。这就导致同一个区段在文件中和在内存中的起点位置不同。二、核心规则你唯一需要记住的物理规律我们把可执行文件的每个区段.text、.data 等想象成一整块乐高积木。当文件加载到内存时操作系统把每一块积木原封不动地端起来放到大桌子内存的另一处。在这个过程中积木的整体位置变了从书架 A 格搬到了 B 格。但积木上任意两个小凸点之间的相对距离不变。由此得出唯一核心规则在同一个区段内部任意数据相对于区段起点的偏移量是固定不变的。三、符号定义简单清楚在开始计算前先约定几个符号符号含义section_foa区段在文件中的起始地址section_rva区段在内存中的起始地址data_foa我们要找的数据在文件中的位置data_rva我们要找的数据在内存中的位置offset数据相对于所在区段起点的距离四、RVA → FOA 的推导从“搬家”看公式第一步数据在内存中离区段起点有多远offset data_rva - section_rva这就是从“积木边缘”到“目标凸点”的距离。第二步这个距离在文件里也是一样的因为积木是整体搬运没有变形所以offset data_foa - section_foa第三步建立等式解出 data_foa既然offset相等就有data_foa - section_foa data_rva - section_rva移项得到唯一的转换公式data_foa section_foa (data_rva - section_rva)✅ 这就是RVA → FOA的全部秘密。五、具体例子手算一次就懂假设已知项目值.text区段在文件中的起点section_foa0x200.text区段在内存中的起点section_rva0x1000某条指令在内存中的地址data_rva0x1020计算过程求偏移量offset 0x1020 - 0x1000 0x20文件地址data_foa 0x200 0x20 0x220✅ 结论内存地址0x1020对应文件中的偏移0x220。六、反向转换FOA → RVA同样的逻辑解出data_rvadata_rva section_rva (data_foa - section_foa)用上面的例子验证data_rva 0x1000 (0x220 - 0x200) 0x1000 0x20 0x1020完美一致。七、直观图解从文件到内存的完整映射大图含所有偏移数据下面这张图展示了一个典型的 PE 文件在磁盘和内存中的布局。关键特征文件头部分 RVA 与 FOA 完全相等区段数据则发生平移和对齐拉伸。文件偏移(FOA) 内存偏移(RVA) 0x0000 ┌──────────────────────┐ 0x0000 ┌──────────────────────┐ │ DOS 头 │ │ DOS 头 │ │ PE 签名 │ │ PE 签名 │ │ 文件头 │ ◄──── 完全一致 ────► │ 文件头 │ │ 可选头 │ (逐字节复制) │ 可选头 │ │ 节表 │ │ 节表 │ 0x01FF ├──────────────────────┤ 0x01FF ├──────────────────────┤ │ │ │ │ │ .text 节 │ │ 【节间填充】 │ │ 代码 │ │ 0x200 ~ 0xFFF │ │ 原始大小 0x800 │ 0x1000 ├──────────────────────┤ │ │ │ .text 节 │ │ │ │ 代码 │ │ │ │ 映射后大小 0x1000 │ 0x09FF ├──────────────────────┤ 0x1FFF ├──────────────────────┤ │ │ │ │ │ .data 节 │ │ 【节间填充】 │ │ 已初始化数据 │ │ 0x2000 ~ 0x2FFF │ │ 原始大小 0x400 │ 0x2000 ├──────────────────────┤ │ │ │ .data 节 │ │ │ │ 数据 │ │ │ │ 映射后大小 0x1000 │ 0x0DFF ├──────────────────────┤ 0x2FFF ├──────────────────────┤ │ │ │ │ │ .rsrc 节 │ │ 【节间填充】 │ │ 资源 │ │ 0x3000 ~ 0x3FFF │ │ 原始大小 0x600 │ 0x3000 ├──────────────────────┤ │ │ │ .rsrc 节 │ │ │ │ 资源 │ │ │ │ 映射后大小 0x1000 │ 0x13FF ├──────────────────────┤ 0x3FFF ├──────────────────────┤ │ │ │ │ │ .reloc 节 │ │ 【节间填充】 │ │ 重定位表 │ │ 0x4000 ~ 0x4FFF │ │ 原始大小 0x200 │ 0x4000 ├──────────────────────┤ │ │ │ .reloc 节 │ │ │ │ 重定位表 │ │ │ │ 映射后大小 0x1000 │ 0x15FF └──────────────────────┘ 0x4FFF └──────────────────────┘图中的关键数据解读文件头区域FOA0x0000-0x01FF整个 PE 头部包括 DOS 头、PE 签名、文件头、可选头、节表在磁盘上是连续的。加载时这一段被原样复制到内存的0x0000-0x01FF因此RVA 始终等于 FOA。这是因为操作系统必须先在内存中读取头部才能解析节表而头部本身不涉及对齐拉伸。第一个节 .text文件起始section_foa 0x0200内存起始section_rva 0x1000必须从 4KB 边界开始文件中实际大小0x800内存中由于节对齐占用0x1000字节尾部填充0至页边界例如内存地址0x1050offset 0x50文件地址 0x0200 0x50 0x250第二个节 .data文件紧接 .text 之后0x0A00内存中由于 .text 结束于0x1FFF下一个页边界是0x2000因此 .data 从0x2000开始文件中的0x0A00~0x0DFF直接搬到0x2000~0x23FF剩余0x2400~0x2FFF为零填充第三个节 .rsrc文件0x0E00内存0x3000文件大小0x600映射到0x3000~0x35FF其余填充第四个节 .reloc文件0x1400内存0x4000文件大小0x200映射到0x4000~0x41FF为什么文件头是个特例因为 PE 头部本身就是控制信息加载器在读取时并不移动它而是直接从文件偏移 0 开始映射到内存地址 0相对基址。头部中的节表又指明了每个节应该被映射到哪个 RVA此时头部已经占用了0x00000x01FF的区域所以第一个节的 RVA 至少要从0x1000开始满足页对齐从而产生了 FOA 和 RVA 的分道扬镳。八、弹簧比喻区段之间被拉伸但弹簧本身不变文件中的区段是紧挨着的内存中因为对齐要求区段之间被塞入空白就像一条弹簧被拉开。但是弹簧本身的每一圈区段内部数据纹理不变相对位置没有改变。文件紧凑 [.text][.data][.rsrc][.reloc] ↑ ↑ ↑ ↑ 紧挨着 ↓ 加载时因页对齐拉伸 ↓ 内存拉伸 [.text]....[.data]....[.rsrc]....[.reloc] ↑ ↑ ↑ ↑ ↑ ↑ ↑ 起点 空白 起点 空白 起点 空白 起点核心结论✅ 弹簧圈内部没有拉伸偏移量不变 →FOA - section_foa RVA - section_rva✅ 弹簧圈之间被拉伸插入空白填充不能直接套用同一个公式九、为什么不能直接让 RVA FOA因为除了文件头区域外节数据的起点被平移了。如果强行认为所有数据 RVA 都等于 FOA实际访问的内存位置就会完全错误程序立刻崩溃。你可以这么记FOA 文件老家地址 (内存新家地址 - 内存老家地址)本质上就是先在内存里找到离家多远的偏移再把这个偏移套用到文件里的家。十、一句话总结你可以永远记住区段整体搬家内部顺序不变。先算离家多远再在新家找到位置。下次在十六进制编辑器或调试器中碰到 PE 地址转换时闭上眼睛回想那张完整的映射图头部文件什么样内存就什么样RVAFOA。节区每个节都被端起来放到了 4KB 对齐的新位置内部偏移不变节间被拉大。记住这个画面公式就会自然浮现在你脑海中。希望这篇博客能帮你彻底建立起 FOA 与 RVA 的直觉。如果还有模糊的地方不妨回到那张“文件→内存全貌图”再想一次。