4. 解压缩前的内存布局

在解压缩代码执行之前,内核映像已经在 RAM 中占据了一个特定的内存布局。理解这个布局是分析地址重定位与校验流程的基础。本章节将详细解析解压缩代码的加载地址(CONFIG_RELOCATABLE 的影响)、物理地址与虚拟地址的映射关系,以及此时内存中各区域的分布情况。

4.1 加载地址与 CONFIG_RELOCATABLE

内核在 RAM 中的加载地址由配置选项 CONFIG_RELOCATABLE 决定。该选项控制内核映像是否可以在任意物理地址加载,而非固定在编译时指定的链接地址。

配置选项 行为描述 典型应用场景
CONFIG_RELOCATABLE=n 内核必须加载到编译时指定的链接地址(例如 0x80000)。如果加载地址不匹配,系统将无法启动。 早期嵌入式系统、固定内存映射的 SoC。
CONFIG_RELOCATABLE=y 内核可以在任意物理地址加载。解压缩代码会通过自相对寻址和重定位表,将自身调整到实际加载位置。 通用 Linux 发行版、支持 KASLR 的安全系统。
关键概念:当 CONFIG_RELOCATABLE 启用时,解压缩代码的入口点(start)会首先计算当前物理地址与链接地址的偏移量,然后使用该偏移量修正所有绝对地址引用。这是实现内核地址空间布局随机化(KASLR)的基础。

下图展示了 CONFIG_RELOCATABLE 启用与禁用时,内核映像在 RAM 中的加载位置差异:

CONFIG_RELOCATABLE 对加载地址的影响 CONFIG_RELOCATABLE=n 内核映像(固定地址) 链接地址 = 加载地址 = 0x80000 ✗ 地址不可变 ✗ 无法支持 KASLR ✗ 加载地址必须严格匹配 CONFIG_RELOCATABLE=y 内核映像(任意地址) 加载地址 = 0x100000(示例) ✓ 地址可动态计算 ✓ 支持 KASLR ✓ 加载地址灵活

4.2 物理地址与虚拟地址的映射

在解压缩阶段,MMU(内存管理单元)通常处于关闭状态。这意味着此时 CPU 访问的所有地址都是物理地址,不存在虚拟地址到物理地址的转换。然而,内核映像本身在编译时使用的是虚拟地址空间(链接地址),这就产生了地址映射的需求。

重要提示:在解压缩代码执行期间,MMU 尚未启用。所有对内存的访问(包括读取内核映像、写入解压缩输出、访问设备寄存器)都直接使用物理地址。链接地址(虚拟地址)仅在代码中作为符号引用存在,需要通过自相对寻址或位置无关代码(PIC)技术来修正。

下表总结了在解压缩阶段,不同地址类型的含义与使用方式:

地址类型 定义 在解压缩阶段的使用
物理地址(PA) CPU 地址总线上的实际地址,直接对应 RAM 或外设的物理位置。 解压缩代码实际运行和访问内存时使用的地址。
链接地址(VA) 编译器和链接器在生成内核映像时使用的地址,通常位于内核虚拟地址空间(如 0xC0008000)。 仅作为符号表中的引用地址,在运行时需要通过重定位表修正为物理地址。
加载地址 Bootloader 将内核映像复制到 RAM 中的实际物理地址。 解压缩代码的入口点,也是重定位计算的基准地址。

下图展示了物理地址与虚拟地址在解压缩阶段的映射关系:

解压缩阶段物理地址与虚拟地址映射(MMU 关闭) 物理地址空间 内核映像(实际位置) 物理地址:0x100000 - 0x1FFFFF 解压缩输出缓冲区 物理地址:0x200000 - 0x2FFFFF 虚拟地址空间(编译时) 内核映像(链接地址) 虚拟地址:0xC0008000 - 0xC00FFFFF 解压缩输出缓冲区 虚拟地址:0xC0100000 - 0xC01FFFFF MMU 关闭 无映射 MMU 关闭 无映射

4.3 解压缩前的内存区域分布

在解压缩代码开始执行时,RAM 中已经包含了多个关键区域。这些区域由 Bootloader 和内核映像本身共同建立,解压缩代码必须清楚每个区域的用途和边界,以避免数据覆盖。

典型内存布局(以 ARM64 为例):
  1. Bootloader 保留区:位于 RAM 起始地址,包含 Bootloader 自身代码、设备树 Blob(DTB)和启动参数。
  2. 内核映像区:压缩后的内核映像(zImage 或 Image.gz)所在位置,由 Bootloader 加载。
  3. 解压缩输出区:解压缩后的内核映像将被写入的区域,通常位于内核映像区之后或之前,需确保不重叠。
  4. 栈与临时数据区:解压缩代码使用的栈空间和临时缓冲区,通常位于内核映像区附近。
  5. 设备树 Blob(DTB)区:由 Bootloader 传递的硬件描述信息,解压缩代码需要将其保留并传递给最终内核。

