文章目录
- 1. 前言
- 2. 背景
- 3. zImage 的构建过程
- 4. 内核引导过程
- 5. 内核解压缩过程
- 6. 内核加压缩过程小结
- 7. 参考资料
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文基于 ARM32架构
+ Linux 4.14内核
进行分析,且仅讨论 zImage
格式的解压缩过程。
3. 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.includearg-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
:
# 内核代码根目录下 MakefileOBJCOPY = $(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
4. 内核引导过程
从 BootLoader
开始,内核的引导过程,可简单概括如下:
BootLoader -> 内核解压程序 -> 内核
在本文限定的上下文中,BootLoader
可以是 U-BOOT
等其它引导程序,内核解压程序
为 arch/arm/boot/zImage(开头一部分)
,内核为 arch/arm/boot/Image
。
5. 内核解压缩过程
从 内核解压程序
的链接脚本 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.alignAR_CLASS( .arm )
start:.type start,#function// 重复 7 条 nop 指令.rept 7__nop.endr
#ifndef CONFIG_THUMB2_KERNEL // ARM 指令模式内核(非 Thumb 指令模式)mov r0, r0 // 第 8 条空指令
#else...
#endifW(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 IDmov r8, r2 @ save atags pointer#ifndef CONFIG_CPU_V7Mmrs r2, cpsr @ get current modetst 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_ZRELADDRmov 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: 解压程序当前运行地址 + 4cmp r0, r4 // 比较 解压程序当前运行地址 和 解压后内核起始加载地址ldrcc r0, LC0+32addcc r0, r0, pccmpcc r4, r0 // 比较 解压后内核起始加载地址 和 解压程序当前结束地址orrcc r4, r4, #1 @ remember we skipped cache_onblcs cache_onrestart: 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 #8ldrb lr, [r10, #2]ldrb r10, [r10, #3]orr r9, r9, lr, lsl #16orr 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...
#endifmov 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, #16384cmp r4, r10bhs wont_overwrite // 情形 (1): 不需要做 解压缩程序 重定位,进入解压缩过程/* 检验 是否是 情形 (2) */add r10, r4, r9 // r10: 解压后内核 结束地址adr r9, wont_overwritecmp r10, r9bls 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 copyadd r9, r9, #31 @ rounded up to a multiplebic r9, r9, #31 @ ... of 32 bytesadd r6, r9, r5 // r6: 解压缩程序 需重定位的代码数据 【旧的】 结束地址add r9, r9, r10 // r9: 解压缩程序 【新的】 重定位起始地址// 将 解压缩程序 重定位到 新的位置: 由 高地址 向 低地址 逆向拷贝
1: ldmdb r6!, {r0 - r3, r10 - r12, lr}cmp r6, r5stmdb 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 堆栈也需要重定位 */
#endifbl 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.SOUTPUT_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.Swont_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, r5beq not_relocatedadd 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 GOTadd r1, r1, r0 @ This fixes up C referencescmp r1, r2 @ if entry >= bss_start &&cmphs r3, r1 @ bss_end > entryaddhi r1, r1, r5 @ entry += dtb size // GOT[i] 再次修正: GOT[i] += DTB 大小str r1, [r11], #4 @ next entrycmp r11, r12blo 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 bssstr r0, [r2], #4str r0, [r2], #4str r0, [r2], #4cmp r2, r3blo 1b/** Did we skip the cache setup earlier?* That is indicated by the LSB in r4.* Do it now if so.*/tst r4, #1bic r4, r4, #1blne 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, r4mov r1, sp @ malloc space above stackadd r2, sp, #0x10000 @ 64k maxmov r3, r7bl decompress_kernel // 解压缩内核到 r4 指向的位置bl cache_clean_flushbl cache_off#ifdef CONFIG_ARM_VIRT_EXT...bne __enter_kernel @ boot kernel directly...
#elseb __enter_kernel // 准备跳转到解压后的内核
#endif...__enter_kernel:mov r0, #0 @ must be 0// 恢复保存的 BootLoader 传递的 CPU 架构、DTB 物理地址 到 r1, r2mov r1, r7 @ restore architecture numbermov r2, r8 @ restore atags pointer// 跳转到内核入口执行: arch/arm/boot/head.S 中 ENTRY(stext) 处执行ARM( mov pc, r4 ) @ call kernelreloc_code_end:
6. 内核加压缩过程小结
我们用一幅流程图来简单小结下内核的解压缩过程。如下:
7. 参考资料
https://www.man7.org/linux/man-pages/man1/objcopy.1.html