《Linux内核深度解析》读书笔记。
1. 读取引导程序的地址
以U-Boot为例,启动代码路径为arch/arm/armv8/start.S
ARM64处理器到虚拟地址0取指令,刚上电时的MMU还未启用,也就是到物理地址0取指令。
2. 保存启动参数
U-boot启动从_start开始,直接跳转至标号reset处,之后做的第一件事就是保存启动时的参数。
reset:
/* Allow the board to save important registers */
b save_boot_params
...
WEAK(save_boot_params)
b save_boot_params_ret /* back to my caller */
ENDPROC(save_boot_params)
这里需要注意的:使用WEAK弱符号类型的函数修饰,可使用强符号以覆盖旧符号。另外,在初始状态下,为小端字节序,禁止MMU,禁止指令/数据cache的状态。
3. 根据处理器当前的异常级别设置寄存器,启用部分硬件功能
switch_el x1, 3f, 2f, 1f
3: set_vbar vbar_el3, x0
mrs x0, scr_el3
orr x0, x0, #0xf /* SCR_EL3.NS|IRQ|FIQ|EA */
msr scr_el3, x0
msr cptr_el3, xzr /* Enable FP/SIMD */
#ifdef COUNTER_FREQUENCY
ldr x0, =COUNTER_FREQUENCY
msr cntfrq_el0, x0 /* Initialize CNTFRQ */
#endif
b 0f
2: set_vbar vbar_el2, x0
mov x0, #0x33ff
msr cptr_el2, x0 /* Enable FP/SIMD */
b 0f
1: set_vbar vbar_el1, x0
mov x0, #3 << 20
msr cpacr_el1, x0 /* Enable FP/SIMD */
根据异常级别,设置异常向量的起始地址,通过CPTR寄存器启用FP/SIMD。在EL3级别时,设置SCR_EL3的NS,IRQ,FIQ和EA四个bit位,将中断,快速中断,同步外部终止和系统错误转发到异常级别3。
4. 执行特定的勘误表和硬件初试化
/* Apply ARM core specific erratas */
bl apply_core_errata
/* Processor specific initialization */
bl lowlevel_init
5.执行_main函数
路径arch/arm/lib/ctr0_64.S
U-Boot分为SPL和正常的U-Boot程序两个部分;SPL即第二程序加载器(可选),处理器内部集成的静态RAM比较小,无法装载一个完整的U-Boot镜像,此时需要SPL,主要负责初始化内存和存储设备驱动,然后把正常的U-Boot镜像从存储设备中读到内存中执行。
5.1 为调用board_init_f函数做环境准备
bic sp, x0, #0xf /* 16-byte alignment for ABI compliance */
mov x0, sp
bl board_init_f_alloc_reserve
mov sp, x0
/* set up gd here, outside any C code */
mov x18, x0
bl board_init_f_init_reserve
设置临时的栈空间,通过调用board_init_f_alloc_reserve在栈的顶部为结构体global_data分配空间,通过 board_init_f_init_reserve初始化global_data结构体。
5.2 调用board_init_f函数
mov x0, #0
bl board_init_f
调用board_init_f,执行前期初始化,包括讲U-Boot程序复制到内存中以及初始化硬件,依次执行common/board_f.c数组init_sequence_f中的每个函数。
void board_init_f(ulong boot_flags)
{
...
if (initcall_run_list(init_sequence_f))
hand();
...
}
5.3 重定位&调用c_runtime_cpu_setup
对复制到内存中的U-boot镜像重新定位,然后调用c_runtime_cpu_setup,设置最终的完整环境,设置异常向量表的起始地址。
#if defined(CONFIG_ARMV8_SPL_EXCEPTION_VECTORS) || !defined(CONFIG_SPL_BUILD)
/* Relocate vBAR */
adr x0, vectors
switch_el x1, 3f, 2f, 1f
3: msr vbar_el3, x0
b 0f
2: msr vbar_el2, x0
b 0f
1: msr vbar_el1, x0
0:
5.5 调用函数board_init_r
调用函数board_init_r执行后期初始化,依次执行init_sequence_r中的每个函数,最后执行run_main_loop。
void board_init_r(gd_t *new_gd, ulong dest_addr)
{
...
if (initcall_run_list(init_sequence_r))
hang();
...
}
5.6 run_main_loop函数
(1) 通过bootdelay_process读取环境变量:bootdelay和bootcmd,分别定义了延迟时间和要执行的命令。
(2) 调用autoboot_command。首先调用abortboot,等待用户按键;在等待时间内用户无按键,就调用函数run_command_list,自动执行bootcmd定义的命令。假如bootcmd定义的命令是"bootm",函数run_command_list查找命令表,发现命令"bootm"的处理函数是do_bootm。
(3) do_bootm先后初始化全局变量"bootm_header_t images",把内核镜像从存储设备读到内存,读取其他信息传递硬件信息的扁平设备树(FDT),将内核加载到正确的位置,之后根据操作系统类型在数组boot_os中查抄引导linux内核的引导函数do_bootm_linux(这是第一次执行do_bootm_linux,实际是为执行内核做准备,如拷贝FDT,根据bootargs设定多处理器启动方式等),第二次调用do_bootm_linux是实际执行内核,跳转到内核入口,第一个参数为FDT二进制文件起始地址(后面三个参数保留),同时根据配置宏CONFIG_GICV2或CONFIG_GICV3发送中断请求唤醒所有从处理器,禁止处理器缓存和内存管理单元,根据配置宏设定内核执行的异常等级。
6. 内核初试化
6.1 preserve_boot_args
把引导程序传递的4个参数保存在全局数组boot_args中。
6.2 el2_setup
设定内核运行的异常等级
(1)当进入内核时,异常等级为1,那么在异常级别1执行内核。
(2)当进入内核时,异常等级为2,支持VHE那么内核继续在2执行,如果不支持那么降到1执行内核。
在虚拟化中,运行虚拟机的操作系统称为host OS,在虚拟机里面的操作系统称为guest OS,guest OS的用户进程在异常0运行,内核在异常级别1运行;kvm的特点就是直接在处理器上执行guest OS。
以虚拟机KVM为例,普通的虚拟化异常等级切换,kvm模块需要穿越异常等级1和2;Arm64引入虚拟化宿主扩展后,在异常级别2执行host OS操作系统内核,kvm就不再需要从异常级别1切换到异常级别2。
6.3 __create_page_tables
(1)创建恒等映射,恒等映射的特点就是虚拟地址和物理地址相同,为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。由__enable_mmu负责开启内存管理单元,内核把函数__enable_mmu附近的代码放在恒等映射代码节(.idmap.text)中,恒等映射代码节的起始地址存放在全局变量__idmap_text_start中,结束地址存放在全局变量__idmap_text_end中。idmap_pg_dir为恒等映射的页全局目录的地址地址。
(2)在内核的页表中为内核镜像创建映射,内核镜像起始地址为_text,结束地址为_end。
6.4 __primary_switch
(1)调用函数__enable_mmu以开启内存管理单元。
(2)调用__primary_switched,设置不同异常级别的栈指针,VBAR_EL1设置为异常向量表的起始地址,计算内核镜像起始虚拟地址和物理地址的差值,保存到全局变量kimage_voffset中,调用start_kernel。
6.5 start_kernel
首先初始化基础设施,即初始化内核的各个子系统,然后调用rest_init创建init和kthreadd线程。
之后,init进程继续执行初始化,主要为smp作初始化和准备,启动所有从处理器,执行脚本0~7的初始化,挂载根文件系统,释放初始化代码和数据占用的内存,最后从文件系统中装载init程序,并转换成用户空间的init进程。