据BootLoader传入的参数,对一些参数的改写
#ifdef CONFIG_RANDOMIZE_BASE
tst x23, ~(MIN_KIMG_ALIGN - 1) // already running randomized?
b.ne 0f
bl kaslr_early_init // parse FDT for KASLR options
cbz x0, 0f // KASLR disabled? just proceed
orr x23, x23, x0 // record KASLR offset
ldp x29, x30, [sp], #16 // we must enable KASLR, return
ret // to __primary_switch()
0:
#endif
bl switch_to_vhe // Prefer VHE if possible ---(8)
ldp x29, x30, [sp], #16 // ---(9)
bl start_kernel // ---(10)
ASM_BUG()
SYM_FUNC_END(__primary_switched)
1. 为init进程(swapper进程)设置好堆栈地址和大小,保存当前进程描述符地址到sp_el0
??在task_pt_regs(current)->stackframe创建一个最终帧记录,这样unwinder就可以根据任务堆栈中的位置来识别任何任务的最终帧记录。保留整个pt_regs空间使用户任务和kthread保持一致性。
/*
* Initialize CPU registers with task-specific and cpu-specific context.
*
* Create a final frame record at task_pt_regs(current)->stackframe, so
* that the unwinder can identify the final frame record of any task by
* its location in the task stack. We reserve the entire pt_regs space
* for consistency with user tasks and kthreads.
*/
.macro init_cpu_task tsk, tmp1, tmp2
msr sp_el0, \tsk ///init_task指针保存在sp_el0,因为当前运行在EL1异常等级,使用sp_el1来保存栈的指针,sp_el0闲置,内核空间中会使用sp_el0来作为current task struct的结构体
ldr \tmp1, [\tsk, #TSK_STACK] // 获取init_task的栈地址,offsetof(struct task_struct, stack)
add sp, \tmp1, #THREAD_SIZE // 栈是由高地址向下生长的,所以SP_ELx要加上THREAD_SIZE
sub sp, sp, #PT_REGS_SIZE // 为struct pt_regs留出空间
stp xzr, xzr, [sp, #S_STACKFRAME] // 将struct pt_regs的u64 stackframe[2]清零
add x29, sp, #S_STACKFRAME // x29(FP)指向栈中pt_regs的stackframe
scs_load \tsk // 用于Clang Shadow Call Stack,此处为空操作
adr_l \tmp1, __per_cpu_offset // 读取__per_cpu_offset[NR_CPUS]数组基地址
ldr w\tmp2, [\tsk, #TSK_CPU] // offsetof(struct task_struct, cpu)
ldr \tmp1, [\tmp1, \tmp2, lsl #3] // tmp1 = __per_cpu_offset[init_task.cpu << 3],通常来说,bootcpu为0
set_this_cpu_offset \tmp1 // 将当前cpu的per_cpu变量的offset值写入TPIDR_ELx
.endm
几个寄存器的最终结果:
SP_EL0 = &init_task
SP_ELx = init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)
x29(FP) = SP_ELx + S_STACKFRAME
2. 设置异常向量表基址寄存器
中断向量表的起始虚拟地址写入到VBAR_EL1。
3. 备份寄存器
此时sp的值为init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。主要工作如下:
- 将x29(FP)和x30(LR)分别保存到sp-16和sp-8的地址上,然后sp -= 16。
- 将sp的值写入到x29(FP)
这是实现了ARM64函数调用标准规定的栈布局,为后续函数调用的入栈出栈做好了准备。
4. 保存设备树物理地址到__fdt_pointer
x21寄存器的值是在preserve_boot_args接口中保存的FDT的地址。
5. 计算kimage_voffset
??kimage_voffset记录了内核镜像映射后的虚拟地址与内核镜像在内存中的物理地址之间的差值。kimage_vaddr记录了_text的链接地址,也就是最终_text的虚拟地址,x0作为传入参数记录了_text的物理地址,相减即可得kimage_voffset。
6. 清空BSS段
7. 先初始化fixmap,然后通过fixmap为fdt建立页表(early_fdt_map)
??early_fdt_map主要为KASLR服务,可能会失败,如果失败,会在setup_arch重新映射。
8. switch_to_vhe
??在回一下异常等级初始化流程,在该流程中会通过hcr_el2.e2h判断是否会进入vhe模式,而这个标志是通过HCR_HOST_NVHE_FLAGS初始化的。因此若该标志未设置e2h位,则即使系统支持vhe也不会实际进入该模式。因为vhe模式的优势,故内核在这里会再给一次进入该模式的机会
9. 恢复x29(FP)和x30(LR)
??从栈中恢复x29(FP)和x30(LR),sp重新指向init_task.stack + THREAD_SIZE - sizeof(struct pt_regs)。
10. 跳转start_kernel
离开头疼的汇编,看到我们熟悉的start_kernel。
??
一旦设定完了页表,那么打开MMU之后,kernel正式就会进入虚拟地址空间的世界,美中不足的是内核的虚拟世界没有那么大。原来拥有的整个物理地址空间都消失了,能看到的仅仅剩下kernel image mapping和identity mapping这两段地址空间是可见的。不过没有关系,这只是刚开始,内存初始化之路还很长。