🔗 .data.bimg.rel.ro 段深度解析

Boot Image Relocation Read-Only Data Section

🎯 核心概念

.data.bimg.rel.ro 是 boot.oat 文件中的一个特殊段,全称为 "Boot Image Relocation Read-Only Data"(启动镜像重定位只读数据段)。

它的核心作用是:存储 boot.oat 中所有指向 boot.art 的指针引用表, 这些指针在运行时需要根据 boot.art 的实际加载地址进行重定位(地址修正)。

❓ 为什么需要 .data.bimg.rel.ro 段

1. 地址空间独立性问题

编译 boot.oat 时,编译器无法预知 boot.art 在运行时会被加载到哪个虚拟地址。 每台设备、每次启动,boot.art 的加载地址可能都不同(受 ASLR - 地址空间布局随机化影响)。

📌 关键问题
boot.oat 中的机器码需要访问 boot.art 中的对象(类、字符串、静态字段等), 但编译时不知道这些对象的最终地址,因此必须预留"空位",在加载时填入真实地址。

2. 编译时 vs 运行时地址

📝 编译时

假设地址

0x10000000

编译器使用的临时基地址

🚀 运行时

实际地址

0x70A3F000

mmap 实际分配的地址

⚠️ 需要修正增量

Patch Delta = 0x70A3F000 - 0x10000000 = 0x60A3F000

所有指针都需要加上这个增量才能指向正确的地址!

3. 只读代码段的限制

boot.oat 的 .text 段(代码段)是只读可执行的(R-X), 出于安全考虑(防止代码注入攻击),运行时无法直接修改代码段中的指令。

🚫 设计约束
不能将指向 boot.art 的指针直接嵌入 .text 段的机器码中,因为无法在运行时修改这些指令。

解决方案:将所有需要重定位的指针集中存储在一个可修改的数据段中 — 这就是 .data.bimg.rel.ro

⚙️ 工作原理

boot.oat 内部结构与指针引用关系
boot.oat 文件
ELF 可执行文件
.text 段
代码段
权限: R-X (只读)
内容: ARM64 机器码
大小: 30-50MB
.data.bimg.rel.ro
指针引用表 ⭐
权限: RW- (可写)
内容: 指向 boot.art 的指针
大小: 2-5MB
.rodata 段
只读数据
权限: R-- (只读)
内容: 常量、跳转表
大小: 5-10MB
间接寻址访问机制
// 代码段中的指令 ldr x0, [ptr_table + #0] ← 从指针表读取地址 ldr x1, [x0] ← 通过地址访问对象 // ptr_table 位于 .data.bimg.rel.ro 段
boot.art
堆快照文件
实际对象地址: 0x70A3F000
包含: 类对象、字符串、静态字段
大小: 40-80MB

核心机制:间接寻址

  1. 编译阶段:
    • dex2oat 将所有需要引用 boot.art 的位置记录下来
    • 在 .data.bimg.rel.ro 段预留指针槽位(初始值为相对偏移)
    • .text 段的代码通过偏移量引用这些槽位
  2. 加载阶段:
    • mmap 加载 boot.art 到地址 BASE_ADDR
    • 遍历 .data.bimg.rel.ro 中的所有指针槽位
    • 将每个相对偏移转换为绝对地址:绝对地址 = BASE_ADDR + 偏移量
  3. 执行阶段:
    • 代码访问对象时,先从 .data.bimg.rel.ro 读取指针
    • 使用该指针间接访问 boot.art 中的实际对象

🔄 重定位过程详解

完整流程

1
mmap 加载 boot.art
分配地址: 0x70A3F000
2
dlopen 加载 boot.oat
.text 段映射到: 0x7F000000 (R-X)
.data.bimg.rel.ro 映射到: 0x7F800000 (RW-)
3
读取 Image Header
patch_delta = 实际地址 - 编译地址
= 0x70A3F000 - 0x10000000 = 0x60A3F000
4
遍历 .data.bimg.rel.ro
for each pointer in section:
    *pointer += patch_delta
5
标记为只读 (可选)
mprotect(.data.bimg.rel.ro, PROT_READ)
6
完成,Zygote 就绪
开始监听 socket,等待 fork 子进程请求

关键代码示例

// art/runtime/image.cc - 重定位过程 uint8_t* image_base = 0x70A3F000; // boot.art 实际加载地址 // 修正 .data.bimg.rel.ro 中的指针 uint64_t* ptr_slot = string_class_ptr; *ptr_slot = image_base + 0x5000; // 0x70A3F000 + 0x5000 = 0x70A44000 // 现在 string_class_ptr 槽位存储的是绝对地址 0x70A44000 // 代码执行时可以直接访问这个地址

📊 数据结构

.data.bimg.rel.ro 段的内容

引用类型 描述 数量级
Class Pointers 指向 boot.art 中类对象的指针 ~3,000-5,000
String Pointers 指向字符串常量池的指针 ~20,000-40,000
Static Field Pointers 指向静态字段存储位置的指针 ~5,000-10,000
ArtMethod Pointers 指向方法元数据的指针 ~10,000-20,000
DexCache Pointers 指向 DEX 文件缓存的指针 ~100-200

⚡ 性能影响分析

✅ 优势

❌ 劣势

❓ 常见问题

Q1: 为什么不在编译时就生成绝对地址?
答:因为无法预知运行时的加载地址。现代操作系统使用 ASLR(地址空间布局随机化) 来增强安全性,每次启动时内存布局都会随机化。即使不考虑 ASLR,不同设备的内存配置也不同, 无法保证同一地址总是可用。
Q2: 为什么重定位后不把这个段设为只读(RO)?
答:实际上在某些实现中会这样做。重定位完成后,通过 mprotect() 将 .data.bimg.rel.ro 设置为只读,防止运行时意外修改这些关键指针。这也是命名中 "ro" 的由来。
Q3: 间接寻址不会显著降低性能吗?
答:影响有限。现代 CPU 的分支预测和预取机制可以很好地处理这种模式。 而且由于指针表是连续存储的,访问时有很好的空间局部性,缓存命中率高。 相比于节省的类加载时间(100-200ms),这点开销(每次访问 1-2 周期)是可以接受的。
Q4: 如果手动修改 .data.bimg.rel.ro 会怎样?
答:灾难性后果!修改这些指针会导致:
  • 访问错误的内存地址 → Segmentation Fault
  • 类型混淆 → 运行时崩溃
  • 静态字段错乱 → 数据损坏
  • 可能导致整个系统无法启动

📝 总结

核心要点

  1. .data.bimg.rel.ro 是 boot.oat 中存储指向 boot.art 的指针引用表
  2. 解决了编译时无法确定 boot.art 运行时加载地址的问题
  3. 通过间接寻址机制,代码通过指针表访问 boot.art 中的对象
  4. 在系统启动时进行一次性重定位,修正所有指针为绝对地址
  5. 权衡了安全性(代码段不可写)和灵活性(支持 ASLR)
  6. 虽然有间接寻址开销,但相比整体性能提升(避免类加载),代价很小
🎓 深入学习资源
  • AOSP 源码: art/runtime/image.cc
  • AOSP 源码: art/compiler/image_writer.cc
  • AOSP 源码: art/runtime/oat_file.cc
  • 论文: "ART: Android Runtime" (Google I/O 2014)

📚 深度技术解析 • 基于 Android 14 AOSP 源码