1.3.1 汇编语言部分
ARM64架构的内核的入口是标号_head,直接跳转到标号stext。
arch/arm64/kernel/head.S 1 _head: 2 #ifdef CONFIG_EFI 3 add x13, x18, #0x16 4 b stext 5 #else 6 b stext // 跳转到内核起始位置 7 .long0 // 保留 8 #endif
配置宏CONFIG_EFI表示提供UEFI运行时支持,UEFI(Unified Extensible Firmware Interface)是统一的可扩展固件接口,用于取代BIOS。
标号stext开始的代码如下:
arch/arm64/kernel/head.S 1 ENTRY(stext) 2 bl preserve_boot_args 3 bl el2_setup // 降级到异常级别1, 寄存器w0存放cpu_boot_mode 4 adrp x23, __PHYS_OFFSET 5 and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR偏移,默认值是0 6 bl set_cpu_boot_mode_flag 7 bl __create_page_tables 8 /* 9 * 下面调用设置处理器的代码,请看文件“arch/arm64/mm/proc.S” 10 * 了解细节。 11 * 返回的时候,处理器已经为开启内存管理单元做好准备, 12 * 转换控制寄存器已经设置好。 13 */ 14 bl __cpu_setup // 初始化处理器 15 b __primary_switch 16 ENDPROC(stext)
第2行代码,调用函数preserve_boot_args,把引导程序传递的4个参数保存在全局数组boot_args中。
第3行代码,调用函数el2_setup:如果处理器当前的异常级别是2,判断是否需要降级到异常级别1。
第6行代码,调用函数set_cpu_boot_mode_flag,根据处理器进入内核时的异常级别设置数组__boot_cpu_mode[2]。__boot_cpu_mode[0]的初始值是BOOT_CPU_MODE_EL2,__boot_cpu_mode[1]的初始值是BOOT_CPU_MODE_EL1。如果异常级别是1,那么把__boot_cpu_mode[0]设置为BOOT_CPU_MODE_EL1;如果异常级别是2,那么把__boot_cpu_mode[1]设置为BOOT_CPU_MODE_EL2。
第7行代码,调用函数__create_page_tables,创建页表映射。
第14行代码,调用函数__cpu_setup,为开启处理器的内存管理单元做准备,初始化处理器。
第15行代码,调用函数__primary_switch,为主处理器开启内存管理单元,搭建C语言执行环境,进入C语言部分的入口函数start_kernel。
1.函数el2_setup
进入内核的时候,ARM64处理器的异常级别可能是1或者2,函数el2_setup的主要工作如下。
(1)如果异常级别是1,那么在异常级别1执行内核。
(2)如果异常级别是2,那么根据处理器是否支持虚拟化宿主扩展(Virtualization Host Extensions, VHE),决定是否需要降级到异常级别1。
1)如果处理器支持虚拟化宿主扩展,那么在异常级别2执行内核。
2)如果处理器不支持虚拟化宿主扩展,那么降级到异常级别1,在异常级别1执行内核。
下面介绍ARM64处理器的异常级别和虚拟化宿主扩展。
如图1.4所示,通常ARM64处理器在异常级别0执行进程,在异常级别1执行内核。
图1.4 普通的异常级别切换
虚拟机是现在流行的虚拟化技术,在计算机上创建一个虚拟机,在虚拟机里面运行一个操作系统,运行虚拟机的操作系统称为宿主操作系统(host OS),虚拟机里面的操作系统称为客户操作系统(guest OS)。
现在常用的虚拟机是基于内核的虚拟机(Kernel-based Virtual Machine, KVM), KVM的主要特点是直接在处理器上执行客户操作系统,因此虚拟机的执行速度很快。KVM是内核的一个模块,把内核变成虚拟机监控程序。如图1.5所示,宿主操作系统中的进程在异常级别0运行,内核在异常级别1运行,KVM模块可以穿越异常级别1和2;客户操作系统中的进程在异常级别0运行,内核在异常级别1运行。
图1.5 支持虚拟化的异常级别切换
常用的开源虚拟机管理软件是QEMU, QEMU支持KVM虚拟机。使用QEMU创建一个KVM虚拟机,和KVM的交互过程如下。
(1)打开KVM字符设备文件。
fd = open("/dev/kvm", O_RDWR);
(2)创建一个虚拟机,QEMU进程得到一个关联到虚拟机的文件描述符。
vmfd = ioctl(fd, KVM_CREATE_VM, 0);
(3)QEMU为虚拟机模拟多个处理器,每个虚拟处理器就是一个线程,调用KVM提供的命令KVM_CREATE_VCPU, KVM为每个虚拟处理器创建一个kvm_vcpu结构体,QEMU进程得到一个关联到虚拟处理器的文件描述符。
vcpu_fd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
从QEMU切换到客户操作系统的过程如下。
(1)QEMU进程调用“ioctl(vcpu_fd, KVM_RUN, 0)”,陷入到内核。
(2)KVM执行命令KVM_RUN,从异常级别1切换到异常级别2。
(3)KVM首先把调用进程的所有寄存器保存在kvm_vcpu结构体中,然后把所有寄存器设置为客户操作系统的寄存器值,最后从异常级别2返回到异常级别1,执行客户操作系统。
如图1.6所示,为了提高切换速度,ARM64架构引入了虚拟化宿主扩展,在异常级别2执行宿主操作系统的内核,从QEMU切换到客户操作系统的时候,KVM不再需要先从异常级别1切换到异常级别2。
图1.6 支持虚拟化宿主扩展的异常级别切换
2.函数__create_page_tables
函数__create_page_tables的主要工作如下。
(1)创建恒等映射(identity mapping)。
(2)为内核镜像创建映射。
恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。函数__enable_mmu负责开启内存管理单元,内核把函数__enable_mmu附近的代码放在恒等映射代码节(.idmap.text)里面,恒等映射代码节的起始地址存放在全局变量__idmap_text_start中,结束地址存放在全局变量__idmap_text_end中。
恒等映射是为恒等映射代码节创建的映射,idmap_pg_dir是恒等映射的页全局目录(即第一级页表)的起始地址。
在内核的页表中为内核镜像创建映射,内核镜像的起始地址是_text,结束地址是_end, swapper_pg_dir是内核的页全局目录的起始地址。
3.函数__primary_switch
函数__primary_switch的主要执行流程如下。
(1)调用函数__enable_mmu以开启内存管理单元。
(2)调用函数__primary_switched。
函数__enable_mmu的主要执行流程如下。
(1)把转换表基准寄存器0(TTBR0_EL1)设置为恒等映射的页全局目录的起始物理地址。
(2)把转换表基准寄存器1(TTBR1_EL1)设置为内核的页全局目录的起始物理地址。
(3)设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。
函数__primary_switched的执行流程如下。
(1)把当前异常级别的栈指针寄存器设置为0号线程内核栈的顶部(init_thread_union +THREAD_SIZE)。
(2)把异常级别0的栈指针寄存器(SP_EL0)设置为0号线程的结构体thread_info的地址(init_task.thread_info)。
(3)把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址(vectors)。(4)计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量kimage_voffset中。
(5)用0初始化内核的未初始化数据段。
(6)调用C语言函数start_kernel。