kernel 启动流程之 【head.S】 学习笔记

ARM 128浏览

1、系统引导程序 【参考:kernel-3.18/Documentation/arm/Booting】

bootloader 要干的主要事情:

  • 找到并初始化内存;

  • 初始化和使能一个串口输出,这个对于调试很重要;

  • 获取CPU类型,指的是具体体现结构CPU的类型,比如ARM Cortex-x系列;

  • 初始化kernel tagged list,向kernel传递系统内存和根文件系统位置和其它信息;

  • 加载 initramfs (基于ram的一种fs);

  • 初始化设备树,跳转到kernel代码段入口stext;

进入kernel执行前的CPU的状态:

r0 = 0,
r1 = CPU 类型.
r2 = kernel参数list的物理地址.
irq & fiq 必须关闭.
MMU 必须关闭,这里内存地址都是物理地址.
D-cache 必须关闭,I-cache 没有要求.

2、Kernel 启动

入口程序是stext (参考 kernel-3.18/arch/arm/kernel/vmlinux.lds.S), 代码位于 kernel-3.18/arch/arm/kernel/head.S 文件中,如下代码: 【head.S 从系统引导程序拿到系统控制权,代码跟体系结构强相关,深入分析需要查阅数据手册学习ARM指令集和体系结构相关知识,这里只关注启动主线流程,不做深入分析】

 
  
/*

 * Kernel startup entry point.
 * ---------------------------
 *
 * This is normally called from the decompressor code.  The requirements
 * are: MMU = off, D-cache = off, I-cache = dont care, r0 = 0,
 * r1 = machine nr, r2 = atags or dtb pointer.
 *
 * This code is mostly position independent, so if you link the kernel at
 * 0xc0008000, you call this at __pa(0xc0008000).
 *
 * See linux/arch/arm/tools/mach-types for the complete list of machine
 * numbers for r1.
 *
 * We're trying to keep crap to a minimum; DO NOT add any machine specific
 * crap here - that's what the boot loader (or in extreme, well justified
 * circumstances, zImage) is for.
 */

	.arm

 __HEAD                @ .section ".head.text","ax"

 ENTRY(stext)          @ 只读内存布局代码段函数入口.定义一个全局声明
 ARM_BE8(setend	be )

 THUMB(	adr	r9, BSYM(1f)	)
 THUMB(	bx	r9		)
 THUMB(	.thumb			)
 THUMB(1:			)

	bl	__hyp_stub_install       @ 允许kernel(EL1)切换到hypervisor mode(EL2)

	safe_svcmode_maskall r9          @ 关闭所有fiq,irq,切换到svc(管理)mode.

	mrc	p15, 0, r9, c0, c0		@ 协处理器指令,获取cpu id,比如armv7 or armv8
	bl	__lookup_processor_type
	movs	r10, r5			        @ 判断cpu id是否有效,movs是会影响cpsr进位bit
    THUMB( it	eq )
	beq	__error_p


	adr	r3, 2f
	ldmia	r3, {r4, r8}                    @ 计算物理地址偏移
	sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET)
	add	r8, r8, r4			@ PHYS_OFFSET

	bl	__vet_atags

	bl	__fixup_smp

	bl	__create_page_tables            @ 创建临时页表.

	ldr	r13, =__mmap_switched           @ 将 __mmap_switched 地址赋值给r13,关键!
						@ 使能mmu,因为内核中都是虚拟地址,
						@ 需要使能mmu实现虚拟地址到物理地址的转换。
	adr	lr, BSYM(1f)	
	mov	r8, r4
 ARM(	add	pc, r10, #PROCINFO_INITFUNC	)
 THUMB(	add	r12, r10, #PROCINFO_INITFUNC	)
 THUMB(	ret	r12				)
1:	b	__enable_mmu
ENDPROC(stext)

这里有个关键地方就是 ldr r13, =__mmap_switched 指令表示把__mmap_switched 地址取出来存到r13,这个下面有用,但是没有看到具体哪里跳转到c语言入口, 这里执行的最后一条指令是: b __enable_mmu .

__enable_mmu:
...
   b  __turn_mmu_on  @ 跳转到下一个函数
ENDPROC(__enable_mmu)

使能 mmu :

__turn_mmu_on

ENTRY(__turn_mmu_on)
	mov	r0, r0
	instr_sync
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	mov	r3, r13       @ 这里把r13赋值给r3
	ret	r3            @ 返回r3,相当于 b r3

__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)

最后执行的是ret r3,而r3是由r13赋值而来,而r13的值又是从stext 函数里面取的__mmap_switched的地址,所以这里就是跳到到 __mmap_switched 代码:

__mmap_switched宏的文件在目录: kernel-3.18/arch/arm/kernel/head_common.S

__mmap_switched:
	adr	r3, __mmap_switched_data

	ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer
	cmp	r7, #0
	strne	r0, [r7]			@ Save control register values
	b	start_kernel       @ 这个就是我们要找到内核c入口,
	                           @ 但是这个怎么和上面串联起来呢?
ENDPROC(__mmap_switched)

所以从上面看就很明显了,r13存了__mmap_switched的地址,然后又赋值给r3, 通过ret指令带回顺利跳转到__mmap_switched 代码中,最后跳转到start_kernel进入 内核c语言执行环境. 这里需要注意一点,在lk启动阶段因为是物理地址,所以需要关闭mmu,而进入内核镜像执行时需要使能mmu和D-cache,因为内核镜像中的地址都是虚拟地址,所以需要使能mmu实现地址转换.

head.S 小结:

1、允许kernel切换到EL2(hypervisor) (EL0/EL1必须实现, EL2/EL3可选实现);

2、关闭 fiq / irq,切换到svc(管理)模式;

3、获取cpu id,并做合法性检查;

4、计算物理地址偏移,创建初始内存页表(PGD,PUD,PMD,PTE),建立物理地址跟虚拟地址映射 PA = f(VA)

5、使能mmu,内核中都是虚拟内存地址,需要mmu提供translation table walk支持;

6、跳转到内核C代码入口 start_kernel,执行内核通用初始化。