arm-linux内核启动学习笔记(一)

版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/45043155
更多内容可关注微信公众号在这里插入图片描述

##arm-linux 内核的启动

  • 这里的分析是从./arch/arm/boot/compressed/head.S:start开始的,这个start标签是zImage的入口代码。 ./arch/arm/kernel里也有个head.S,这个head.S中的stext标签,就是zImage中decompress_kernel之后要跳转的地址,即Image的入口代码(见 arm-linux内核编译过程小结)。
  • arm启动后执行的第一条指令并不是head.S:start,在此之前一般都会有一个平台相关的bootloader(如mt6582的代码就是平台相关的代码,在mtk平台上bootloader分为preloader和lk两部分)来初始化一些平台相关的信息,然后再跳转到start标签,来执行体系结构相关的代码(如arm的代码就是体系结构相关的代码)。
  • bootloader部分(如mtk平台中preloader和lk部分)的代码会完成对硬件的默认初始化,准备start的结构ID和atags两个参数。

##Part1: [start ,cache_on]
这部分组要是找到平台相关的cache_on函数,然后调用之。cache_on函数的左右就是开启缓存,
一般需要通过初始化页表并使能mmu来实现。

//部分关系不大代码已省略
start:
//指定start这个符号是函数类型
		.type	start,#function
//.rept count .endr 把rept和endr之间的指令重复count次
//mov r0,r0 实际上就是nop指令,这里的意思应该是是清空指令流水线
		.rept	7
		mov	r0, r0
		.endr
	    ARM(mov	r0, r0)
//跳到前面(下面的1)
	    ARM(b	1f)

//这个魔数是在bootloader中用于判断zImage存在的,是内核和bootloader约定好的(mtk中参见lk代码)
		.word	0x016f2818		
//这里存的是start的默认加载地址,这个地址是编译时确定的,与代码最终加载到哪里无关,
//但如果start最终加载的物理地址(zImage这会没开mmu,所以物理地址)
//不是这个编译地址的话,可能需要重定位。
		.word	start			
//zImage的结束地址,也是编译地址,这个值是在连接的时候确定的,其定义在vmlinux.lds中
		.word	_edata			@ zImage end address
//THUMB宏在这里没有定义的,所以这里没啥用,如果定义了,.thumb表示往下都是用thumb指令集。
		THUMB(.thumb)
		
//r1, r2分别存着bootloader传递过来的结构ID和atags
1:		mov	r7, r1			@ save architecture ID
		mov	r8, r2			@ save atags pointer

//关中断
#ifndef __ARM_ARCH_2__
//__ARM_ARCH_2__下的关中断方式,goldfish中也是这种方式
		mrs	r2, cpsr		@ get current mode
		tst	r2, #3			@ not user?
		bne	not_angel
		mov	r0, #0x17		@ angel_SWIreason_EnterSVC
		ARM(swi	0x123456)	@ angel_SWI_ARM
		THUMB(svc 0xab)	    @ angel_SWI_THUMB
not_angel:
		mrs	r2, cpsr		@ turn off interrupts to
		orr	r2, r2, #0xc0	@ prevent angel from runing
		msr	cpsr_c, r2
#else
//arm 版本2,3的核心的关中断方式
		teqp	pc, #0x0c000003		@ turn off interrupts
#endif
		.text
#ifdef CONFIG_AUTO_ZRELADDR
		@ determine final kernel image address
		mov	r4, pc
		and	r4, r4, #0xf8000000
		add	r4, r4, #TEXT_OFFSET
#else
//我的内核配置了zreladdr,将其加载到r4,这是Image的加载地址
		ldr	r4, =zreladdr
#endif
		bl	cache_on

反汇编后的代码:

