[Linux]学习笔记系列 -- [arm]boot

发布于:2025-08-07 ⋅ 阅读:(22) ⋅ 点赞:(0)


在这里插入图片描述

https://github.com/wdfk-prog/linux-study

解压过程

  1. 依据arch/arm/kernel/vmlinux.lds 生成linux内核源码根目录下的vmlinux,这个vmlinux属于未压缩,带调试信息、符号表的最初的内核,大小约23MB;
  2. 将上面的vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/Image,这是不带多余信息的linux内核,Image的大小约3.2MB
  3. 将 arch/arm/boot/Image 用gzip -9 压缩生成arch/arm/boot/compressed/piggy.gz大小约1.5MB;
  4. 编译arch/arm/boot/compressed/piggy.S 生成arch/arm/boot/compressed/piggy.o大小约1.5MB,这里实际上是将piggy.gz通过piggy.S编译进piggy.o文件中。而piggy.S文件仅有6行,只是包含了文件piggy.gz;
  5. 依据arch/arm/boot/compressed/vmlinux.lds 将arch/arm/boot/compressed/目录下的文件head.o 、piggy.o 、misc.o链接生成 arch/arm/boot/compressed/vmlinux,这个vmlinux是经过压缩且含有自解压代码的内核,大小约1.5MB;
  6. 将arch/arm/boot/compressed/vmlinux去除调试信息、注释、符号表等内容,生成arch/arm/boot/zImage大小约1.5MB;这已经是一个可以使用的linux内核映像文件了;
  7. 将arch/arm/boot/zImage添加64Bytes的相关信息打包为arch/arm/boot/uImage大小约1.5MB;

Makefile

TEXT_OFFSET 内核映像的字节偏移量 0x00008000

# 文本偏移量。此列表按地址进行数字排序,以便
# 提供一种避免/解决多架构内核冲突的方法。
# 注意:该值以下的 32kB 是预留给内核使用的
# 在引导期间,这个偏移量对于kexec-tools 的
textofs-y	:= 0x00008000
# TRAM 中内核映像的字节偏移量从 RAM 开始。
TEXT_OFFSET := $(textofs-y)

piggy_data

  1. 生成vmlinux时一同生成piggy_data
  2. piggy_data包含了内核的压缩数据,压缩算法由 CONFIG_KERNEL_GZIP等决定
  3. piggy_data.o,使用piggy_data的内容生成.o文件

include/asm/unified.h

AR_CLASS M_CLASS 选择使用的架构指令

#ifdef CONFIG_CPU_V7M
#define AR_CLASS(x...)
#define M_CLASS(x...)	x
#else
#define AR_CLASS(x...)	x
#define M_CLASS(x...)
#endif

compressed/efi-header.S

__nop 空指令

		.macro	__nop
 AR_CLASS(	mov	r0, r0		)
  M_CLASS(	nop.w			)
		.endm

__initial_nops 执行2个__nop

		.macro __initial_nops
#ifdef CONFIG_EFI_STUB
		.inst	MZ_MAGIC | (0xe225 << 16)	@ eor r5, r5, 0x4d000
		eor	r5, r5, 0x4d000			@ undo previous insn
#else
		__nop
		__nop
#endif
		.endm

compressed/fdt_check_mem_start.c 从FDT中检查内存是否可用,返回可用内存起始地址

  1. 检查传入的 R1,即 mov r8, r2 @ save atags pointer传入的FDT指针是否为空
  2. 32位地址空间,检查 #address-cells#size-cells是否大于2
  3. 检查 /chosen节点下是否存在 linux,usable-memory-range属性,是否有可用内存限制
  4. 检查 device_typememory的节点,是否存在 linux,usable-memoryreg属性
  5. 获取内存的起始地址和大小,检查是否在可用内存范围内
  6. 如果在可用内存范围内,则返回传入的 mem_start;否则更新可用内存的起始地址使用最小的可用内存
  7. 如果没有找到可用内存,则使用传入的 mem_start作为返回值
  8. 返回值:要使用的物理内存起始地址