下表列出了这些区域在典型 32 位 ARM 系统中的地址范围示例:

区域名称 起始地址 大小 内容说明
Bootloader 保留区 0x00000000 64 KB Bootloader 代码、参数、页表预留空间
设备树 Blob(DTB) 0x00010000 128 KB 由 Bootloader 填充的硬件描述数据
压缩内核映像 0x00080000 2 MB zImage 或 Image.gz,包含解压缩代码
解压缩输出缓冲区 0x00280000 8 MB 解压缩后的原始内核映像(vmlinux)
栈与临时数据 0x00A80000 256 KB 解压缩代码运行时使用的栈和临时缓冲区

下图以图形方式展示了上述内存布局:

解压缩前 RAM 内存布局示例(ARM32) Bootloader 保留区 (0x00000000 - 0x0000FFFF) 设备树 Blob (DTB) (0x00010000 - 0x0002FFFF) 压缩内核映像 (zImage) (0x00080000 - 0x0027FFFF) 包含解压缩代码 + 压缩数据 解压缩输出缓冲区 (0x00280000 - 0x00A7FFFF) 解压缩后的原始内核映像 (vmlinux) 栈与临时数据区 (0x00A80000 - 0x00ABFFFF) 剩余可用 RAM (0x00AC0000 - 结束) 用于后续内核初始化、页表、动态分配 0x00000000 0x00500000 0x00FFFFFF

4.4 地址重定位的关键步骤

当 CONFIG_RELOCATABLE 启用时,解压缩代码在开始解压缩之前会执行以下地址重定位步骤:

  1. 获取当前物理地址:通过读取程序计数器(PC)或链接寄存器(LR)的值,获取当前执行指令的物理地址。
  2. 计算偏移量:将当前物理地址与链接地址相减,得到重定位偏移量 offset = current_PA - link_VA。
  3. 修正全局符号:遍历重定位表(通常位于 .rel.dyn 或 .rela.dyn 段),将每个需要重定位的符号地址加上偏移量。
  4. 修正函数指针:更新所有绝对函数指针和全局变量指针,使其指向正确的物理地址。
  5. 设置栈指针:将栈指针(SP)调整为基于当前物理地址的值,确保栈操作不会访问到错误的内存区域。
性能优化:在 ARM64 架构中,解压缩代码通常使用位置无关代码(PIC)技术,通过 adrp 和 add 指令实现相对寻址,从而避免在运行时进行大量的重定位表遍历。只有在需要访问绝对地址(如设备树指针)时,才进行显式的地址修正。

下图展示了地址重定位的完整流程:

地址重定位流程 1. 获取当前 PC 读取物理地址 2. 计算偏移量 PA - VA = offset 3. 遍历重定位表 修正全局符号 4. 修正 指针 5. 设置栈指针 SP = 物理地址 + 偏移 6. 开始解压缩 调用 decompress_kernel() 重定位完成后的状态 所有代码和数据的物理地址已修正,栈指针指向安全区域, 解压缩代码可以安全地访问内核映像中的任何符号。

4.5 常见问题与避坑指南

常见错误 1:加载地址与链接地址不匹配导致解压缩失败

当 CONFIG_RELOCATABLE 未启用时,如果 Bootloader 将内核映像加载到非链接地址,解压缩代码会尝试从错误的地址读取数据,导致解压缩失败或系统崩溃。解决方案是确保 Bootloader 的加载地址与内核的链接地址一致,或者启用 CONFIG_RELOCATABLE。

常见错误 2:解压缩输出缓冲区与输入缓冲区重叠

如果解压缩输出缓冲区与压缩内核映像所在区域重叠,解压缩过程会覆盖尚未读取的压缩数据,导致数据损坏。解压缩代码必须通过 decompress_kernel() 函数中的安全检查来避免这种情况,通常要求输出缓冲区起始地址高于输入缓冲区结束地址。

调试技巧:在解压缩代码中添加早期串口输出(early printk)可以帮助定位地址重定位问题。在重定位完成后,打印当前物理地址、链接地址和偏移量,验证计算是否正确。例如:
// 伪代码示例:在重定位后打印调试信息
void relocate_kernel(void) {
    unsigned long pa = get_current_physical_address();
    unsigned long va = KERNEL_LINK_ADDR;
    unsigned long offset = pa - va;
    early_printf("PA: 0x%lx, VA: 0x%lx, Offset: 0x%lx\n", pa, va, offset);
    // ... 继续执行解压缩
}