ROM:00000000                 AREA ROM, CODE, READWRITE, ALIGN=0
ROM:00000000                 CODE32
; mov r0, r0 被解释成了nop指令,这里应该是用来清空指令流水的
ROM:00000000                 NOP                     
ROM:00000004                 NOP                     
ROM:00000008                 NOP
ROM:0000000C                 NOP
ROM:00000010                 NOP
ROM:00000014                 NOP
ROM:00000018                 NOP
ROM:0000001C                 NOP
ROM:00000020                 B close_IRQ
ROM:00000020 ; ----------------------------------------
; zImage魔数
ROM:00000024                 DCD 0x16F2818      
; start 标签的编译地址,这里编译地址就是0,也就是说zImage默认被加载到0地址     
ROM:00000028                 DCD 0                   
; 这个数与 ll zImage的大小完全一样,也是zImage编译的结束地址。
ROM:0000002C                 DCD 0x27A1F8           
ROM:00000030 ; ----------------------------------------
ROM:00000030
ROM:00000030 close_IRQ  	; CODE XREF: ROM:00000020j
ROM:00000030                 MOV             R7, R1
ROM:00000034                 MOV             R8, R2
ROM:00000038                 MRS             R2, CPSR
ROM:0000003C                 TST             R2, #3
ROM:00000040                 BNE             not_angel
ROM:00000044                 MOV             R0, #0x17
ROM:00000048                 SVC             0x123456
ROM:0000004C
ROM:0000004C not_angel      ; CODE XREF: ROM:00000040j
ROM:0000004C                 MRS             R2, CPSR
ROM:00000050                 ORR             R2, R2, #0xC0
ROM:00000054                 MSR             CPSR_c, R2
ROM:00000058                 ANDEQ           R0, R0, R0
ROM:0000005C                 ANDEQ           R0, R0, R0
; 在goldfish上,r4 存入立即数 0x00008000,为Image的默认加载地址
ROM:00000060                 LDR         R4,=dword_8000 
ROM:00000064                 BL              cache_on

###cache_on
cache_on函数主要的功能是根据处理器ID,找到处理器对应的体系结构(armXXX)的cache_on函数并调用。kernel支持的所有arm型号的cache_on函数都会包含在zImage里面(这个head.S文件中),如果有新的arm处理器,内核想要支持的话,就需要在kernel.org提交这部分的patch。

//这个8 是proc_types数组中的第三个元素,代表某个处理器型号中cache_on函数的地址。
cache_on:	mov	r3, #8			@ cache_on function
		b	call_cache_fn

cache_on->call_cache_fn

//call_cache_fn为一个循环,根据当前cpu的魔数(可能是从cp15中获取,或编译时指定的),找到并调用对应处理器的cache_on函数
call_cache_fn:	adr	r12, proc_types

//获取处理器ID,goldfish中是通过cp15获取的处理器ID
#ifdef CONFIG_CPU_CP15
		mrc	p15, 0, r9, c0, c0	@ get processor ID
#else
		ldr	r9, =CONFIG_PROCESSOR_ID
#endif