uint32_t fdt_check_mem_start(uint32_t mem_start, const void *fdt)
{
	uint32_t addr_cells, size_cells, usable_base, base;
	uint32_t fdt_mem_start = 0xffffffff;
	const fdt32_t *usable, *reg, *endp;
	uint64_t size, usable_end, end;
	const char *type;
	int offset, len;

	if (!fdt)
		return mem_start;

	if (fdt_magic(fdt) != FDT_MAGIC)
		return mem_start;

	/* There may be multiple cells on LPAE platforms */
	addr_cells = get_cells(fdt, "#address-cells");
	size_cells = get_cells(fdt, "#size-cells");
	if (addr_cells > 2 || size_cells > 2)
		return mem_start;

	/*
	 * 崩溃转储内核时的可用内存
	 * 此属性描述限制:此范围内的内存为
	 * 仅在通过其他机制进行描述时有效
	 */
	usable = get_prop(fdt, "/chosen", "linux,usable-memory-range",
			  (addr_cells + size_cells) * sizeof(fdt32_t));
	if (usable) {
		size = get_val(usable + addr_cells, size_cells);
		if (!size)
			return mem_start;

		if (addr_cells > 1 && fdt32_ld(usable)) {
			/* Outside 32-bit address space */
			return mem_start;
		}

		usable_base = fdt32_ld(usable + addr_cells - 1);
		usable_end = usable_base + size;
	}

	/* 遍历所有内存节点和区域 */
	for (offset = fdt_next_node(fdt, -1, NULL); offset >= 0;
	     offset = fdt_next_node(fdt, offset, NULL)) {
		type = fdt_getprop(fdt, offset, "device_type", NULL);
		if (!type || strcmp(type, "memory"))
			continue;

		reg = fdt_getprop(fdt, offset, "linux,usable-memory", &len);
		if (!reg)
			reg = fdt_getprop(fdt, offset, "reg", &len);
		if (!reg)
			continue;

		for (endp = reg + (len / sizeof(fdt32_t));	//获取当前节点的reg属性
		     endp - reg >= addr_cells + size_cells;	//reg属性的长度大于一对参数
		     reg += addr_cells + size_cells) {		//获取下一个reg属性
			size = get_val(reg + addr_cells, size_cells);	//获取当前节点的size
			if (!size)
				continue;

			if (addr_cells > 1 && fdt32_ld(reg)) {
				/* Outside 32-bit address space, skipping */
				continue;
			}

			base = fdt32_ld(reg + addr_cells - 1);
			end = base + size;
			if (usable) {
				/*
				 * Clip to usable range, which takes precedence
				 * over mem_start
				 */
				if (base < usable_base)
					base = usable_base;

				if (end > usable_end)
					end = usable_end;

				if (end <= base)
					continue;
			} else if (mem_start >= base && mem_start < end) {
				/* 计算的地址有效,使用它*/
				return mem_start;
			}
			//更新可用内存的起始地址,使用最小的可用内存
			if (base < fdt_mem_start)
				fdt_mem_start = base;
		}
	}

	if (fdt_mem_start == 0xffffffff) {
		/* 未找到可用内存,正在回退到默认值*/
		return mem_start;
	}

	/*
	 *计算出的地址不可用,或者被
	 * “linux,usable-memory-range” 属性。
	 * 改用 DTB 中可用的最低物理内存地址,
	 * 并确保这是 2 MiB 的倍数,以便进行 phys/virt 修补。
	 */
	return round_up(fdt_mem_start, SZ_2M);
}

compressed/piggy.S 设置piggy_data的内容与piggy_data_end的地址

	//只读属性
	.section .piggydata, "a"
	//全局符号
	.globl	input_data
input_data:
	//指定文件 piggy_data 的内容直接包含到当前的汇编文件中
	.incbin	"arch/arm/boot/compressed/piggy_data"
	.globl	input_data_end
input_data_end:

compressed/misc.c 杂项

decompress_kernel 调用解压内核函数,并打印日志

