天天看点

arm-linux内核启动学习笔记(二)(废弃)

更多内容可关注微信公众号
arm-linux内核启动学习笔记(二)(废弃)

#已合并到arm-linux内核启动学习笔记(一)

###Part2:(cache_on,call_kernel]

####vmlinux(小)的链接脚本:

//./arch/arm/boot/compressed/vmlinux.lds.in
//这个脚本是用来链接vmlinux(小)的,故zImage代码执行时
//内核各个段的分布也是在这个脚本中定义的(部分代码已省略)。
OUTPUT_ARCH(arm)
ENTRY(_start)
SECTIONS
{
  . = TEXT_START;
  _text = .;
  .text : {
    _start = .;
    *(.start)
    *(.text)
    *(.text.*)
    *(.fixup)
    *(.gnu.warning)
    *(.glue_7t)
    *(.glue_7)
  }
  .rodata : {
    *(.rodata)
    *(.rodata.*)
  }
  //这里是piggy.gz的位置
  .piggydata : {
    *(.piggydata)
  }
  . = ALIGN(4);
  _etext = .;
  .got.plt		: { *(.got.plt) }
  _got_start = .;
  .got			: { *(.got) }
  _got_end = .;

  .pad			: { BYTE(0); . = ALIGN(8); }
  _edata = .;

  . = BSS_START;
  __bss_start = .;
  .bss			: { *(.bss) }
  _end = .;
  
  . = ALIGN(8);		/* the stack must be 64-bit aligned */
  .stack		: { *(.stack) }

  .stab 0		: { *(.stab) }
  .stabstr 0		: { *(.stabstr) }
  .stab.excl 0		: { *(.stab.excl) }
  .stab.exclstr 0	: { *(.stab.exclstr) }
  .stab.index 0		: { *(.stab.index) }
  .stab.indexstr 0	: { *(.stab.indexstr) }
  .comment 0		: { *(.comment) }
}

           

####piggy.gzip.S

piggy.gzip.S是用来生成piggy.gzip.o的,其实际上相当于将一个piggy.gzip文件当

二进制打包到vmlinux(小)中了。

//./arch/arm/boot/compressed/piggy.gzip.S
	.section .piggydata,#alloc
	//global只是声明,并不分配空间
	.globl	input_data
input_data:
##INCBIN 指令在被汇编的文件内包含一个文件。 该文件按原样包含,没有进行汇编。
	.incbin	"arch/arm/boot/compressed/piggy.gzip"
	.globl	input_data_end
input_data_end:
           

####cache_on之后

cache_on之后段代的代码大体可认为做了三件事:

  1. 检查内核是否需要移动,如果需要移动则将其移动并重定向.got的内容。
  2. 调用decompress_kernel解压内核。
  3. 调用call_kernel跳转到Image。
//cache_on函数的实际作用是开启高速缓存,这样可以加快后续代码的执行速度
		//在arm体系结构中,高速缓存TLB必须依赖于mmu,所以cache_on函数内部通过
		//调用__setup_mmu来初始化页表(代码段的页表属性允许缓存)
		//调用__common_mmu_cache_on来使能mmu
		bl	cache_on
		//跳转到这里,mmu已经开启了,恒等映射!
/*		
		LC0的代码本来是在后面的,这里为了方便解释,先在前面列出了
		各个代码段的分布见后(这些值都是编译地址!)。
LC0:		.word	LC0			@ r1
		//bss段的开始位置
		.word	__bss_start		@ r2
		//bss的结束位置
		.word	_end			@ r3
		//数据段的结束位置
		.word	_edata			@ r6
		//pizzy.gz的结束位置
		.word	input_data_end - 4	@ r10 (inflated size location)
		.word	_got_start		@ r11
		.word	_got_end		@ r12 (ip)
		//栈的结束位置
		.word	.L_user_stack_end	@ sp
		//这个应该是个伪指令,指定LC0大小的
		.size	LC0, . - LC0
*/
		//adr是相对于当前pc的相对寻址,一般用于获取指令的真是地址,而不是加载地址
