以下内容源于朱有鹏嵌入式课程的学习,如有侵权,请告知删除。
一、内容总结
汇编阶段或者说内核引导阶段,主要是/arch/arm/kernel/head.S文件,主要完成以下内容:
(1)校验启动合法性(CPU ID、机器码,uboot给内核的传参格式等)。
(2)建立段式映射的页表,并开启MMU以方便使用内存。
(3)构建C运行环境,跳入C阶段。
整个汇编阶段的工作,都体现在下面这段代码中:
二、分析head.S文件
由内核的链接脚本可知,内核的入口地址在/arch/arm/kernel/head.S文件的ENTRY(stext)处。
所以我们开始分析 /arch/arm/kernel/head.S 文件。
2.1 内核的链接地址与物理地址
(1)KERNEL_RAM_VADDR,表示内核的链接地址,它是一个虚拟地址,值为 0xC0008000 。
(2)KERNEL_RAM_PADDR,表示内核的链接地址所对应的物理地址,其值为 0x30008000。
2.2 内核的入口地址
(1)__HEAD定义了一个段名为“.head.text ”的段。
// 在/include/linux/init.h文件中
/* For assembly routines */
#define __HEAD .section ".head.text","ax" //定义了段名为.head.text的段
#define __INIT .section ".init.text","ax"
#define __FINIT .previous
(2)ENTRY(stext),这表明内核的入口地址是标号stext。
在/include/linux/linkage.h文件中有如下代码:
//在/include/linux/linkage.h文件中
#ifndef ENTRY
#define ENTRY(name) \
.globl name; \
ALIGN; \
name:
#endif
则 ENTRY(stext) 就相当于下面的代码:
.globl stext
ALIGN
stext:
(3)uboot启动内核后,实际调用zImage前面的那段未经压缩的解压代码,解压代码运行时先将zImage后面的部分解压开,然后再到内核入口(即这里的stext)去启动内核。话说这过程具体是怎样的?
(4)内核启动需要一定先决条件,这个条件由启动内核的bootloader(比如uboot)来构建保证。
(5)ARM体系中,函数调用时实际是通过寄存器传参的。函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参。uboot中最后theKernel (0, machid, bd->bi_boot_params);执行内核时,实际把0放在r0中,machid放在r1中,bd->bi_boot_params放在r2中。ARM的这种处理技巧刚好满足了kernel启动的条件和要求。
(6)因为内核还没有启动起来,此时MMU是关闭的,因此硬件上需要的是物理地址。但是内核的链接地址是一个虚拟地址。因此head.S文件中尚未开启MMU之前的代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。
2.3 校验CPU_ID的合法性
通过__lookup_processor_type函数检验CPU_ID的合法性:
__lookup_processor_type函数位于/arch/arm/kernel/head-common.S文件中,内容如下:
首先从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用__lookup_processor_type函数来进行合法性检验(内核会维护一个本内核支持的CPU ID号码的数组,然后该函数把从硬件中读取到的CPU ID和数组中存储的各个ID号进行对比,如果没有一个相等则不合法,如果有一个相等的则合法),如果合法则继续启动,如果不合法则停止启动,转向__error_p启动失败。
2.4 校验机器码的合法性
通过__lookup_machine_type函数校验机器码的合法性:
__lookup_machine_type函数位于/arch/arm/kernel/head-common.S文件中,内容如下:
__lookup_machine_type函数的设计方法,与上面校验cpu id的函数一样的。
内核启动时设计上面的两个校验,也是为了内核启动的安全性着想。
2.5 校验uboot给内核传参的格式
利用__vet_atags函数,对uboot通过struct tag给内核传参的格式进行校验。
__vet_atags函数位于/arch/arm/kernel/head-common.S文件中,内容如下:
该函数的设计方法与上面__lookup_machine_type函数一样,用来对uboot通过struct tag给内核传参的格式进行校验。传递的参数包括:板子的内存分布、bootargs等内容。
如果uboot给内核传参的格式不对(比如uboot的bootargs设置不正确),内核将启动不起来。
2.6 建立段式页表
利用__create_page_tables函数建立段式页表。
__create_page_tables函数位于/arch/arm/kernel/head-common.S文件中,内容如下:
/*
* Setup the initial page tables. We only setup the barest
* amount which are required to get the kernel running, which
* generally means mapping in the kernel code.
*
* r8 = machinfo
* r9 = cpuid
* r10 = procinfo
*
* Returns:
* r0, r3, r6, r7 corrupted
* r4 = physical page table address
*/
__create_page_tables:
pgtbl r4 @ page table address
/*
* Clear the 16K level 1 swapper page table
*/
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping for first MB of kernel to
* cater for the MMU enable. This identity mapping
* will be removed by paging_init(). We use our current program
* counter to determine corresponding section base address.
*/
mov r6, pc
mov r6, r6, lsr #20 @ start of kernel section
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
#ifdef CONFIG_XIP_KERNEL
/*
* Map some ram to cover our .data and .bss areas.
*/
orr r3, r7, #(KERNEL_RAM_PADDR & 0xff000000)
.if (KERNEL_RAM_PADDR & 0x00f00000)
orr r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000)
.endif
add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> 18
str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]!
ldr r6, =(_end - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
#endif
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
#ifdef CONFIG_DEBUG_LL
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
/*
* Map in IO space for serial debugging.
* This allows debug messages to be output
* via a serial console before paging_init.
*/
ldr r3, [r8, #MACHINFO_PGOFFIO]
add r0, r4, r3
rsb r3, r3, #0x4000 @ PTRS_PER_PGD*sizeof(long)
cmp r3, #0x0800 @ limit to 512MB
movhi r3, #0x0800
add r6, r0, r3
ldr r3, [r8, #MACHINFO_PHYSIO]
orr r3, r3, r7
1: str r3, [r0], #4
add r3, r3, #1 << 20
teq r0, r6
bne 1b
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
/*
* If we're using the NetWinder or CATS, we also need to map
* in the 16550-type serial port for the debug messages
*/
add r0, r4, #0xff000000 >> 18
orr r3, r7, #0x7c000000
str r3, [r0]
#endif
#ifdef CONFIG_ARCH_RPC
/*
* Map in screen at 0x02000000 & SCREEN2_BASE
* Similar reasons here - for debug. This is
* only for Acorn RiscPC architectures.
*/
add r0, r4, #0x02000000 >> 18
orr r3, r7, #0x02000000
str r3, [r0]
add r0, r4, #0xd8000000 >> 18
str r3, [r0]
#endif
#endif
mov pc, lr
(1)Linux内核本身被链接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU。
(2)kernel建立页表分为以下两步:
第一步,先建立一个段式页表(1MB为单位的段页表)。段式页表建立过程简单(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),但不能精细管理内存。上面的函数就是用来建立段式页表的。
第二步,然后建立一个细页表(4kb为单位的细页表),然后启用新的细页表,并废除第一步建立的段式映射页表。
(3)内核启动的早期建立段式页表,并在内核启动早期使用;内核启动的后期再次建立细页表并启用。等内核工作起来后,就只有细页表了。
2.7 构建C语言运行环境
建立段式页表后进入__switch_data部分,它是一个函数指针数组,定义在/arch/arm/kernel/head-common.S文件中。
分析可知下一步要执行__mmap_switched函数。
该函数主要完成以下工作:
(1)复制数据段、清除bss段(目的是构建C语言运行环境)。
(2)将CPU ID号、机器码、TAG传参的首地址保存起来。
(3)通过“b start_kernel”跳转到C语言运行阶段。