2. 内核解压与入口点分析:zImage/Image的生成过程、解压代码位置与汇编入口

嵌入式 Linux 内核的启动从引导加载程序(Bootloader)将内核映像加载到内存开始。但实际执行的第一条指令并不在 C 语言编写的 start_kernel 中,而是位于一个精心设计的汇编解压程序里。理解 zImage 与 Image 的生成差异、解压代码的链接位置以及汇编入口的约定,是分析内核启动日志、定位早期崩溃的关键能力。

2.1 Image 与 zImage:两种内核映像的生成链路

内核编译的最终产物通常有两种形式:未压缩的 Image 和自解压的 zImage(或 uImage)。它们的生成流程在 Makefile 中严格区分。

映像类型 生成目标 核心 Makefile 规则 体积特征 启动方式
Image arch/arm/boot/Image 直接链接 vmlinux,去除调试段,输出纯二进制 较大(通常 4~8 MB) Bootloader 直接跳转到入口,无解压阶段
zImage arch/arm/boot/zImage 先压缩 Image,再链接解压桩(decompressor) 较小(通常 1.5~3 MB) 解压桩先运行,解压完成后跳转到解压后的内核
uImage arch/arm/boot/uImage 在 zImage 前添加 U-Boot 头部(64 字节) 与 zImage 相近 U-Boot 解析头部后跳转到 zImage 入口
调优要点: 在调试早期启动阶段,优先使用 Image 可以跳过解压环节,直接通过 JTAG 或仿真器设置断点在 stext 处。生产环境则必须使用 zImage 以节省存储空间和 Flash 读取时间。

2.2 zImage 的生成过程:压缩 + 解压桩链接

zImage 的构建并非简单压缩,而是将压缩后的内核数据与一段独立的解压程序(decompressor)打包在一起。其 Makefile 核心逻辑位于 arch/arm/boot/compressed/Makefile。

# arch/arm/boot/compressed/Makefile (简化示意)
# 1. 编译解压桩目标文件
obj-y += head.o misc.o decompress.o string.o

# 2. 链接生成 vmlinux(解压桩的ELF)
SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/; s/LOAD_ADDR/$(ZRELADDR)/
$(obj)/vmlinux: $(obj)/head.o $(obj)/piggy.o $(obj)/misc.o \
                $(obj)/decompress.o $(obj)/string.o FORCE
    $(call if_changed,ld)
    @$(OBJCOPY) -O binary -R .note -R .comment $@ $@.bin

# 3. piggy.o 包含压缩后的内核数据
$(obj)/piggy.o: $(obj)/piggy_data $(obj)/piggy.S FORCE
    $(call if_changed,as)

# 4. 最终生成 zImage
$(obj)/zImage: $(obj)/vmlinux.bin FORCE
    $(call if_changed,gzip)
关键变量:
  • ZTEXTADDR:解压代码的链接起始地址,通常为 0x00008000 或 0x30008000,由平台配置。
  • ZRELADDR:解压后内核的预期运行地址,即 Image 的链接地址。
  • piggy_data:对 Image 进行 gzip 压缩后的数据。
zImage 生成流程(Makefile 视角) vmlinux (ELF) objcopy 去段 Image (纯二进制) gzip 压缩 piggy_data head.o / misc.o / decompress.o 链接 (ZTEXTADDR) zImage (自解压内核) 解压桩 + 压缩数据 = zImage

2.3 解压代码位置与汇编入口:head.S 分析

zImage 的入口点位于 arch/arm/boot/compressed/head.S。当 Bootloader 跳转到 zImage 的起始地址时,CPU 首先执行这段汇编代码。其核心职责是:设置解压环境、解压内核、跳转到解压后的内核入口。

2.3.1 入口宏与启动模式检测

@ arch/arm/boot/compressed/head.S (ARMv7 典型入口)
.section ".start", "ax"

    .type   _start, #function
    .globl  _start
_start:
    @ 保存启动参数 (r1 = machine ID, r2 = ATAGs / DTB 地址)
    mov     r7, r1          @ 保存 machine ID
    mov     r8, r2          @ 保存 tags 地址

    @ 检查是否支持当前 CPU 架构
    mrc     p15, 0, r9, c0, c0      @ 读 CPU ID 寄存器
    bl      __lookup_processor_type
    teq     r10, #0
    beq     __error_p               @ 不支持的 CPU 则死循环

    @ 检查是否支持当前机器类型 (仅非 DT 启动)
    bl      __lookup_machine_type
    teq     r8, #0
    beq     __error_a               @ 不支持的机器则死循环
常见启动失败点: 如果 Bootloader 传递的 machine ID 与内核编译时支持的列表不匹配,CPU 会在 __error_a 处进入死循环。此时串口无任何输出,需要通过 JTAG 或 LED 指示灯定位。

2.3.2 解压地址重定位与缓存使能