void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,
		unsigned long free_mem_ptr_end_p,
		int arch_id)
{
	int ret;

	output_data		= (unsigned char *)output_start;
	free_mem_ptr		= free_mem_ptr_p;
	free_mem_end_ptr	= free_mem_ptr_end_p;
	__machine_arch_type	= arch_id;

	arch_decomp_setup();	//arch/arm/include/debug/uncompress.h为空函数不执行

	putstr("Uncompressing Linux...");	//putstr最终调用puts,在debug.s中实现
	ret = do_decompress(input_data, input_data_end - input_data,
			    output_data, error);
	if (ret)
		error("decompressor returned an error");	//输出错误后,while(1)死循环
	else
		putstr(" done, booting the kernel.\n");
}

arch/arm/boot/compressed/decompress.c 不同解压的中间层

  • makefile和konconfig中进行了选择配置需要的解压算法
#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif

#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif

#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif

#ifdef CONFIG_KERNEL_XZ
/* Prevent KASAN override of string helpers in decompressor */
#undef memmove
#define memmove memmove
#undef memcpy
#define memcpy memcpy
#include "../../../../lib/decompress_unxz.c"
#endif

#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif

int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
	return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
}

compressed/decompress.c

  1. 根据所选的解压算法,包含不同的解压算法.同时只能选择一个解压算法
  2. 注意定义了STATICSTATIC_RW_DATA,并且decompress_inflate.c是**include**的.分析时需要注意包括这两个宏
#define STATIC static
#define STATIC_RW_DATA	/* non-static please */

#ifdef CONFIG_KERNEL_GZIP
#include "../../../../lib/decompress_inflate.c"
#endif

#ifdef CONFIG_KERNEL_LZO
#include "../../../../lib/decompress_unlzo.c"
#endif

#ifdef CONFIG_KERNEL_LZMA
#include "../../../../lib/decompress_unlzma.c"
#endif

#ifdef CONFIG_KERNEL_XZ
#include "../../../../lib/decompress_unxz.c"
#endif

#ifdef CONFIG_KERNEL_LZ4
#include "../../../../lib/decompress_unlz4.c"
#endif

int do_decompress(u8 *input, int len, u8 *output, void (*error)(char *x))
{
	return __decompress(input, len, NULL, NULL, output, 0, NULL, error);
}

compressed/head.S

  • 使用__nop指令填充32字节,跳过a.out头部.
    • 其中 __initial_nops跳过2
    • 循环5个
    • M_CLASS( nop.w )跳过一个
  • 跳转到1,重定向栈顶地址,开启缓存页表
  • cache_on开启缓存,call_cache_fn获取处理器ID.(V7M直接返回)
  • restart函数重定向并获取解压后的内核大小,判断页表是否会覆盖内核映像,如果会则拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行.不会则跳转到wont_overwrite函数
  • wont_overwrite修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址,顺序执行到not_relocated标签

start 入口函数

		.section ".start", "ax"
		.align
 AR_CLASS(	.arm	)
start:
		.type	start,#function
		/*
	     * 这 7 个 nop 以及下面的 1 个 nop 为
		 * !THUMB2 形成 8 个 nop,使压缩的内核可启动
		 * 在假定内核在 a.out 中的传统 ARM 系统上
		 * 二进制格式。这些系统上的引导加载程序将
		 * 将 32 字节跳转到图像中以跳过 a.out 标头。
		 * 这 8 个 nop 正好填满了 32 个字节,一切仍然如此
		 * 在这些旧系统上按预期工作。Thumb2 模式保持
		 * 7 个 nop,因为事实证明一些引导加载程序
		 * 正在修补内核的初始指令,即
		 * 已开始利用此 “补丁区域”。
		 */
		__initial_nops
		.rept	5
		__nop
		.endr
#ifndef CONFIG_THUMB2_KERNEL
		__nop
