• Linux:内核解压缩过程简析


    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)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    # 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
    
    ...
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    /* 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:
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    $(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, @:)
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    # scripts/Makefile.lib
    
    # Objcopy
    # ---------------------------------------------------------------------------
    
    quiet_cmd_objcopy = OBJCOPY $@
    cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) $(OBJCOPYFLAGS_$(@F)) $< $@
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    内核代码根目录下 Makefile ,定义了 OBJCOPY

    # 内核代码根目录下 Makefile
    
    OBJCOPY  = $(CROSS_COMPILE)objcopy
    ...
    • 1
    • 2
    • 3

    具体架构目录的 Makefile ,定义了 OBJCOPYFLAGS

    # arch/arm/boot/Makefile
    
    # -O binary  : 输出文件(Image,zImage) 的 BFD 格式为 binary
    # -R .comment:移除输入文件(vmlinux) 中 的 注释段
    # -S         : 移除输入文件(vmlinux) 中 的 符号信息、重定位信息、调试信息
    OBJCOPYFLAGS :=-O binary -R .comment -S
    ...
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    通过上面的简单分析,可以将 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
    • 1
    • 2
    • 3
    • 4
    • 5

    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)
      	}
      
      	...
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21

      了解到解压程序的入口位置在 .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
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47

      然后是确定内核被解压后放置地址到寄存器 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
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25

      接下来,看 解压后的内核 和 内核解压程序中解压相关部分代码,是否存在空间重叠如果两者存在空间重叠将 解压缩程序中解压相关部分代码 重定位 到解压后的内核 之后的空间上去。来看细节:

      // 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 标号处 重新执行 */
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109
      • 110
      • 111
      • 112
      • 113
      • 114
      • 115
      • 116
      • 117
      • 118
      • 119
      • 120
      • 121
      • 122
      • 123
      • 124
      • 125
      • 126
      • 127
      • 128
      • 129
      • 130
      • 131
      • 132
      • 133
      • 134
      • 135
      • 136
      • 137
      • 138
      • 139
      • 140
      • 141
      • 142
      • 143
      • 144
      • 145
      • 146
      • 147
      • 148
      • 149
      • 150
      • 151
      • 152
      • 153
      • 154
      • 155
      • 156
      • 157
      • 158
      • 159
      • 160
      • 161
      • 162
      • 163
      • 164
      • 165
      • 166

      上述过程中,涉及的数据、堆栈、链接脚本如下:

      // 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:
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      // 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:
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      // 链接脚本: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) }
      
      	...
      }
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82

      完成了空间覆盖检测、重定位工作后,最后剩下的就是内核的解压了,看具体细节:

      // 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:
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30
      • 31
      • 32
      • 33
      • 34
      • 35
      • 36
      • 37
      • 38
      • 39
      • 40
      • 41
      • 42
      • 43
      • 44
      • 45
      • 46
      • 47
      • 48
      • 49
      • 50
      • 51
      • 52
      • 53
      • 54
      • 55
      • 56
      • 57
      • 58
      • 59
      • 60
      • 61
      • 62
      • 63
      • 64
      • 65
      • 66
      • 67
      • 68
      • 69
      • 70
      • 71
      • 72
      • 73
      • 74
      • 75
      • 76
      • 77
      • 78
      • 79
      • 80
      • 81
      • 82
      • 83
      • 84
      • 85
      • 86
      • 87
      • 88
      • 89
      • 90
      • 91
      • 92
      • 93
      • 94
      • 95
      • 96
      • 97
      • 98
      • 99
      • 100
      • 101
      • 102
      • 103
      • 104
      • 105
      • 106
      • 107
      • 108
      • 109

      6. 内核加压缩过程小结

      我们用一幅流程图来简单小结下内核的解压缩过程。如下:
      在这里插入图片描述

      7. 参考资料

      https://www.man7.org/linux/man-pages/man1/objcopy.1.html

    • 相关阅读:
      数组对象去重
      linux安全配置规范
      说说HBase读、写流程
      阿里妈妈API接口 按关键字或网址搜索商品
      数据湖:数据库数据迁移工具Sqoop
      Audacity 使用教程:轻松录制、编辑音频
      javaee SpringMVC文件上传 项目结构
      电容笔和Apple pencil的区别有哪些?十大电容笔知名品牌
      多监控系统产生的告警如何高效管理 - 运维事件中心
      Automatic Detection of Welding Defects Using Faster R-CNN
    • 原文地址:https://blog.csdn.net/JiMoKuangXiangQu/article/details/132576663