解压代码需要确保目标地址(ZRELADDR)与当前运行地址不重叠,否则会覆盖自身。head.S 通过计算当前 PC 与链接地址的偏移来决定是否需要重定位。

@ 判断是否需要自搬移
    adr     r0, _start              @ 当前运行地址
    ldr     r1, =_start             @ 链接地址 (ZTEXTADDR)
    subs    r0, r1, r0              @ 计算偏移
    beq     not_relocated           @ 偏移为0,无需搬移

    @ 需要重定位:将自身复制到链接地址
    ldr     r2, =_end               @ 解压桩结束地址
    add     r2, r2, r0              @ 计算源结束地址
    bl      memmove                 @ 搬移自身

not_relocated:
    @ 使能缓存,加速解压
    mrc     p15, 0, r0, c1, c0, 0   @ 读 SCTLR
    bic     r0, r0, #2              @ 清 A 位 (对齐错误)
    orr     r0, r0, #0x1000         @ 置 I 位 (指令缓存)
    mcr     p15, 0, r0, c1, c0, 0   @ 写 SCTLR

2.3.3 调用解压函数并跳转到内核

@ 准备解压参数
    ldr     r0, =_input_data        @ 压缩数据起始地址
    ldr     r1, =_input_data_end    @ 压缩数据结束地址
    ldr     r2, =_output_data       @ 解压目标地址 (ZRELADDR)
    mov     r3, #0                  @ 解压标志 (0: gzip)

    @ 调用 C 语言解压函数
    bl      decompress_kernel

    @ 解压完成,跳转到解压后的内核入口
    @ 内核入口在解压后的 Image 头部,即 stext
    ldr     r0, =_output_data       @ 解压后的内核基址
    mov     r1, r7                  @ 恢复 machine ID
    mov     r2, r8                  @ 恢复 tags 地址
    mov     pc, r0                  @ 跳转到内核入口
zImage 解压与入口跳转流程 Bootloader 跳转到 zImage _start: 保存参数 检测 CPU / 机器类型 重定位判断 & 缓存使能 decompress_kernel() 跳转到 Image 入口 (stext)

2.4 内核入口点:stext 与初始页表

当解压完成并跳转到 ZRELADDR 后,CPU 开始执行真正的内核入口——stext,定义在 arch/arm/kernel/head.S 中。这是内核启动的第一个 C 语言之前的汇编阶段。

@ arch/arm/kernel/head.S
    .globl  stext
    .type   stext, #function
stext:
    @ 设置为 SVC32 模式,关闭中断
    msr     cpsr_c, #(PSR_I_BIT | PSR_F_BIT | SVC_MODE)

    @ 检查 CPU ID 是否匹配 (再次确认)
    mrc     p15, 0, r9, c0, c0
    bl      __lookup_processor_type
    teq     r10, #0
    beq     __error_p

    @ 创建初始页表 (为开启 MMU 做准备)
    bl      __create_page_tables

    @ 开启 MMU
    ldr     r13, =__mmap_switched
    adr     lr, BSYM(__enable_mmu)
    mov     pc, lr
入口点对比总结:
  • zImage 入口 (_start): 位于解压桩中,负责解压。Bootloader 直接跳转至此。
  • 内核入口 (stext): 位于解压后的 Image 头部,负责 MMU 初始化、页表创建。解压完成后跳转至此。
  • start_kernel: 位于 init/main.c,是第一个 C 函数,标志着架构无关的初始化开始。

2.5 启动日志中的解压阶段标识

在启用了早期串口输出的平台上,解压阶段会打印特定字符串。通过分析这些日志,可以判断解压是否成功以及入口跳转是否正常。

日志内容 来源文件 含义
Uncompressing Linux... arch/arm/boot/compressed/misc.c 解压开始,由 decompress_kernel 调用
done, booting the kernel. arch/arm/boot/compressed/misc.c 解压完成,即将跳转到 stext
Booting Linux on physical CPU 0x... arch/arm/kernel/setup.c 已进入 start_kernel,架构初始化开始
调优实战提示: 如果串口只输出 Uncompressing Linux... 但没有 done,说明解压过程中发生了数据异常(如压缩数据损坏、目标地址被覆盖)。此时应检查 ZRELADDR 配置是否与内存布局冲突,或使用 Image 跳过解压来隔离问题。

2.6 本章小结

  • Image 是纯二进制内核,无解压阶段;zImage 包含解压桩,体积更小但启动多一个阶段。
  • zImage 的生成由 arch/arm/boot/compressed/Makefile 控制,核心是链接解压桩目标文件与压缩数据。
  • 汇编入口 _start 负责保存参数、检测 CPU/机器类型、重定位、使能缓存、调用解压函数。
  • 解压完成后跳转到 stext,开始创建页表并开启 MMU,最终进入 start_kernel。
  • 启动日志中的 Uncompressing Linux... 和 done 是判断解压阶段是否正常的关键标志。