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 压缩后的数据。
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 @ 跳转到内核入口
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是判断解压阶段是否正常的关键标志。