#else
 AR_CLASS(	sub	pc, pc, #3	)	@ A/R: switch to Thumb2 mode
  M_CLASS(	nop.w			)	@ M: already in Thumb2 mode
		.thumb                  @使用thumb指令集
#endif
		W(b)	1f              @跳转到1
        @r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。
		mov	r7, r1			@ save architecture ID
		mov	r8, r2			@ save atags pointer

LC0 保存着内核的基地址和数据段结束地址

  1. got:全局偏移表,存储动态链接的函数的地址
		.align	2
		.type	LC0, #object
LC0:		.word	LC0			@ r1
		.word	__bss_start		@ r2
		.word	_end			@ r3
		.word	_got_start		@ r11
		.word	_got_end		@ ip
		.size	LC0, . - LC0

LC1 存放着内核的栈顶地址和数据段结束地址的位置无关偏移量

  • LC1中存放着内核的栈顶地址和数据段结束地址
  • 通过存储栈顶地址-LC1的地址来获取位置无关的地址
		.type	LC1, #object
LC1:		.word	.L_user_stack_end - LC1	@ sp
		.word	_edata - LC1		@ r6 image_end
		.size	LC1, . - LC1        @确定LC1占用的字节数

.L_user_stack:	.space	4096
.L_user_stack_end:

Lheadroom 内核代码大小+16kb(页表)+DTB预留大小(1mb)

.Lheadroom:
		.word	_end - restart + 16384 + 1024*1024

1 使用位置无关执行重定向栈顶地址,并开启缓存页表

  1. 对齐128MB
  2. 修正sp栈顶指针
  3. 通过 fdt_check_mem_start检查内存是否可用,并返回可用内存起始地址
  4. 确定最终的内核映像地址
  5. 判断页表是否会覆盖内核映像.(在内核映像地址范围下的16kb页表与1mb的DTB)
  6. 设置之后的页表超过了内核映像,则跳过cache_on不进行设置.r4的LSB位表示
    • 如果r4的LSB位为1,则跳过cache_on
    • 如果r4的LSB位为0,则调用cache_on开启缓存
  7. 否则调用 cache_on开启缓存
		.text
#ifdef CONFIG_AUTO_ZRELADDR
		/*
		 * 查找物理内存的起点。 当我们执行时如果不开启 MMU,我们将处于物理地址空间。
		 * 我们只需要通过对齐地址。
		 *
		 * 这种一致性是不同的平台 - 我们选择了 128MB 来允许对齐其物理内存开始的平台
		 * 设置为 128MB 以使用此功能,同时允许 zImage放置在其他平台。 增加对齐意味着我们将
		 * 对物理开始时的对齐要求更严格 记忆,但放松它意味着我们打破了那些 已经将他们的 zImage 放在(例如)前 64MB 中
		 * 的
		 */
		mov	r0, pc
		and	r0, r0, #0xf8000000     @对齐128MB
#ifdef CONFIG_USE_OF
        @LC1 存放着内核的栈顶地址和数据段结束地址
		adr	r1, LC1                 @r1 存储LC1的内存地址
		ldr	sp, [r1]	@ get stack location
        @ 这里需要加上LC1的地址偏移量,修正存放在LC1处减去的偏移量
		add	sp, sp, r1	@ apply relocation

		/*根据通过的 DTB 验证计算的开始时间 */
		mov	r1, r8  @r8 save atags pointer
		bl	fdt_check_mem_start
1:
#endif /* CONFIG_USE_OF */
		/*确定最终的内核映像地址. */
		add	r4, r0, #TEXT_OFFSET
#endif

		/*
		 * 仅当页表不会覆盖我们自己时,才设置页表。
		 * 这意味着 r4 < pc || r4 - 16k 页目录 > &_end。
		 * 鉴于 r4 > &_end 最不频繁,我们添加了一个粗略的
		 * 额外 1MB 的空间用于可能的附加 DTB。
		 */
		mov	r0, pc
		/*
		if(pc <= 确定最终的内核映像地址) {
			r0 = headroom;
			r0 += pc;
		}
		//设置之后的页表超过了内核映像,则跳过cache_on不进行设置
		if(确定最终的内核映像地址 <= r0) {
			r4 |= 1;
			//跳过cache_on
		}
		*/
		cmp	r0, r4
		ldrcc	r0, .Lheadroom
		addcc	r0, r0, pc
		cmpcc	r4, r0
		orrcc	r4, r4, #1	@ 记住我们跳过了cache_on ,用R4的LSB位表示
		blcs	cache_on	@上次第一个操作数大于或等于第二个操作数,调用cache_on

cache_on 开启缓存

  1. 32字节对齐,传入8,调用 call_cache_fn,然后返回
  2. 这段代码的目的是开启缓存(cache)。为了开启指令缓存(I cache)和数据缓存(D cache),需要设置一些页表。页表被放置在内核执行地址向下16KB的位置,希望这个位置没有被其他东西使用。如果被使用了,可能会导致程序崩溃。
  3. 在进入这个例程时:
    • r4 寄存器包含内核执行地址
    • r7 寄存器包含架构编号
    • r8 寄存器包含 ATAGs 指针
  4. 在退出这个例程时,以下寄存器的值可能会被破坏:
    • r0, r1, r2, r3, r9, r10, r12
  5. 这个例程必须保留 r4, r7, r8 的值。
		.align	5
cache_on:	mov	r3, #8			@ cache_on function
		b	call_cache_fn

call_cache_fn 获取处理器ID

  1. r12设置为 proc_types的地址
  2. 获取处理器ID
  3. 根据处理器ID跳转到不同的函数
  4. CONFIG_CPU_V7M则在其他地方实现,这里直接返回
/*
 * 以下是
 * 各种处理器。 这是一个通用的钩子,用于定位
 * 输入并跳转到指定偏移量处的指令
 * 从区块的开头开始。 请注意,这是所有位置
 * 独立代码。
 *
 * r1 = 损坏
 * r2 = 损坏
 * r3 = 块偏移
 * r9 = 损坏
 * r12 = 损坏
 */
call_cache_fn:	adr	r12, proc_types
#ifdef CONFIG_CPU_CP15
		mrc	p15, 0, r9, c0, c0	@ get processor ID
#elif defined(CONFIG_CPU_V7M)
		/*
		 * 在 v7-M 上,处理器 ID 位于 V7M_SCB_CPUID
		 * register,但由于缓存处理是在
		 * v7-M(如果存在的话)我们只是提前返回这里。
		 * 如果使用了 V7M_SCB_CPUID 则 cpu ID 函数(即
		 * __armv7_mmu_cache_{on,off,flush}) 将被选中,该
		 * 使用未在 v7-M 上实现的 CP15 寄存器。
		 */
		bx	lr
#else
		ldr	r9, =CONFIG_PROCESSOR_ID
#endif

reloc_code_end 记录重定向代码的结束地址

restart:	//331
reloc_code_end:	//1437

dbgkc 打印调试内核信息

		.macro	kputc,val
		mov	r0, \val
		bl	putc
		.endm

		/*
		 * Debug kernel copy by printing the memory addresses involved
		 */
		.macro dbgkc, begin, end, cbegin, cend
#ifdef DEBUG
		kputc   #'C'
		kputc   #':'
		kputc   #'0'
		kputc   #'x'
		kphex   \begin, 8	/* Start of compressed kernel */
		kputc	#'-'
		kputc	#'0'
		kputc	#'x'
		kphex	\end, 8		/* End of compressed kernel */
		kputc	#'-'
		kputc	#'>'
		kputc   #'0'
		kputc   #'x'
		kphex   \cbegin, 8	/* Start of kernel copy */
		kputc	#'-'
		kputc	#'0'
		kputc	#'x'
		kphex	\cend, 8	/* End of kernel copy */
		kputc	#'\n'
#endif
		.endm

cache_clean_flush 清除缓存并刷新

/*
 * 清理并刷新缓存以保持一致性。
 *
 * 入境时,
 * r0 = 起始地址
 * r1 = 结束地址(不包括)
 * 退出时,
 * R1、R2、R3、R9、R10、R11、R12 损坏
 * 此例程必须保留:
 * R4、R6、R7、R8
 */
		.align	5
cache_clean_flush:
		mov	r3, #16
		mov	r11, r1
		b	call_cache_fn

restart 重定向并获取解压后的内核大小

  1. 重定向栈顶地址
  2. 重定向数据段结束地址
  3. 调用 get_inflated_image_size获取解压后的内核大小
  4. 分配64k的内存空间用于malloc
    #arch/arm/boot/compressed/Makefile
    MALLOC_SIZE	:= 65536
    AFLAGS_head.o += -DTEXT_OFFSET=$(TEXT_OFFSET) -DMALLOC_SIZE=$(MALLOC_SIZE)
    
  5. 检查页表是否会覆盖内核映像
  6. 不会则跳转到 wont_overwrite函数
  7. 会则修正内核映像地址,拷贝内核到新的内存区域,重定向zimage的地址,并跳转到重定向后的restart的地址进行执行
restart:	adr	r0, LC1		//获取LC1的地址
		ldr	sp, [r0]	//获取栈顶地址
		ldr	r6, [r0, #4]	//获取数据段结束地址
		add	sp, sp, r0	//修正栈顶地址
		add	r6, r6, r0	//修正数据段结束地址

		get_inflated_image_size	r9, r10, lr
#ifndef CONFIG_ZBOOT_ROM	//ROM/flash 中的压缩引导加载程序
		/* malloc 空间位于重新定位的堆栈上方(最大 64K) */
		add	r10, sp, #MALLOC_SIZE
#else
		/*
		 * 如果 BSS/堆栈ZBOOT_ROM不可重定位,
		 * 但是有人仍然可以从 RAM 运行此代码,
		 * 在这种情况下,我们的参考是 _edata。
		 */
		mov	r10, r6
#endif

		mov	r5, #0			@ init dtb size 设置为 0
// 使用附加的设备树blob到zImage(实验)
#ifdef CONFIG_ARM_APPENDED_DTB
#endif
/*
 * 检查我们是否会覆盖自己。
 * r4 = 最终内核地址(可能设置了 LSB)
 * r9 = 解压缩图像的大小
 * r10 = 此映像的结尾,包括 bss/stack/malloc 空间(如果非 XIP)
 * 我们基本上想要:
 * r4 - 16k 页面目录 >= r10 -> OK
 * r4 + 图像长度 <= wont_overwrite 的地址 -> OK
 * 注意:r4 中可能的 LSB 在这里是无害的。
 */
		add	r10, r10, #16384	//页表大小
		cmp	r4, r10				//是否会覆盖自己
		bhs	wont_overwrite		//if(r4 >= r10 + 16k)跳转到wont_overwrite
		add	r10, r4, r9			//r10 = 解压缩图像的结束地址
		adr	r9, wont_overwrite	//将 wont_overwrite 标签的地址加载到 r9 寄存器中。
		cmp	r10, r9
		bls	wont_overwrite		//if(解压缩图像的结束地址 <= wont_overwrite 的地址)跳转到wont_overwrite

// 没有跳转,需要修复
/*
 * 重新定位到解压缩的内核末尾之后。
 * r6 = _edata(image_end)
 * r10 = 解压内核的结束地址
 * 因为我们总是提前复制,所以我们需要从头开始做.如果源和目标重叠,则向后移动。
 */
		/*
		 * 移动到已添加重定位代码的下一个 256 字节边界.
		 * 这样可以避免当偏移量较小时覆盖我们自己。
		 */
		add	r10, r10, #((reloc_code_end - restart + 256) & ~255)
		bic	r10, r10, #255	@对齐到256字节

		/* 获取我们想要复制的代码的开头并将其对齐。 */
		adr	r5, restart
		bic	r5, r5, #31	@对齐到32字节
/* Relocate the hyp vector base if necessary */
#ifdef CONFIG_ARM_VIRT_EXT	//无使用
#endif
		//R9 = _edata(image_end) + restart的地址
		sub	r9, r6, r5		@ size to copy
		add	r9, r9, #31
		bic	r9, r9, #31		@ 确保 r9 的值是32字节对齐的。
		add	r6, r9, r5		//r6 = restart + size to copy
		add	r9, r9, r10		//r9 = size to copy + 解压内核的结束地址
#ifdef DEBUG	//打印内核信息
		sub     r10, r6, r5
		sub     r10, r9, r10
		/*
		 * 我们即将将内核复制到新的内存区域。
		 * 新内存区域的边界可以在 R10 和 R9,而 R5 和 R6 包含边界
		 * 的 MEMORY 中。致电 dbgkc 将有助于打印此内容信息。
		 */
		dbgkc	r5, r6, r10, r9
		/* 	Start of compressed kernel 
			End of compressed kernel
			Start of kernel copy
			End of kernel copy
			C:0xC0080060-0xC02151C0->0xC02E3000-0xC0478160
		*/
#endif
		// 将image_end的数据拷贝到新的内存区域(size to copy + 解压内核的结束地址)
		// r6 image_end
1:		ldmdb	r6!, {r0 - r3, r10 - r12, lr}
		cmp	r6, r5
		stmdb	r9!, {r0 - r3, r10 - r12, lr}
		bhi	1b	//if(r6 > restart的地址)跳转到1继续拷贝

		/*保留 offset 以重新定位的代码。*/
		sub	r6, r9, r6

		mov	r0, r9			@ 重新定位的 zImage 的开始
		add	r1, sp, r6		@ 重新定位的 zImage 结束
		bl	cache_clean_flush

		badr	r0, restart		//将 restart 标签的地址加载到 r0 寄存器中
		add	r0, r0, r6		//重定位后的代码的起始地址
		mov	pc, r0			//跳转到重定位后的代码的起始地址 restart处

Linflated_image_size_offset 解压后的内核大小偏移量

  • 减去 4 的目的是为了获取解压后内核镜像大小的地址,因为这个大小通常存储在 input_data_end 之前的4个字节中。减去当前地址 . 是为了得到一个相对偏移量,这样在运行时可以正确地访问解压后内核镜像大小的位置。
		.long	(input_data_end - 4) - .

get_inflated_image_size 获取解压后的内核大小

		/*
		 * The kernel build system appends the size of the
		 * decompressed kernel at the end of the compressed data
		 * in little-endian form.
		 * 执行之后res = 解压后的内核大小
		 */
		.macro	get_inflated_image_size, res:req, tmp1:req, tmp2:req
		//获取解压后的内核大小偏移量的地址-当前地址
		adr	\res, .Linflated_image_size_offset
		ldr	\tmp1, [\res]		//获取解压后的内核大小偏移量到\tmp1
		add	\tmp1, \tmp1, \res	@ 解压的图像大小地址 = 偏移量 + 当前地址
		//通过一系列的 ldrb 和 orr 指令以小端格式读取解压后镜像大小的四个字节。
		ldrb	\res, [\tmp1]		@ get_unaligned_le32
		ldrb	\tmp2, [\tmp1, #1]
		orr	\res, \res, \tmp2, lsl #8
		ldrb	\tmp2, [\tmp1, #2]
		ldrb	\tmp1, [\tmp1, #3]
		orr	\res, \res, \tmp2, lsl #16
		orr	\res, \res, \tmp1, lsl #24
		.endm
  • 验证如下:
    Image未压缩的,去除调试信息的内核大小为2992000(0x2DA780)字节
-rwxrwxr-x  1 embedsky embedsky 2992000 Mar  2 14:49 Image*

piggy_data的最后四字节转换成小端也为2992000(0x2DA780)字节

hexdump -C arch/arm/boot/compressed/piggy_data | tail -n 2
00190730  10 ca f1 73 80 a7 2d 00

wont_overwrite 修正LC0存储的内容,BSS区域的起始和结束地址,GOT表的起始和结束地址

wont_overwrite:
		adr	r0, LC0
		ldmia	r0, {r1, r2, r3, r11, r12}
		sub	r0, r0, r1		@ 偏移量 = 存储的LC0的地址 - 当前LC0地址

/*
 * 如果 delta 为零,我们正在运行链接地址。
 *   r0  = delta
 *   r2  = BSS 起始地址
 *   r3  = BSS 结束地址
 *   r4  = 内核执行地址(可能设置了 LSB)
 *   r5  = 附加的 dtb 大小(如果不存在则为 0)
 *   r7  = 架构 ID
 *   r8  = atags 指针
 *   r11 = GOT 起始地址
 *   r12 = GOT 结束地址
 *   sp  = 堆栈指针
 */
		orrs	r1, r0, r5	//delta 不为0 或者 附加的 dtb 大小不为0,则不会跳转到 not_relocated
		beq	not_relocated

		add	r11, r11, r0
		add	r12, r12, r0

#ifndef CONFIG_ZBOOT_ROM
	/*
		 * 如果我们完全运行 PIC === CONFIG_ZBOOT_ROM = n,
		 * 我们需要修正指向 BSS 区域的指针。
		 * 请注意,堆栈指针已经被修正。
		 */
		add	r2, r2, r0
		add	r3, r3, r0

		/*
		 * 重新定位GOT表中的所有条目。将bss条目提升到_edata + dtb大小
		 */
1:		ldr	r1, [r11, #0]		@ 重新定位 GOT 中的条目
		add	r1, r1, r0			@ 这修复了 C 引用
		cmp	r1, r2				@ if entry >= bss_start &&
		cmphs	r3, r1			@       bss_end > entry
		addhi	r1, r1, r5		@    entry += dtb size
		str	r1, [r11], #4		@ next entry
		cmp	r11, r12			@ if not end of GOT
		blo	1b					@ 跳到1处继续循环

		/* 也提升我们的 BSS 指针*/
		add	r2, r2, r5			@调整bss指针到dtb 大小后
		add	r3, r3, r5			@调整bss结束指针到dtb 大小后
#endif

not_relocated 重定向已完成执行

  1. 循环清除 BSS 区域
  2. 判断是否跳过缓存设置,如果跳过则直接调用cache_on开启缓存
  3. 传入参数进行解压内核
    • r0 = 内核执行地址
    • r1 = 堆栈上方的 malloc 空间
    • r2 = 表示动态内存分配空间的最大地址
    • r3 = 架构 ID
not_relocated:	mov	r0, #0
		// 循环清除 BSS 区域
1:		str	r0, [r2], #4		@ clear bss
		str	r0, [r2], #4
		str	r0, [r2], #4
		str	r0, [r2], #4
		cmp	r2, r3
		blo	1b

		/*
		 * 我们之前跳过了缓存设置吗?
		 * 这由 r4 中的 LSB 表示。
		 * 如果是这样,现在就做。
		 */
		//这`1 使用位置无关执行重定向栈顶地址,并开启缓存页表`中进行判断
		//设置之后的页表超过了内核映像,则跳过cache_on不进行设置,否则直接进行cache_on
		tst	r4, #1		//将 r4 寄存器的值与立即数1进行按位与操作,并根据结果更新条件标志
		bic	r4, r4, #1	//清除最低有效位
		blne	cache_on	//如果 tst 指令的结果表明 r4 的最低有效位为1(即之前跳过了缓存设置)

/*
 * 现在应该已经充分设置了 C 运行环境。
 * 设置一些指针,然后开始解压。
 * r4 = 内核执行地址
 * r7 = 架构 ID
 * r8 = atags 指针
 */
		mov	r0, r4
		mov	r1, sp			@ 堆栈上方的 malloc 空间
		add	r2, sp, #MALLOC_SIZE	@ 64k max
		mov	r3, r7
		/*
		* r0 = 内核执行地址
		* r1 = 堆栈上方的 malloc 空间
		* r2 = 表示动态内存分配空间的最大地址
		* r3 = 架构 ID
		*/
		bl	decompress_kernel
		/*
			进入这个宏之后
			r1 = 解压后的镜像大小
		*/
		get_inflated_image_size	r1, r2, r3

		mov	r0, r4			@ 解压镜像的开始地址
		add	r1, r1, r0		@ 解压镜像的结束地址
		bl	cache_clean_flush
		bl	cache_off
		mov	r0, r4			@ start of inflated image
		add	r1, r1, r0		@ end of inflated image
		bl	cache_clean_flush
		bl	cache_off

#ifdef CONFIG_ARM_VIRT_EXT	//ARM 虚拟化

#else
		b	__enter_kernel	//跳转进入内核
#endif

__enter_kernel 进入内核

  1. V7M不执行ARM,执行THUMBM_CLASS
  2. r4为内核的入口地址
		mov	r0, #0				@ must be 0
		mov	r1, r7				@ 恢复架构编号
		mov	r2, r8				@ 恢复 Atags 指针
 ARM(		mov	pc, r4		)	@ call kernel
 M_CLASS(	add	r4, r4, #1	)	@ 在 M 类的 Thumb 模式下进入
 THUMB(		bx	r4		)		@ 对于 A/R 类,入口点始终为 ARM

网站公告

今日签到

点亮在社区的每一天
去签到