//可以先到下面看下proc_types的结构,这里以第一个循环为例
//r1 = proc_types[0] = 0x41560600 代表ARM6/610
1:		ldr	r1, [r12, #0]		@ get value
//r2位掩码 r2 = proc_types[1] = 0xffffffe0
		ldr	r2, [r12, #4]		@ get mask
//EOR <Rd>,<Rn>, <shifter_operand>, 将shifter_operand与Rn做异或操作,结果存于Rd
		eor	r1, r1, r9		@ (real ^ match)
//TST <Rn>, <shifter_operand>, Rn - shifter_operand,用结果更新标志位
		tst	r1, r2			@       & mask
//如果r1 = r2,代表处理器匹配,跳转到对应的cache_on函数
		ARM(addeq	pc, r12, r3) @ call cache function
		THUMB(addeq	r12, r3)
		THUMB(moveq	pc, r12) @ call cache function
//如果不一致,则增加到下一个位置,继续测试
		add	r12, r12, #PROC_ENTRY_SIZE
//如果不匹配,则往上跳,循环。
		b	1b

proc_types里面记录各个arm处理器的魔数,掩码,以及相应的cache函数,如下:

/*
 *   - CPU ID match
 *   - CPU ID mask
 *   - 'cache on' method instruction
 *   - 'cache off' method instruction
 *   - 'cache flush' method instruction
*/
		.align	2
		.type	proc_types,#object
proc_types:
		.word	0x41560600		@ ARM6/610
		.word	0xffffffe0
		W(b) __arm6_mmu_cache_off	@ works, but slow
		W(b)	__arm6_mmu_cache_off
		mov	pc, lr
		.word	0x41007000		@ ARM7/710
		.word	0xfff8fe00
		W(b)	__arm7_mmu_cache_off
		W(b)	__arm7_mmu_cache_off
		mov	pc, lr
		THUMB(		nop				)
		.word	0x41807200	@ ARM720T (writethrough)
		.word	0xffffff00
		W(b)	__armv4_mmu_cache_on
		W(b)	__armv4_mmu_cache_off
		mov	pc, lr
		THUMB(		nop				)
		.word	0x41007400		@ ARM74x
		.word	0xff00ff00
		W(b)	__armv3_mpu_cache_on
		W(b)	__armv3_mpu_cache_off
		W(b)	__armv3_mpu_cache_flush
		
		.word	0x41009400		@ ARM94x
		.word	0xff00ff00
		W(b)	__armv4_mpu_cache_on
		W(b)	__armv4_mpu_cache_off
		W(b)	__armv4_mpu_cache_flush

		.word	0x41069260		@ ARM926EJ-S (v5TEJ)
		.word	0xff0ffff0
		W(b)	__arm926ejs_mmu_cache_on
		W(b)	__armv4_mmu_cache_off
		W(b)	__armv5tej_mmu_cache_flush
		
		......

这里以__armv4_mmu_cache_on为例(goldfish 应该是armv7的处理器,看/proc/cpuinfo ):
cache_on->call_cache_fn->__armv4_mmu_cache_on

//cache_on函数做了两件事:初始化页表,开启mmu,因为只有在开启mmu的情况下才能使用cache
__armv4_mmu_cache_on:
//这个函数是通过addeq	pc, r12, r3指令进来的,
//这里保存的lr是start标签的那句bl cache_on之后的位置的
		mov	r12, lr
#ifdef CONFIG_MMU
//开启缓存相关,缓存具体怎么开怎么用,这里先不管,
//主要先看__setup_mmu是如何启动分页机制的
		bl	__setup_mmu
//高速缓存相关操作
		mov	r0, #0
		mcr	p15, 0, r0, c7, c10, 4	
		mcr	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg
		orr	r0, r0, #0x5000		
		orr	r0, r0, #0x0030
//这个函数中有一步是将页表基地址写入cp15.c2寄存器,就是开启mmu。
		bl	__common_mmu_cache_on
		mov	r0, #0
		mcr	p15, 0, r0, c8, c7, 0	
//返回到start标签的bl cache_on后面那一句
		mov	pc, r12

__setup_mmu

调用流程是:cache_on->call_cache_fn->__armv4_mmu_cache_on->__setup_mmu

/* 跳转到__setup_mmu之前:
 * r4 = zreladdr,为Image的加载地址(在goldfish里面为0x00008000,
 * 有的系统为0x3/5/7/0008000,这个值在./arch/arm/$(MACH)/
 * Makefile.boot中有定义,$(MACH)指的是具体的芯片)。
 * Image前的16KB内存(glodfish上默认为0x00004000 - 0x00008000)
 * 是内核的整个页表,大小为16K。
 * 
 *  __setup_mmu函数的主要工作是初始化全局页表(Image加载地址向前16KB)
 * 将部分区域加上C/B属性(cache和write buffer),其他区域就直接默认属性了,
 * 这里将当前pc所在的两页和解压后的内核所在的256MB空间加上了C/B属性
 * 估计因为这一片可能为代码区域,加上C/B属性可以提升访问速度???
 */ 

//r3 = r4 - 0x4000(16K) 最终r3 = 0x00004000,为页表基地址(pgd) 
__setup_mmu:	sub	r3, r4, #16384
//r3 = r3 & 0xffffc000 (清除r3的0x3fff位)
		bic	r3, r3, #0xff		@ Align the pointer
		bic	r3, r3, #0x3f00
//当前页表项指针 r0 = 0x00004000
		mov	r0, r3
//要初始化为C/B属性的起始物理地址,这里r9 = 0x0
		mov	r9, r0, lsr #18
		mov	r9, r9, lsl #18		@ start of RAM
//要初始化为C/B属性的结束物理地址 r10 = r9 + 256MB
		add	r10, r9, #0x10000000	
//r1 = 0xc12, 这是默认的页表属性
		mov	r1, #0x12
		orr	r1, r1, #3 << 10
//页表项的结束位置 r2 = 0x00008000
		add	r2, r3, #16384
//到这里开始初始化页表了,当前的r1是0xc12,同时也能代表虚拟地址0x00000000所在的页
//因为0xc12是个很小的数。每次循环r1都会递增1MB,在每一次循环中都设定r1
//代表的这1MB虚拟地址如何映射。这里做的是一个恒等映射,唯一不同就是各个区段
//的映射属性不同
1:		cmp	r1, r9			@ if virt > start of RAM
//如果r1 > r9 则执行 r1 = r1|0x0C = 0x0C1E
		orrhs	r1, r1, #0x0c	
//如果 r1 > r10(过了0x10004000),则清除缓存标记
		cmp	r1, r10			@ if virt > end of RAM
		bichs	r1, r1, #0x0c		
//r1是当前页描述符,r0是当前页表项的地址,这里是向当前页表项写入页描述符: *r0 = r1; r0 += 4;
		str	r1, [r0], #4		
//r1 += 0x100000 = 2^20 = 1MB,设置下一个页描述符的内容
		add	r1, r1, #1048576
//如果r0 != r2(没到页表项结束位置)则向后(向上)跳转到1,实现循环。
//这里是将整个0x00004000- 0x00008000处的内存全部初始化作为页表。
		teq	r0, r2
		bne	1b
//16KB页表代表内核4GB空间,其初始化到这里结束,后面修正当前pc所在的虚拟地址
//(等于物理地址)对应的页表的属性了,这是怕pc不在前面那个0xC1E的范围内。
//r1 = 0x0C1E
		mov	r1, #0x1e
		orr	r1, r1, #3 << 10
//要修正pc所在页的地址
		mov	r2, pc
//段对齐
		mov	r2, r2, lsr #20
//加上页属性,r1为要写入的页表项内容(恒等映射)
		orr	r1, r1, r2, lsl #20
//获取pc所在页的虚拟地址在全局页表pgd中的页表项的地址 -> r0
		add	r0, r3, r2, lsl #2
//修改pc所在页和其后面一页的页表属性为0xC1E
		str	r1, [r0], #4
		add	r1, r1, #1048576
		str	r1, [r0]
//返回
		mov	pc, lr
ENDPROC(__setup_mmu)

/* 最终一级页表的内容:
其中:
第一列是各个页描述符的物理地址
第二列是各个页描述符的内容,整个第二列就是物理内存中的一级页表
第三列是表示当前页描述符处于一级页表中的第几个页表项。
/物理地址/页描述符内容/当前是第几个页表项/
|0x00004000|0x00000C1E|0000| //最后单独修改的两项页表项
|0x00004004|0x00100C1E|0001| //页表项内容>0x00004000
|0x00004008|0x00200C1E|0002| //<0x10004000的属于C/B区
|0x0000400C|0x00300C1E|0003| 
......
|0x00004400|0x10000C1E|0256| //页表项内容<0x10004000
|0x00004404|0x10100C12|0257| //页表项内容>0x10004000
|0x00004408|0x10200C12|0258|
......
|0x00007FFC|0xFFF00C12|4095|
一个一级页描述符作为段描述符时,可表示的地址范围为2^20 = 1MB(见后面一级页描述符), 32位cpu的寻址空间为4GB,所以一共需要4096个一级页描述符。前面可以看到16KB,刚好能存储4096个页描述符,所以内核初始化的时候,页表的大小为16KB. 16K/4096 = 4B 一个地址。

此恒等映射最终的结果就是,4GB的任意虚拟地址 = 物理地址。
*/

##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");
}

这里要注意一点的是:decompress_kernel函数中就已经开始调用打印函数了,这一句

putstr("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();
}

而这个putc,具体平台实现的方式不同,在goldfish上:

//./arch/arm/mach-goldfish/include/mach/uncompress.h
#define GOLDFISH_TTY_PUT_CHAR (*(volatile unsigned int *)0xff002000)
static void putc(int c)
{
		//向IO端口0xff002000直接写入字符,这就是goldfish的uart端口
        GOLDFISH_TTY_PUT_CHAR = c;
}

在MT6582上,也是通过一个地址写入的,如下:

//./mediatek/platform/mt6582/kernel/core/include/mach/uncompress.h
#define MT_UART_PHY_BASE 0x11002000
#define MT_UART_THR *(volatile unsigned char *)(MT_UART_PHY_BASE+0x0)
static inline void putc(int c)
{
   //向IO端口0x11002000直接写入字符,这就是mt6582的uart端口
   //这一句while循环是测试控制位的,具体位作用需参考板子的手册。
   while (!(MT_UART_LSR & 0x20));
   MT_UART_THR = c;
}

这种写入也就是在内核刚开始,有恒等映射的时候,后续page_init二次映射的时候这些地址都会映射到内核的内核空间的其他地方了(后续分析)。

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

  • arm MMU 一级页表寻址:
    arm协处理器CP15的C2寄存器记录着页基地址(又可以叫做一级页表基地址),对于一个地址addr, 在arm中第一级寻址算法为:C2[31:14] + addr[31:20] + 00,如图:

这里写图片描述
注1:
1) C2中记录的页表基地址,又可以叫做一级页表基地址
2) 一级页表基地址中的每一项元素叫做一级页表项
3) 一级页表项中的内容叫做一级页表描述符
4) 一级页表描述符,描述的是否为一个二级页表,这个得看描述符的最后两位是什么。
注2: 一级寻址只与页表基地址的前18位(一级页表基地址),虚拟地址的前12位(一级页表项的页内偏移)有关 (+最后两位00),没有任何标记位,mmu通过将二者组合成一个32位物理地址,这个就是一级页表项的物理地址,从这个物理地址中读取到的内容就是一级页表描述符。

  • 一级页表描述符:
    arm 开启MMU后,当用户访问一个虚拟地址时,先根据C2和虚拟地址的前12位标记的物理地址中取出一个一级页表描述符,虚拟地址中剩下20位如何解析,是由这个一级页表描述符的低2位决定的。一共四种组合,对应四种不同的解析方式。在__setup_mmu中初始化的一级页表项的最后两位都是10,这里先介绍10。
    一级页表描述符[1:0]为0b10,表示该一级描述符为段描述符。该一级描述符应该如下解析:
    这里写图片描述

虚拟地址->物理地址的过程:
这里写图片描述

这个过程的总体描述如下:

  1. 用户访问一个虚拟地址addr。
  2. MMU取出addr的前12位,并根据C2定位一级页表描述符X。
  3. MMU 发现 X的最后两位为10,则取出x的前12位,拼接addr的后20位,组成物理地址。
  4. 访问此物理地址,获取数据返回给用户。

###part2
在zImage的反汇编代码中,input_data在_got_start段,值为0x4015,如下:
这里写图片描述

查看其内容:
这里写图片描述

查看piggy.gz:
这里写图片描述

可知整个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(大)的入口函数,也是体系结构相关的!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值