PE 文件寻址基石:彻底理解 FOA 与 RVA 的转换

文章目录

PE 文件寻址基石:彻底理解 FOA 与 RVA 的转换

如果你正在学习 PE 文件结构、逆向工程或恶意软件分析,你一定见过这两个词:FOARVA。很多教程一上来就甩出公式,让你死记硬背。但如果我们不理解背后的物理原因,很快就会混淆。

这篇博客的目标是:彻底丢掉笔记,从零开始建立正确的直觉。 我们将用"搬书"、"搬乐高"和"弹簧拉伸"这三个比喻,让你再也不会忘记 FOA 和 RVA 的关系。


一、先搞清楚这两个概念是干什么用的

FOA(File Offset Address)

数据在文件中的位置。从文件第一个字节开始数,第几个字节。

RVA(Relative 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_foa 0x200
.text 区段在内存中的起点 section_rva 0x1000
某条指令在内存中的地址 data_rva 0x1020

计算过程:

  1. 求偏移量:offset = 0x1020 - 0x1000 = 0x20
  2. 文件地址:data_foa = 0x200 + 0x20 = 0x220

✅ 结论:内存地址 0x1020 对应文件中的偏移 0x220


六、反向转换:FOA → RVA

同样的逻辑,解出 data_rva

复制代码
data_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 └──────────────────────┘

图中的关键数据解读:

  • 文件头区域(FOA 0x0000 - 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 地址转换时,闭上眼睛回想那张完整的映射图:

  • 头部:文件什么样,内存就什么样,RVA=FOA
  • 节区:每个节都被端起来放到了 4KB 对齐的新位置,内部偏移不变,节间被拉大

记住这个画面,公式就会自然浮现在你脑海中。


希望这篇博客能帮你彻底建立起 FOA 与 RVA 的直觉。如果还有模糊的地方,不妨回到那张"文件→内存全貌图"再想一次。

相关推荐
AI大佬的小弟1 个月前
提示词工程(1)---提示词工程简介
提示词·pe·模型幻觉·大模型基础·提示词工程简介·提示词工程零基础学习·系统性学习提示词工程
浩浩测试一下2 个月前
PE结构 ---> 9.RvaToFoa 内存状体到文件状态
pe·va·pe壳·foatorva·rva·foa
Joy T2 个月前
【PE 实践】从“写提示词”到“构建高可用大模型系统”
prompt·pe·提示词工程·few shot
浩浩测试一下3 个月前
PE结构 ----> PE结构基础知识点汇总(与安全开发关联)
安全·网络安全·pe·windowspe·pe基础格式
阿杰学AI5 个月前
AI核心知识56——大语言模型之ToT(简洁且通俗易懂版)
人工智能·ai·语言模型·提示工程·tot·pe·思维树
阿杰学AI5 个月前
AI核心知识53——大语言模型之Structured CoT 超级模版(简洁且通俗易懂版)
人工智能·ai·语言模型·prompt·提示词·pe·structured cot
阿杰学AI5 个月前
AI核心知识54——大语言模型之Structured CoT(简洁且通俗易懂版)
人工智能·ai·语言模型·prompt·pe·结构化提示词·structured cot
百里香酚兰1 年前
【AI学习笔记】Coze工作流写入飞书多维表格(即:多维表格飞书官方插件使用教程)
笔记·学习·大模型·飞书·pe·coze
百里香酚兰1 年前
【AI学习笔记】Coze平台实现将Excel文档批量导入数据库全过程
人工智能·笔记·大模型·aigc·工作流·pe·coze