限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
本文基于 ARM32架构
+ Linux 4.14内核
进行分析,且仅讨论 zImage
格式的解压缩过程。
要理解内核镜像的解压缩过程,首先要了解内核镜像的建立过程。下面来看内核镜像 zImage
的构建过程。
# arch/arm/boot/Makefile
# arch/arm/boot/Image 由 内核 ELF 文件 vmlinux 通过 objcopy 生成:
# objcopy
# vmlinux =======> arch/arm/boot/Image
$(obj)/Image: vmlinux FORCE
$(call if_changed,objcopy)
# arch/arm/boot/compressed/vmlinux 依赖于 arch/arm/boot/Image:
# 修改了 arch/arm/boot/Image 必须更新 arch/arm/boot/compressed/vmlinux
$(obj)/compressed/vmlinux: $(obj)/Image FORCE
$(Q)$(MAKE) $(build)=$(obj)/compressed $@
# arch/arm/boot/zImage 由 arch/arm/boot/compressed/vmlinux 通过 objcopy 生成:
# objcopy
# arch/arm/boot/compressed/vmlinux =======> arch/arm/boot/zImage
$(obj)/zImage: $(obj)/compressed/vmlinux FORCE
$(call if_changed,objcopy)
# arch/arm/boot/compressed/Makfile
# 内核解压缩程序主干代码
AFLAGS_head.o += -DTEXT_OFFSET=$(TEXT_OFFSET)
HEAD = head.o
OBJS += misc.o decompress.o
...
# 不同的内核配置,会使用不同的压缩算法对内核进行压缩
compress-$(CONFIG_KERNEL_GZIP) = gzip
compress-$(CONFIG_KERNEL_LZO) = lzo
compress-$(CONFIG_KERNEL_LZMA) = lzma
compress-$(CONFIG_KERNEL_XZ) = xzkern
compress-$(CONFIG_KERNEL_LZ4) = lz4
...
# 生成 解压缩程序 arch/arm/boot/compressed/vmlinux:
# {head.o,piggy.o,misc.o,decompress.o,...} ==> arch/arm/boot/compressed/vmlinux
$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o \
$(addprefix $(obj)/, $(OBJS)) $(lib1funcs) $(ashldi3) \
$(bswapsdi2) $(efi-obj-y) FORCE
@$(check_for_multiple_zreladdr)
$(call if_changed,ld)
@$(check_for_bad_syms)
# 将内核 Image 文件压缩成 piggy_data 文件:
# arch/arm/boot/Image ==> arch/arm/boot/compressed/piggy_data
$(obj)/piggy_data: $(obj)/../Image FORCE
$(call if_changed,$(compress-y))
# piggy.S ==> piggy.o
# arch/arm/boot/compressed/piggy.S 包含了 piggy_data (压缩的内核镜像 Image)
$(obj)/piggy.o: $(obj)/piggy_data
...
/* arch/arm/boot/compressed/piggy.S */
.section .piggydata,#alloc
.globl input_data
input_data:
.incbin "arch/arm/boot/compressed/piggy_data" /* 被压缩后的内核 Image */
.globl input_data_end
input_data_end:
$(call if_changed,objcopy)
用来调用 objcopy
,简单的看下它是怎么工作的:
# scripts/Kbuild.include
arg-check = $(if $(strip $(cmd_$@)),,1)
make-cmd = $(call escsq,$(subst $(pound),$$(pound),$(subst $$,$$$$,$(cmd_$(1)))))
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)
# 在合适的条件下,调用命令 cmd_XXX (如 cmd_objcopy)
# Execute command if command has changed or prerequisite(s) are updated.
if_changed = $(if $(strip $(any-prereq) $(arg-check)), \
@set -e; \
$(echo-cmd) $(cmd_$(1)); \
printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)
# scripts/Makefile.lib
# Objcopy
# ---------------------------------------------------------------------------
quiet_cmd_objcopy = OBJCOPY $@
cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $(OBJCOPYFLAGS_$(@F)) $< $@
内核代码根目录下 Makefile
,定义了 OBJCOPY
:
# 内核代码根目录下 Makefile
OBJCOPY = $(CROSS_COMPILE)objcopy
...
具体架构目录的 Makefile
,定义了 OBJCOPYFLAGS
:
# arch/arm/boot/Makefile
# -O binary : 输出文件(Image,zImage) 的 BFD 格式为 binary
# -R .comment:移除输入文件(vmlinux) 中 的 注释段
# -S : 移除输入文件(vmlinux) 中 的 符号信息、重定义信息、调试信息
OBJCOPYFLAGS :=-O binary -R .comment -S
...
通过上面的简单分析,可以将 zImage
的构建过程总结如下图:
编译+链接 objcopy 压缩(gzip,lzo,lzma,...)
1. linux源代码 ---------> vmlinux(elf文件) -------> arch/arm/boot/Image -----------------------> piggy_data
编译
2. piggy.S(包含 piggy_data 压缩内核) ------> piggy.o
链接 objcopy
3. (head.o,misc.o,decompress.o,...) + piggy.o ----> arch/arm/boot/compressed/vmlinux ------> arch/arm/boot/zImage
从 BootLoader
开始,内核的引导过程,可简单概括如下:
BootLoader -> 内核解压程序 -> 内核
在本文限定的上下文中,BootLoader
可以是 U-BOOT
等其它引导程序,内核解压程序
为 arch/arm/boot/zImage(开头一部分)
,内核为 arch/arm/boot/Image
。
从 内核解压程序
的链接脚本 arch/arm/boot/compressed/vmlinux.ld.S
片段
OUTPUT_ARCH(arm)
ENTRY(_start) // 指定内核解压程序入口
SECTIONS
{
...
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start) // 解压程序入口位置
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.glue_7t)
*(.glue_7)
}
...
}
了解到解压程序的入口位置在 .start 代码段
,我们从这里开始分析内核解压过程。一开始,会将处理器设置为 SVC 模式,并禁用 FIQ 和 IRQ 中断,以及保存一下上下文(如保存 CPU 架构 和 DTB 数据物理地址 等)
:
// arch/arm/boot/compressed/head.S
/* 内核解压程序入口 */
.section ".start", #alloc, #execinstr
.align
AR_CLASS( .arm )
start:
.type start,#function
// 重复 7 条 nop 指令
.rept 7
__nop
.endr
#ifndef CONFIG_THUMB2_KERNEL // ARM 指令模式内核(非 Thumb 指令模式)
mov r0, r0 // 第 8 条空指令
#else
...
#endif
W(b) 1f
// 一些 MAGIC 数字数据,
// 以及 UEFI 启动的 数据(本文不讨论 UEFI,ARM32 没见过用 UEFI 模式启动的)
...
1:
AR_CLASS( mrs r9, cpsr ) // 读取程序状态寄存器 cpsr 到 r9
...
/*
* BootLoader
* . 从 r1 传递硬件架构 ID
* . 从 r2 传递 DTB 物理地址
* 后续的代码会破坏 r1, r2 的值,这里先:
* 保存 硬件架构 ID 到 r7
* 保存 DTB 地址到 r8
*/
mov r7, r1 @ save architecture ID
mov r8, r2 @ save atags pointer
#ifndef CONFIG_CPU_V7M
mrs r2, cpsr @ get current mode
tst r2, #3 @ not user?
bne not_angel // 如果不是 User 模式,跳转到 not_angle 标号处
...
not_angel:
safe_svcmode_maskall r0 /* 将处理器设置为 SVC 模式, 同时禁用 FIQ & IRQ */
msr spsr_cxsf, r9 @ Save the CPU boot mode in
@ SPSR
#endif
然后是确定内核被解压后放置地址
到寄存器 r4
:
// arch/arm/boot/compressed/head.S
.text
/* 设定 内核 被解压缩后 的 加载地址 到 r4 */
#ifdef CONFIG_AUTO_ZRELADDR
mov r4, pc
/*
* 将加载向下对齐到 128MB:
* 这要求内核镜像被加载到所在物理内存 (128MB - TEXT_OFFSET) 位置开始及往上空间.
*/
and r4, r4, #0xf8000000
/*
* TEXT_OFFSET 由两个 Makefile 一起定义:
* (1) arch/arm/Makefile)
* textofs-y := 0x00008000
* ...
* TEXT_OFFSET := $(textofs-y)
* ...
* export TEXT_OFFSET GZFLAGS MMUEXT
* (2) arch/arm/boot/compressed/Makefile
* AFLAGS_head.o += -DTEXT_OFFSET=$(TEXT_OFFSET)
*/
add r4, r4, #TEXT_OFFSET /* 设定 解压后 内核的加载地址到 r4 */
#else
...
#endif
接下来,看 解压后的内核 和 内核解压程序中解压相关部分代码,是否存在空间重叠
,如果
两者存在空间重叠
,将 解压缩程序中解压相关部分代码 重定位 到解压后的内核 之后的空间
上去。来看细节:
// arch/arm/boot/compressed/head.S
/*
* 比较 解压程序当前运行地址 和 解压后内核加载起始地址:
* if (r0 < r4) { // 解压程序当前运行地址 < 解压后内核加载起始地址
* r0 = 解压程序当前结束位置地址(尾部向后扩展了部分空间)
* if (r4 < r0) // 内核加载起始地址 < 解压程序当前结束位置地址
* r4 |= 1 // 标记解压过程未使用 cache 加速
* else // 内核加载起始地址 >= 解压程序当前结束位置地址: 两者无空间重叠
* blcs cache_on // 开启 cache 加速解压过程
* } else { // 解压程序当前运行地址 >= 解压后内核加载起始地址: : 两者无空间重叠
* blcs cache_on // 开启 cache 加速解压过程
* }
* 从上面逻辑看到,如果 解压程序 和 解压后内核 位置在空间上没有重叠,则开启 cache
* 加速解压过程,这样做的原因,可能有更高的命中率 ???
*/
mov r0, pc // r0: 解压程序当前运行地址 + 4
cmp r0, r4 // 比较 解压程序当前运行地址 和 解压后内核起始加载地址
ldrcc r0, LC0+32
addcc r0, r0, pc
cmpcc r4, r0 // 比较 解压后内核起始加载地址 和 解压程序当前结束地址
orrcc r4, r4, #1 @ remember we skipped cache_on
blcs cache_on
restart: adr r0, LC0 /* r0: LC0 的 当前运行时地址 */
/*
* r1 : LC0 的 链接地址
* r2 : __bss_start 的 链接地址
* r3 : _end 的 链接地址
* r6 : _edata 的 链接地址
* r10: input_data_end 的 链接地址, 即 紧邻 压缩内核(piggy_data) 后的
* 4字节 的 链接地址,该地址开始的 4个字节存储了压缩前 内核的大小.
* 详见 piggy.S
* r11: _got_start 的 链接地址
* r12: _got_end 的 链接地址
*/
ldmia r0, {r1, r2, r3, r6, r10, r11, r12}
/* sp : 指向预分配 4K 的堆栈空间 .L_user_stack 底部链接地址,
* 即 .L_user_stack_end 的链接地址 (堆栈向低地址增长)
*/
ldr sp, [r0, #28]
// r0: LC0 当前运行时地址 - LC0 的链接地址
sub r0, r0, r1 @ calculate the delta offset
// r6: _edata 运行时地址 (解压缩程序 当前运行时 结束地址)
add r6, r6, r0 @ _edata
// r10: input_data_end 当前运行时地址
add r10, r10, r0 @ inflated kernel size location
// 读取压缩前内核大小到 r9 (即 arch/arm/boot/Image 的大小)
ldrb r9, [r10, #0]
ldrb lr, [r10, #1]
orr r9, r9, lr, lsl #8
ldrb lr, [r10, #2]
ldrb r10, [r10, #3]
orr r9, r9, lr, lsl #16
orr r9, r9, r10, lsl #24
#ifndef CONFIG_ZBOOT_ROM
/* malloc space is above the relocated stack (64k max) */
add sp, sp, r0 // 修正 sp 堆栈指针:指向栈空间底部 .L_user_stack_end 的 当前运行时地址
add r10, sp, #0x10000 // 移动到距离 .L_user_stack_end 64K 的位置
#else
...
#endif
mov r5, #0 @ init dtb size to 0
#ifdef CONFIG_ARM_APPENDED_DTB
// 早期支持 dts 的内核,要求 DTB 紧贴在内核之后的位置,后期的内核不再有这个要求
#endif
/*
* 检查 解压缩程序 和 解压后内核 是否存在空间重叠的情形。
* 解压缩程序 和 解压后内核 位置不重叠 有如下 两种 情形:
* (1) 解压缩程序 整个在 解压后内核 之前
* -------------
* | |
* | 解压缩程序 |
* | |
* \-------------\
* \ \
* |-------------|
* | |
* | 解压后内核 |
* | |
* -------------
*
* (2) 解压缩程序 整个在 解压后内核 之后
* -------------
* | |
* | 解压后内核 |
* | |
* \-------------\
* \ \
* |-------------|
* | |
* | 解压缩程序 |
* | |
* -------------
* 我们注意到, 检查代码中,解压缩程序顶部是以 wont_overwrite
* 为边界, 为什么? 因为如果存在除 (1) 或 (2) 之外的覆盖情形,如果覆盖
* 的不是 wont_overwrite 之后、用来解压缩的代码部分,前面这些已经运行
* 过的代码,即使覆盖了也无所谓,因为已经用不着了。
*/
/* 检验 是否是 情形 (1) */
add r10, r10, #16384
cmp r4, r10
bhs wont_overwrite // 情形 (1): 不需要做 解压缩程序 重定位,进入解压缩过程
/* 检验 是否是 情形 (2) */
add r10, r4, r9 // r10: 解压后内核 结束地址
adr r9, wont_overwrite
cmp r10, r9
bls wont_overwrite // 情形 (2): 不需要做 解压缩程序 重定位,进入解压缩过程
/*
* 解压缩程序(wont_overwrite 之后的解码代码) 和 解压后内核
* 存在空间重叠,需要对 解压缩程序 进行重定位,然后重新检测,
* 并最终进入解码 wont_overwrite 后的解压逻辑。
* 不管是什么情形的重叠,都是将 解压缩程序(区间
* [restart,reloc_code_end] 部分) 重定位到 内核 后面位置。
*/
/*
* Bump to the next 256-byte boundary with the size of
* the relocation code added. This avoids overwriting
* ourself when the offset is small.
*/
add r10, r10, #((reloc_code_end - restart + 256) & ~255)
bic r10, r10, #255
/* Get start of code we want to copy and align it down. */
adr r5, restart /* r5: restart 标号 的 当前运行时地址 */
bic r5, r5, #31
...
// 当前的上下文:
// r6: _edata 运行时地址 (解压缩程序 运行时 结束地址)
// r5: restart 标号的 运行时地址
// r10: 解压后内核 结束地址
// r9: 解压缩程序 需重定位的 代码数据 大小 (向上、向下均对齐后的大小)
sub r9, r6, r5 @ size to copy
add r9, r9, #31 @ rounded up to a multiple
bic r9, r9, #31 @ ... of 32 bytes
add r6, r9, r5 // r6: 解压缩程序 需重定位的代码数据 【旧的】 结束地址
add r9, r9, r10 // r9: 解压缩程序 【新的】 重定位起始地址
// 将 解压缩程序 重定位到 新的位置: 由 高地址 向 低地址 逆向拷贝
1: ldmdb r6!, {r0 - r3, r10 - r12, lr}
cmp r6, r5
stmdb r9!, {r0 - r3, r10 - r12, lr}
bhi 1b
/* Preserve offset to relocated code. */
// r6: 重定位后,新、旧 解压缩程序 开始位置 之间的距离
sub r6, r9, r6
#ifndef CONFIG_ZBOOT_ROM
/* cache_clean_flush may use the stack, so relocate it */
add sp, sp, r6 /* 随着重定位 解压缩程序 到新位置,4K .L_user_stack 堆栈也需要重定位 */
#endif
bl cache_clean_flush // cache 清理
badr r0, restart // r0: restart 当前的 运行时地址
add r0, r0, r6 // r0: restart 重定位后、新的 运行时地址
mov pc, r0 /* 重定位 解压缩程序 后, 跳转 restart 标号处 重新执行 */
上述过程中,涉及的数据、堆栈、链接脚本如下:
// arch/arm/boot/compressed/head.S
// 解压程序代码、数据的边界标记符号
.align 2
.type LC0, #object
LC0: .word LC0 @ r1 // +0
.word __bss_start @ r2 // +4
.word _end @ r3 // +8
.word _edata @ r6 // +12
.word input_data_end - 4 @ r10 (inflated size location) // + 16
.word _got_start @ r11 // + 20
.word _got_end @ ip // +24
.word .L_user_stack_end @ sp // +28
.word _end - restart + 16384 + 1024*1024 // +32
.size LC0, . - LC0 // +36
...
// 解压过程使用的堆栈空间
.align
.section ".stack", "aw", %nobits
.L_user_stack: .space 4096
.L_user_stack_end:
// arch/arm/boot/compressed/piggy.S
/* SPDX-License-Identifier: GPL-2.0 */
.section .piggydata,#alloc
.globl input_data
input_data:
/*
* arch/arm/boot/compressed/Makefile:
*
* $(obj)/piggy_data: $(obj)/../Image FORCE
* $(call if_changed,$(compress-y))
*
* $(obj)/piggy.o: $(obj)/piggy_data
*
* 被压缩后的内核 Image, 压缩程序(如 gzip) 会在 piggy_data 的
* 最后 4个字节 储存 压缩前内核的大小(即 arch/arm/boot/Image 的大小) 。
*/
.incbin "arch/arm/boot/compressed/piggy_data"
.globl input_data_end
input_data_end:
// 链接脚本:arch/arm/boot/compressed/vmlinux.ld.S
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
/DISCARD/ : {
*(.ARM.exidx*)
*(.ARM.extab*)
/*
* Discard any r/w data - this produces a link error if we have any,
* which is required for PIC decompression. Local data generates
* GOTOFF relocations, which prevents it being relocated independently
* of the text/got segments.
*/
*(.data) // 这里很重要,因为解压涉及到重定位,允许包 r/w 数据影响到程序重定位
}
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.glue_7t)
*(.glue_7)
}
.rodata : {
*(.rodata)
*(.rodata.*)
}
.piggydata : { // 内核 Image 压缩数据
*(.piggydata)
}
. = ALIGN(4);
_etext = .; // 只读代码、数据结束位置
// GOT(Global Offset Table) :PIC(位置无关代码)重定位表
.got.plt : { *(.got.plt) }
_got_start = .;
.got : { *(.got) }
_got_end = .;
/* ensure the zImage file size is always a multiple of 64 bits */
/* (without a dummy byte, ld just ignores the empty section) */
.pad : { BYTE(0); . = ALIGN(8); }
...
_edata = .;
.image_end (NOLOAD) : {
_edata_real = .;
}
_magic_sig = ZIMAGE_MAGIC(0x016f2818);
_magic_start = ZIMAGE_MAGIC(_start);
_magic_end = ZIMAGE_MAGIC(_edata);
. = BSS_START;
__bss_start = .;
.bss : { *(.bss) }
_end = .; // 解压程序结束位置
. = ALIGN(8); /* the stack must be 64-bit aligned */
/*
* 解压缩程序使用的堆栈段.
* arch/arm/boot/compressed/head.S:
* .align
* .section ".stack", "aw", %nobits
* .L_user_stack: .space 4096
* .L_user_stack_end:
* 这是 解压缩程序 中 【唯一一个】.stack 段。
*/
.stack : { *(.stack) }
...
}
完成了空间覆盖检测、重定位工作后,最后剩下的就是内核的解压了,看具体细节:
// arch/arm/boot/compressed/head.S
wont_overwrite:
/*
* If delta is zero, we are running at the address we were linked at.
* r0 = delta
* r2 = BSS start
* r3 = BSS end
* r4 = kernel execution address (possibly with LSB set)
* r5 = appended dtb size (0 if not present)
* r7 = architecture ID
* r8 = atags pointer
* r11 = GOT start
* r12 = GOT end
* sp = stack pointer
*/
/*
* r0: 运行时地址 - 链接地址
*/
orrs r1, r0, r5
beq not_relocated
add r11, r11, r0 // r11: _got_start (GOT 表起始位置) 当前运行时地址
add r12, r12, r0 // r12: _got_end (GOT 表结束位置) 当前运行时地址
#ifndef CONFIG_ZBOOT_ROM
/*
* If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
* we need to fix up pointers into the BSS region.
* Note that the stack pointer has already been fixed up.
*/
// 修正 BSS 段的位置
add r2, r2, r0 // r2: BSS 段起始位置 (__bss_start) 当前运行时地址
add r3, r3, r0 // r3: BSS 段结束位置 当前运行时地址
/*
* Relocate all entries in the GOT table.
* Bump bss entries to _edata + dtb size
*/
/* 遍历修正所有 GOT 表项: GOT[i] += (运行时地址 - 链接地址) */
1: ldr r1, [r11, #0] @ relocate entries in the GOT
add r1, r1, r0 @ This fixes up C references
cmp r1, r2 @ if entry >= bss_start &&
cmphs r3, r1 @ bss_end > entry
addhi r1, r1, r5 @ entry += dtb size // GOT[i] 再次修正: GOT[i] += DTB 大小
str r1, [r11], #4 @ next entry
cmp r11, r12
blo 1b
/* bump our bss pointers too */
add r2, r2, r5 // 再次 BSS 段起始位置 (__bss_start): GOT[i] += DTB 大小
add r3, r3, r5 // 再次 BSS 段结束位置: GOT[i] += DTB 大小
#else
...
#endif
// 清 0 整个 BSS 段
not_relocated: mov r0, #0
1: str r0, [r2], #4 @ clear bss
str r0, [r2], #4
str r0, [r2], #4
str r0, [r2], #4
cmp r2, r3
blo 1b
/*
* Did we skip the cache setup earlier?
* That is indicated by the LSB in r4.
* Do it now if so.
*/
tst r4, #1
bic r4, r4, #1
blne cache_on
/*
* The C runtime environment should now be setup sufficiently.
* Set up some pointers, and start decompressing.
* r4 = kernel execution address
* r7 = architecture ID
* r8 = atags pointer
*/
/* 解压内核调用 C 函数 decompress_kernel() ,需设置好 C 运行时环境。 */
// 解压内核到 r4 指向的地址
mov r0, r4
mov r1, sp @ malloc space above stack
add r2, sp, #0x10000 @ 64k max
mov r3, r7
bl decompress_kernel // 解压缩内核到 r4 指向的位置
bl cache_clean_flush
bl cache_off
#ifdef CONFIG_ARM_VIRT_EXT
...
bne __enter_kernel @ boot kernel directly
...
#else
b __enter_kernel // 准备跳转到解压后的内核
#endif
...
__enter_kernel:
mov r0, #0 @ must be 0
// 恢复保存的 BootLoader 传递的 CPU 架构、DTB 物理地址 到 r1, r2
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
// 跳转到内核入口执行: arch/arm/boot/head.S 中 ENTRY(stext) 处执行
ARM( mov pc, r4 ) @ call kernel
reloc_code_end:
https://www.man7.org/linux/man-pages/man1/objcopy.1.html