内核的实际起始函数为 start_kernel() 函数,然后再调用其他函数来执行启动。再调用此函数之前,需要先将通过编译内核获得的 zImage 进行解压,请按成页目录构建等基本任务。
调用 start_kernel 的过程分为以下三个阶段:
- 解压内核映像 zImage 前的准备阶段,通过与处理器版本相符的处理器类型列表,执行打开/关闭/清除缓存等任务,为MMU构建16KB的页目录;
- 对 zImage 执行解压缩
- 检查处理器及机器信息、通过启动加载项获得 atag 信息的有效性,然后激活 MMU 调用内核的起始函数 start_kernel()。
3.1 内核解压
3.1.1 准备阶段
解压缩准备阶段将执行中断禁用、分配动态内存、初始化BBS区域、初始化页目录、打开缓存等任务。
在该阶段,zImage 解压位置的下级 16KB 构建用于保存页目录的空间,在CP15的c2寄存器中保存页目录的位置。
ARM中,页目录将 4GB 的内存以 1MB 节区为单位进行管理。因此,为了管理 4GB 的内存,需要有 4096 个以 1MB为单位的项。由于以32位的字符为单位管理各项,所以共需要 16KB (4字节 X 4096各项 = 16KB)。之后,向相当于页目录位置的项设置 cacheable 和 bufferable,使页目录得到缓冲并能快速访问。
从start 标签到解压缩准备阶段的流程图
- 启动加载项必须提供5种功能
- RAM初始化
- 串行端口初始化
- 查找机器类别
- 构建 tagged list 内核
- 将控制移交到内核镜像
3.1.1.1 进入启动加载后结束首个启动--start 标签
通过加载项完成对软硬件的默认初始化后,最先执行的是 head.S (arch\arm\boot\compressed) 下的 start 标签中的代码。 完成的主要功能如下:
- 从启动加载项接收结构ID和atags信息
- 禁用中断
- 初始化寄存器,跳转到 not_relocated 标签
- 从 start 标签开始执行,共执行了 8 (rept 7 + 1) 次 "mov r0, r0" 指令(等同于 nop 指令),空出了 32 字节的用来存放 ARM 的中断向量表的位置,然后跳转到 "1" 标签处。
1 start: 2 .type start,#function 3 .rept 7 4 __nop 5 .endr 6 7 mov r0, r0 8 W(b) 1f
使用.type标号来指明start的符号类型是函数类型,然后重复执行.rept到.endr之间的指令7次,这里一共执行了7次mov r0, r0指令,共占用了4*7 = 28个字节,这是用来存放ARM的异常向量表的。向前跳转到标号为1处执行
2. 保存 cpsr 的值到 r9 中,保存架构 ID 和 atags 指针分别到 r7 和 r8 中。
1 1: 2 ARM_BE8( setend be ) @ go BE8 if compiled for BE8 3 AR_CLASS( mrs r9, cpsr ) 4 /* 将启动加载项传递的结构ID和 atags 信息分别保存到寄存器 r7 r8 中 */ 5 mov r7, r1 @ 保存结构ID 6 mov r8, r2 @ 保存 atags 指针
当中还有未贴出来的代码,不相关的
这里将CPU的工作模式保存到r9寄存器中,将uboot通过r1传入的机器码保存到r7寄存器中,将启动参数tags的地址保存到r8寄存器中。
- CONFIG_ARM_VIRT_EXT 表明启用了 ARM 虚拟化扩展。
- 从 bootloader 中接收了 3 个参数,分别为
- R0 = 0
- R1 = 架构 ID
- R2 = atags 指针
3.继续在标签“1”中运行,判断当前 CPU 的工作模式,若不是在用户模式下,则跳转到 "not_angel" 标签处,否则通过 swi 指令产生软中断异常的方式来进入 SVC 模式。
1 2 /* 3 * Booting from Angel - need to enter SVC mode and disable 4 * FIQs/IRQs (numeric definitions from angel arm.h source). 5 * We only do this if we were in user mode on entry. 6 */ 7 mrs r2, cpsr @ 将CPSR状态寄存器读取,保存到R1中,即获取当前CPU模式 8 tst r2, #3 @ 判断CPU是否为用户模式 9 bne not_angel 10 mov r0, #0x17 @ angel_SWIreason_EnterSVC 11 ARM( swi 0x123456 ) @ angel_SWI_ARM 12 THUMB( svc 0xab ) @ angel_SWI_THUMB
这里将CPU的工作模式保存到r2寄存器中,然后判断是否是SVC模式,如果是USER模式就会通过swi指令产生软中断异常的方式来自动进入SVC模式。由于我这里在uboot中已经将CPU的模式设置为SVC模式了,所以就直接跳到not_angel符号处执行。
4.借助 safe_svcmode_maskall 宏, 屏蔽 IRQ、FIQ中断,切换到 SVC 模式;将 r9 中保存的原来的 CPSR 的值保存到 SPSR 中。
1 /* 设置CPU为SVC模式的具体操作 */ 2 not_angel: 3 /* .macro safe_svcmode_maskall reg:req 在 Assembler.h (arch\arm\include\asm)中定义*/ 4 safe_svcmode_maskall r0 5 msr spsr_cxsf, r9 @ Save the CPU boot mode in 6 @ SPSR
(1)借助 safe_svcmode_maskall 宏, 屏蔽 IRQ、FIQ中断,切换到 SVC 模式;将 r9 中保存的原来的 CPSR 的值保存到 SPSR 中。
arch/arm/include/asm/assembler.h
这里的注释已经说明了,这里是强制将CPU的工作模式切换到SVC模式,并且关闭IRQ和FIQ中断。然后将r9中保存的原始CPU配置保存到SPSR中。
1 /* 此处出现的 MODE_MASK、PSR_I_BIT 等常量被宏定义在 arch/arm/include/uapi/asm/ptrace.h */ 2 .macro safe_svcmode_maskall reg:req 3 #if __LINUX_ARM_ARCH__ >= 6 && !defined(CONFIG_CPU_V7M) 4 mrs \reg , cpsr 5 eor \reg, \reg, #HYP_MODE 6 tst \reg, #MODE_MASK 7 bic \reg , \reg , #MODE_MASK @ 将模式位M[4:0]清0 8 /* 通过设置低 8 位为 110 10011,达到了关闭 IRQ、FIQ、设置 CPU 工作模式为 SVC 模式的目标 */ 9 orr \reg , \reg , #PSR_I_BIT | PSR_F_BIT | SVC_MODE 10 THUMB( orr \reg , \reg , #PSR_T_BIT ) 11 bne 1f 12 orr \reg, \reg, #PSR_A_BIT 13 badr lr, 2f 14 msr spsr_cxsf, \reg 15 __MSR_ELR_HYP(14) 16 __ERET 17 1: msr cpsr_c, \reg 18 2: 19 #else 20 /* 21 * workaround for possibly broken pre-v6 hardware 22 * (akita, Sharp Zaurus C-1000, PXA270-based) 23 */ 24 setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, \reg 25 #endif 26 .endm
5.将内核解压地址(ZRELADDR)保存到 R4 中,依然在 "not_angel" 标签中运行
1 /* 字符段开始区域 */ 2 .text 3 4 #ifdef CONFIG_AUTO_ZRELADDR 5 mov r4, pc 6 and r4, r4, #0xf8000000 7 /* Determine final kernel image address. */ 8 add r4, r4, #TEXT_OFFSET 9 #else 10 ldr r4, =zreladdr 11 #endif
内核配置项AUTO_ZRELDDR表示自动计算内核解压地址(Auto calculation of the decompressed kernelimage address),这里没有选择这个配置项,所以保存到r4中的内核解压地址就是zreladdr
(1)定义了 CONFIG_AUTO_ZRELADDR, 将在运行时计算确定 ZRELADDR
ZRELADDR 的值为:
- 先是 pc 值和 0xf8000000 做与操作;
注:此处与 0xf8000000 做 and 操作的原因样是我们默认 zImage 被放置的位置一定在距离 PHYS_OFFSET 的 128MB 之内。
- 再加上 TEXT_OFFSET(内核最终存放的物理地址与内存起始处之间的偏移)
TEXT_OFFSET 定义如下所示:
File: /arch/arm/Makefile
此处的 textofs-y 定义如下所示:
即 TEXT_OFFSET 的值为 0x00008000 = 32KB
此处之所以加上 TEXT_OFFSET 这个 32KB 的值的原因如下图所示:
PHY_OFFSET的值不一定为 0x60000000,根据硬件来确定。
(2)未定义 CONFIG_AUTO_ZRELADDR 时,直接加载 zreladdr 到 R4 中
zreladdr 的定义如下所示:
File: /arch/arm/boot/compressed/Makefile
ZERLADDR定义如下:
File: /arch/arm/boot/Makefile
看一下params_phys和initrd_phys的值,他们最终由arch/arm/mach-$(SOC)/Makefile.boot决定,我这里使用的soc是bcm2807(bcm2835),他的Makefile.boot内容如下:
zreladdr-y := 0x00008000
params_phys-y := 0x00000100
initrd_phys-y :=0x00800000
params_phys-y和initrd_phys-y是内核参数的物理地址和initrd文件系统的物理地址。其实除了zreladdr外这些地址uboot都会传入的。
这里的 zreladdr-y 定义在 /arch/arm/mach-xxx/Makefile.boot 中。
比如所用的 2440
这些地址都是通过uboot 传入进来的
6.缓存和MMU初始化cache_on的执行流程
这里将比较当前PC地址和内核解压地址,只有在不会自覆盖的情况下才会创建一个页表,如果当前运行地址PC < 解压地址 r4,则读取 LC0+32 地址处的内容加载到 r0 中,否则跳转到 cache_on 处执行缓存初始化和MMU初始化。
代码如下,此处代码依然在 "not_angel" 标签中运行
1 mov r0, pc 2 cmp r0, r4 3 ldrcc r0, LC0+32 4 addcc r0, r0, pc 5 cmpcc r4, r0 6 orrcc r4, r4, #1 @ remember we skipped cache_on 7 blcs cache_on
LC0的定义如下:
LC0+32地址处的内容为:_end -restart + 16384 + 1024*1024,所指的就是程序长度+16k的页表长+1M的DTB空间。
继续比较解压地址r4(0x00008000)和当前运行程序的(结束地址+16384 + 1024*1024),如果小于则不进行缓存初始化并置位r4最低位进行标识。
分情况总结一下:
(1) PC >= r4:直接进行缓存初始化
(2) PC < r4 && _end + 16384+ 1024*1024 > r4:不进行缓存初始化
(3) PC < r4 && _end + 16384+ 1024*1024 <= r4:执行缓存初始化
cache on 开始执行:
1 /* 2 * Turn on the cache. We need to setup some page tables so that we 3 * can have both the I and D caches on. 4 * 5 * We place the page tables 16k down from the kernel execution address, 6 * and we hope that nothing else is using it. If we‘re using it, we 7 * will go pop! 8 * 9 * On entry, 10 * r4 = kernel execution address 11 * r7 = architecture number 12 * r8 = atags pointer 13 * On exit, 14 * r0, r1, r2, r3, r9, r10, r12 corrupted 15 * This routine must preserve: 16 * r4, r7, r8 17 */ 18 .align 5 19 cache_on: mov r3, #8 @ cache_on function 20 b call_cache_fn
注释中说明了,为了开启I Cache和D Cache,需要建立页表(开启MMU),而页表使用的就是内核运行地址以下的16KB空间(对于我的环境来说地址就等于0x00004000~0x00008000)。同时在运行的过程中r0~r3以及r9、r10和r12寄存器会被使用。
这里首先在r3中保存打开缓存函数表项在cache操作表中的地址偏移(这里为8,cache操作表见后文),然后跳转到call_cache_fn中。
1 /* 2 * Here follow the relocatable cache support functions for the 3 * various processors. This is a generic hook for locating an 4 * entry and jumping to an instruction at the specified offset 5 * from the start of the block. Please note this is all position 6 * independent code. 7 * 8 * r1 = corrupted 9 * r2 = corrupted 10 * r3 = block offset 11 * r9 = corrupted 12 * r12 = corrupted 13 */ 14 15 call_cache_fn: adr r12, proc_types 16 #ifdef CONFIG_CPU_CP15 17 mrc p15, 0, r9, c0, c0 @ get processor ID 18 #elif defined(CONFIG_CPU_V7M) 19 /* 20 * On v7-M the processor id is located in the V7M_SCB_CPUID 21 * register, but as cache handling is IMPLEMENTATION DEFINED on 22 * v7-M (if existant at all) we just return early here. 23 * If V7M_SCB_CPUID were used the cpu ID functions (i.e. 24 * __armv7_mmu_cache_{on,off,flush}) would be selected which 25 * use cp15 registers that are not implemented on v7-M. 26 */ 27 bx lr 28 #else 29 ldr r9, =CONFIG_PROCESSOR_ID 30 #endif 31 1: ldr r1, [r12, #0] @ get value 32 ldr r2, [r12, #4] @ get mask 33 eor r1, r1, r9 @ (real ^ match) 34 tst r1, r2 @ & mask 35 ARM( addeq pc, r12, r3 ) @ call cache function 36 THUMB( addeq r12, r3 ) 37 THUMB( moveq pc, r12 ) @ call cache function 38 add r12, r12, #PROC_ENTRY_SIZE 39 b 1b
首先保存cache操作表的运行地址到r12寄存器中,proc_types定义在head.s中:
1 /* 2 * Table for cache operations. This is basically: 3 * - CPU ID match 4 * - CPU ID mask 5 * - ‘cache on‘ method instruction 6 * - ‘cache off‘ method instruction 7 * - ‘cache flush‘ method instruction 8 * 9 * We match an entry using: ((real_id ^ match) & mask) == 0 10 * 11 * Writethrough caches generally only need ‘on‘ and ‘off‘ 12 * methods. Writeback caches _must_ have the flush method 13 * defined. 14 */ 15 .align 2 16 .type proc_types,#object
表中的每一类处理器都包含以下5项(如果不存在缓存操作函数则使用“mov pc, lr”占位):
(1) CPU ID
(2) CPU ID 位掩码(用于匹配CPU类型用)
(3) 打开缓存“cache on”函数入口
(4) 关闭缓存“cache off”函数入口
(5) 刷新缓存“cache flush”函数入口
我所用的CPU为ARM920T的 S3C2440,为ARMV4T架构,一般架构如下图:
对应的代码为:
若配置了CPU_CP15条件编译项,所以这里将从CP15中获取CPU型号而不是从内核配置项中获取。
然后逐条对cache操作表中的CPU类型进行匹配,如果匹配上了就跳转到相应的函数入口执行。
遍历 proc_types 列表,查找想对应的处理器类型,找到之后 pc = r12 + r3,r3 中存储的是常数 8,即 pc 指向了相对应的 cache on 子例程。执行如下
1 call_cache_fn: adr r12, proc_types 2 #ifdef CONFIG_CPU_CP15 3 mrc p15, 0, r9, c0, c0 @ get processor ID 4 #elif defined(CONFIG_CPU_V7M) 5 /* 6 * On v7-M the processor id is located in the V7M_SCB_CPUID 7 * register, but as cache handling is IMPLEMENTATION DEFINED on 8 * v7-M (if existant at all) we just return early here. 9 * If V7M_SCB_CPUID were used the cpu ID functions (i.e. 10 * __armv7_mmu_cache_{on,off,flush}) would be selected which 11 * use cp15 registers that are not implemented on v7-M. 12 */ 13 bx lr 14 #else 15 ldr r9, =CONFIG_PROCESSOR_ID 16 #endif 17 1: ldr r1, [r12, #0] @ get value 18 ldr r2, [r12, #4] @ get mask 19 eor r1, r1, r9 @ (real ^ match) 20 tst r1, r2 @ & mask 21 ARM( addeq pc, r12, r3 ) @ call cache function 22 THUMB( addeq r12, r3 ) 23 THUMB( moveq pc, r12 ) @ call cache function 24 add r12, r12, #PROC_ENTRY_SIZE 25 b 1b
代码注释已经很清楚,之后调用 cache 函数,对应 2440 则调用函数:__armv4_mmu_cache_on