restart:	adr	r0, LC0
		//把上面LC0中的一堆变量载入到各个寄存器中
		ldmia	r0, {r1, r2, r3, r6, r10, r11, r12}
		//设置临时栈, sp = L_user_stack_end,目前sp还指向栈底
		ldr	sp, [r0, #28]

		/*
			zImage的加载地址未必是编译地址,所以这里需要
			将各个变量由编译地址修正为真实地址。
		*/
		//计算LC0的真实地址(既是物理地址,又是虚拟地址)和编译地址的差值
		sub	r0, r0, r1		@ calculate the delta offset
		//将_edata的编译地址转为真实地址
		add	r6, r6, r0		@ _edata
		//将input_data_end的编译地址转为真实地址
		add	r10, r10, r0		@ inflated kernel size location

		//将sp的编译地址修正为真实地址,并向上增加64K,作为栈顶
		add	sp, sp, r0
		add	r10, sp, #0x10000

		/*
		 * The kernel build system appends the size of the
		 * decompressed kernel at the end of the compressed data
		 * in little-endian form.
		 */
		/*
			 r9是piggy.gzip这个gzip文件的最后四个字节,这四个字节记录的
			 是解压后的Image的大小存的.在我这里piggy.gzip最后: 30c1 b500 0a
			 Image大小为0xb5c130, 最后那个0a估计最后会去掉的。
			 这里用ldrb指令是考虑到这个存Image大小的位置可能不是4byte对齐的。
		 */
		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
		
/*
 *   这一段代码是来检测是否需要复制自身的
 *   r4是最终内核要解压到的地址,是通过zreladdr来指定的
 *   r9是Image镜像的大小,是从piggy.gz文件末尾获取的
 *   r10是piggy.gz的结束位置
 * 往下执行需要满足两个条件之一:
 *   1) 最终内核要解压到的地址(r4) - 16k页表 >= zImage的基本代码结束位置(r10)
 *   2) 最终内核解压后的结束地址(r4 + r9(image length)) <= 后续执行的代码(wont_overwrite的地址)
0x00000000 -----------------------------------------------------------------------------

												------ Image(zreladdr)
												  |
												  |
     ----- zImage起始地址                          | 
	   |                                          |
	   |                                          |
	 ----- 当前代码(pc)                          ------ Image end 在此之上都可以
	 ----- wont_overwrite的代码                      
	   |
     ----- zImage基本代码结束(piggy.gz末尾,r10)
		     									----- Image(zreladdr)
				     								  ;如果解压到这个位置往下都是可以的
									              |
									              |
									            ----- Image end
									             
0xffffffff -----------------------------------------------------------------------------
 */

 		//这里r10 += 0x4000是预留给页表的
		add	r10, r10, #16384
		cmp	r4, r10
		//如果r4 >= r10则满足条件1,Image要解压的地址在zImage后面,
		//跳转到wont_overwrite,不需移动自身代码。
		bhs	wont_overwrite
		//r10 = r4 + r9, r10为预计解压后的image的结束地址
		add	r10, r4, r9
		//获取wont_overwrite的物理地址
		adr	r9, wont_overwrite
		//如果否满足条件2,即image解压后没有覆盖wont_overwrite
		//之后的代码,则也无需移动自身代码。
		cmp	r10, r9
		bls	wont_overwrite
/*
 * Relocate ourselves past the end of the decompressed kernel.
 *   r6  = _edata
 *   r10 = end of the decompressed kernel
 * Because we always copy ahead, we need to do it from the end and go
 * backward in case the source and destination overlap.
 */
		/*
		 * Bump to the next 256-byte boundary with the size of
		 * the relocation code added. This avoids overwriting
		 * ourself when the offset is small.
		 */
		//否则就需要移动代码的位置了,这里的逻辑是把从restart到zImage结束的代码
		//移动到Image预计解压后的结束位置的后面,为什么从restart开始见下面。
		add	r10, r10, #((reloc_code_end - restart + 256) & ~255)
		//这里的r10应该就是最终要移动到的地址了
		bic	r10, r10, #255

		/* Get start of code we want to copy and align it down. */
		//要复制的代码的起始地址
		adr	r5, restart
		//清除末尾位
		bic	r5, r5, #31
		//要复制的数据大小
		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
		//要复制到的结束位置
		add	r9, r9, r10

		//复制数据
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. */
		sub	r6, r9, r6

		//代码被移动过了,所以清缓存
		bl	cache_clean_flush
		//跳转到restart重新执行,因为当前地址变了,所以LC0的当前地址和编译地址
		//的差值就变了,前面的好多变量都是根据这个差值算出来的,所以这里跳到
		//restart重新来过,所以前面检测的时候是检测wont_overwrite之前
		//的代码是否被覆盖,而移动得要从restart的代码开始移动。
		adr	r0, BSYM(restart)
		add	r0, r0, r6
		mov	pc, r0
		//到这里
		
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
 *   r5  = appended dtb size (0 if not present)
 *   r7  = architecture ID
 *   r8  = atags pointer
 *   r11 = GOT start
 *   r12 = GOT end
 *   sp  = stack pointer
 */
		//r5在一开始被初始化为0
		orrs	r1, r0, r5
		//这里是如果运行地址与链接地址相等则跳转到not_relocated
		//不执行got段的重定位
		beq	not_relocated
		
		//否则将r11,r12存的GOT表的编译地址修改为当前地址
		add	r11, r11, r0
		add	r12, r12, r0
		//修正bbs段的起始,结束地址
		add	r2, r2, r0
		add	r3, r3, r0
		//对GOT表中的所有元素做重定位
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
		str	r1, [r11], #4		@ next entry
		cmp	r11, r12
		blo	1b
		/* bump our bss pointers too */
		add	r2, r2, r5
		add	r3, r3, r5

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

/*
 * 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
 */
		mov	r0, r4
		mov	r1, sp			@ malloc space above stack
		add	r2, sp, #0x10000	@ 64k max
		mov	r3, r7
		//这个即是将piggy.gz解压为Image的函数,这里是c代码
		bl	decompress_kernel
		//解压后再次刷新缓存(个人理解应该是代码区域有代码变动就应该刷新缓存)
		bl	cache_clean_flush
		bl	cache_off
		mov	r0, #0			@ must be zero
		mov	r1, r7			@ restore architecture number
		mov	r2, r8			@ restore atags pointer
		//r4是Image的入口地址,也是Image的解压地址,Image
		//本身是个binary文件,第一个字节即为指令,这里跳转到Image
		//即./arch/arm/kernel/head.s:stext
 ARM(	mov	pc, r4	)		@ call kernel
           

###decompress_kernel

decompress_kernel是用c函数完成的,其代码如下

//./kernel/arch/arm/boot/compressed/Misc.c
extern char input_data[];
void decompress_kernel(
	unsigned long output_start,        //zImage的解压地址(r4)
	unsigned long free_mem_ptr_p,      //临时空间,解压用(这里用的是栈)
	unsigned long free_mem_ptr_end_p,  //临时空间结尾 
	int arch_id)                       // 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();
	//putstr是内核启动早期的打印函数,一般都是直接向IO端口写的数据
	putstr("Uncompressing Linux...");
	//内核解压函数,这里用的是gzip解压,这个input_data和input_data_end
	//定义在piggy.gzip.S中,是piggy.gzip的起始和结束位置。
	ret = do_decompress(input_data, input_data_end - input_data, output_data, error);
	if (ret)
		error("decompressor returned an error");
	else
		putstr(" done, booting the kernel.\n");
}
           

