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 中的加载位置差异:
4.2 物理地址与虚拟地址的映射
在解压缩阶段,MMU(内存管理单元)通常处于关闭状态。这意味着此时 CPU 访问的所有地址都是物理地址,不存在虚拟地址到物理地址的转换。然而,内核映像本身在编译时使用的是虚拟地址空间(链接地址),这就产生了地址映射的需求。
下表总结了在解压缩阶段,不同地址类型的含义与使用方式:
| 地址类型 | 定义 | 在解压缩阶段的使用 |
|---|---|---|
| 物理地址(PA) | CPU 地址总线上的实际地址,直接对应 RAM 或外设的物理位置。 | 解压缩代码实际运行和访问内存时使用的地址。 |
| 链接地址(VA) | 编译器和链接器在生成内核映像时使用的地址,通常位于内核虚拟地址空间(如 0xC0008000)。 |
仅作为符号表中的引用地址,在运行时需要通过重定位表修正为物理地址。 |
| 加载地址 | Bootloader 将内核映像复制到 RAM 中的实际物理地址。 | 解压缩代码的入口点,也是重定位计算的基准地址。 |
下图展示了物理地址与虚拟地址在解压缩阶段的映射关系:
4.3 解压缩前的内存区域分布
在解压缩代码开始执行时,RAM 中已经包含了多个关键区域。这些区域由 Bootloader 和内核映像本身共同建立,解压缩代码必须清楚每个区域的用途和边界,以避免数据覆盖。
- Bootloader 保留区:位于 RAM 起始地址,包含 Bootloader 自身代码、设备树 Blob(DTB)和启动参数。
- 内核映像区:压缩后的内核映像(zImage 或 Image.gz)所在位置,由 Bootloader 加载。
- 解压缩输出区:解压缩后的内核映像将被写入的区域,通常位于内核映像区之后或之前,需确保不重叠。
- 栈与临时数据区:解压缩代码使用的栈空间和临时缓冲区,通常位于内核映像区附近。
- 设备树 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 | 解压缩代码运行时使用的栈和临时缓冲区 |
下图以图形方式展示了上述内存布局:
4.4 地址重定位的关键步骤
当 CONFIG_RELOCATABLE 启用时,解压缩代码在开始解压缩之前会执行以下地址重定位步骤:
- 获取当前物理地址:通过读取程序计数器(PC)或链接寄存器(LR)的值,获取当前执行指令的物理地址。
- 计算偏移量:将当前物理地址与链接地址相减,得到重定位偏移量
offset = current_PA - link_VA。 - 修正全局符号:遍历重定位表(通常位于
.rel.dyn或.rela.dyn段),将每个需要重定位的符号地址加上偏移量。 - 修正函数指针:更新所有绝对函数指针和全局变量指针,使其指向正确的物理地址。
- 设置栈指针:将栈指针(SP)调整为基于当前物理地址的值,确保栈操作不会访问到错误的内存区域。
adrp 和 add 指令实现相对寻址,从而避免在运行时进行大量的重定位表遍历。只有在需要访问绝对地址(如设备树指针)时,才进行显式的地址修正。
下图展示了地址重定位的完整流程:
4.5 常见问题与避坑指南
当 CONFIG_RELOCATABLE 未启用时,如果 Bootloader 将内核映像加载到非链接地址,解压缩代码会尝试从错误的地址读取数据,导致解压缩失败或系统崩溃。解决方案是确保 Bootloader 的加载地址与内核的链接地址一致,或者启用 CONFIG_RELOCATABLE。
如果解压缩输出缓冲区与压缩内核映像所在区域重叠,解压缩过程会覆盖尚未读取的压缩数据,导致数据损坏。解压缩代码必须通过 decompress_kernel() 函数中的安全检查来避免这种情况,通常要求输出缓冲区起始地址高于输入缓冲区结束地址。
// 伪代码示例:在重定位后打印调试信息
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);
// ... 继续执行解压缩
}