这里的这一句”Uncompressing Linux…”在goldfish启动的时候是可以打印出来的,在真实设别上(如MT6582),通过串口也是可以看到这些日志的,其实现如下:

./kernel/arch/arm/boot/compressed/Misc.c
static void putstr(const char *ptr)
{
    char c;
    while ((c = *ptr++) != '\0') {
        if (c == '\n')
            putc('\r');
        putc(c);
    }
    flush();
}
           

###旧版废弃的内容,但是正确的

在zImage的反汇编代码中,input_data在_got_start段,值为0x4015,如下:

arm-linux内核启动学习笔记(二)(废弃)

查看其内容:

arm-linux内核启动学习笔记(二)(废弃)

查看piggy.gz:

arm-linux内核启动学习笔记(二)(废弃)

可知整个zImage,实际上就是vmlinux(2.51MB那个)掐头去尾一点,vmlinux是个标准ELF文件,而zImage就是这个vmlinux去了ELF头尾组成的,其二进制差异不过1%20.

也就是说,实际上是vmlinux包含了piggy.gz,而将其裁剪一点,就形成了最终的zImage。

  1. vmlinux(小的那个),是一个标准的elf文件,将其掐头去尾后就形成了zImage,这个就是最终的内核镜像。
  2. zImage是最终的内核镜像,其内部包含了一个原封不动的piggy.gz,这是一个压缩后的内核。
  3. piggy.gz是image通过gzip命令压缩来的。
  4. Image文件是一个纯二进制文件,没有elf头,zImage中的最后一句 MOV PC, R4 ; call_kernel,这个R4就是之前kernel解压的地址,就是直接跳到了Image的相对偏移0位置的指令。
  5. Image是从vmlinux(大)中抽取出来的,是objcopy -o binary vmlinux(大)来的。vmlinux(大)入口地址的第一条指令,就是Image这个二进制文件的第一条指令。

所以综上所述,zImage解压内核,跳转到Image偏移0处的指令,实际上相当于执行了vmlinux(大)的入口函数。而vmlinux(大)的入口函数为ENTRY(stext),定义在./arch/arm/kernel/head.S(注,zImage的入口函数定义在./arch/arm/boot/compressed/head.S),也就是说vmlinux(大)的入口函数,也是体系结构相关的!

继续阅读