内核启动

树莓派启动过程

在树莓派 3B+ 真机上,通过 SD 卡启动时,上电后会运行 ROM 中的特定固件,接着加载并运行 SD 卡上的 bootcode.binstart.elf,后者进而根据 config.txt 中的配置,加载指定的 kernel 映像文件(纯 binary 格式,通常名为 kernel8.img)到内存的 0x80000 位置并跳转到该地址开始执行。

而在 QEMU 模拟的 raspi3b(旧版 QEMU 为 raspi3)机器上,则可以通过 -kernel 参数直接指定 ELF 格式的 kernel 映像文件,进而直接启动到 ELF 头部中指定的入口地址,即 _start 函数(实际上也在 0x80000,因为 ChCore 通过 linker script 强制指定了该函数在 ELF 中的位置,如有兴趣请参考附录)。

启动 CPU 0 号核

_start 函数(位于 kernel/arch/aarch64/boot/raspi3/init/start.S)是 ChCore 内核启动时执行的第一块代码。由于 QEMU 在模拟机器启动时会同时开启 4 个 CPU 核心,于是 4 个核会同时开始执行 _start 函数。而在内核的初始化过程中,我们通常需要首先让其中一个核进入初始化流程,待进行了一些基本的初始化后,再让其他核继续执行。

思考题 1

阅读 _start 函数的开头,尝试说明 ChCore 是如何让其中一个核首先进入初始化流程,并让其他核暂停执行的。

hint

可以在 Arm Architecture Reference Manual 找到 mpidr_el1 等系统寄存器的详细信息。

切换异常级别

AArch64 架构中,特权级被称为异常级别(Exception Level,EL),四个异常级别分别为 EL0、EL1、EL2、EL3,其中 EL3 为最高异常级别,常用于安全监控器(Secure Monitor),EL2 其次,常用于虚拟机监控器(Hypervisor),EL1 是内核常用的异常级别,也就是通常所说的内核态,EL0 是最低异常级别,也就是通常所说的用户态。

QEMU raspi3b 机器启动时,CPU 异常级别为 EL3,我们需要在启动代码中将异常级别降为 EL1,也就是进入内核态。具体地,这件事是在 arm64_elX_to_el1 函数(位于 kernel/arch/aarch64/boot/raspi3/init/tools.S)中完成的。

为了使 arm64_elX_to_el1 函数具有通用性,我们没有直接写死从 EL3 降至 EL1 的逻辑,而是首先判断当前所在的异常级别,并根据当前异常级别的不同,跳转到相应的代码执行。

BEGIN_FUNC(arm64_elX_to_el1)
	/* LAB 1 TODO 1 BEGIN */
	/* BLANK BEGIN */
	/* BLANK END */
	/* LAB 1 TODO 1 END */

	// Check the current exception level.
	cmp x9, CURRENTEL_EL1
	beq .Ltarget
	cmp x9, CURRENTEL_EL2
	beq .Lin_el2
	// Otherwise, we are in EL3.

	// Set EL2 to 64bit and enable the HVC instruction.
	mrs x9, scr_el3
	mov x10, SCR_EL3_NS | SCR_EL3_HCE | SCR_EL3_RW
	orr x9, x9, x10
	msr scr_el3, x9

	// Set the return address and exception level.
	/* LAB 1 TODO 2 BEGIN */
	/* BLANK BEGIN */
	/* BLANK END */
	/* LAB 1 TODO 2 END */

.Lin_el2:
	// Disable EL1 timer traps and the timer offset.
	mrs x9, cnthctl_el2
	orr x9, x9, CNTHCTL_EL2_EL1PCEN | CNTHCTL_EL2_EL1PCTEN
	msr cnthctl_el2, x9
	msr cntvoff_el2, xzr

	// Disable stage 2 translations.
	msr vttbr_el2, xzr

	// Disable EL2 coprocessor traps.
	mov x9, CPTR_EL2_RES1
	msr cptr_el2, x9

	// Disable EL1 FPU traps.
	mov x9, CPACR_EL1_FPEN
	msr cpacr_el1, x9

	// Check whether the GIC system registers are supported.
	mrs x9, id_aa64pfr0_el1
	and x9, x9, ID_AA64PFR0_EL1_GIC
	cbz x9, .Lno_gic_sr

	// Enable the GIC system registers in EL2, and allow their use in EL1.
	mrs x9, ICC_SRE_EL2
	mov x10, ICC_SRE_EL2_ENABLE | ICC_SRE_EL2_SRE
	orr x9, x9, x10
	msr ICC_SRE_EL2, x9

	// Disable the GIC virtual CPU interface.
	msr ICH_HCR_EL2, xzr

.Lno_gic_sr:
	// Set EL1 to 64bit.
	mov x9, HCR_EL2_RW
	msr hcr_el2, x9

	// Set the return address and exception level.
	adr x9, .Ltarget
	msr elr_el2, x9
	mov x9, SPSR_ELX_DAIF | SPSR_ELX_EL1H
	msr spsr_el2, x9

	isb
	eret

.Ltarget:
	ret
END_FUNC(arm64_elX_to_el1)

练习题 2

arm64_elX_to_el1 函数的 LAB 1 TODO 1 处填写一行汇编代码,获取 CPU 当前异常级别。

hint

通过 CurrentEL 系统寄存器可获得当前异常级别。通过 GDB 在指令级别单步调试可验证实现是否正确。注意参考文档理解 CurrentEL 各个 bits 的意义

eret指令可用于从高异常级别跳到更低的异常级别,在执行它之前我们需要设置 设置 elr_elx(异常链接寄存器)和 spsr_elx(保存的程序状态寄存器),分别控制eret执行后的指令地址(PC)和程序状态(包括异常返回后的异常级别)。

练习题 3

arm64_elX_to_el1 函数的 LAB 1 TODO 2 处填写大约 4 行汇编代码,设置从 EL3 跳转到 EL1 所需的 elr_el3spsr_el3 寄存器值。

hint

elr_el3 的正确设置应使得控制流在 eret 后从 arm64_elX_to_el1 返回到 _start 继续执行初始化。 spsr_el3 的正确设置应正确屏蔽 DAIF 四类中断,并且将 SP 正确设置为 EL1h. 在设置好这两个系统寄存器后,不需要立即 eret.

练习完成后,可使用 GDB 跟踪内核代码的执行过程,由于此时不会有任何输出,可通过是否正确从 arm64_elX_to_el1 函数返回到 _start 来判断代码的正确性。

跳转到第一行 C 代码

降低异常级别到 EL1 后,我们准备从汇编跳转到 C 代码,在此之前我们先设置栈(SP)。因此,_start 函数在执行 arm64_elX_to_el1 后,即设置内核启动阶段的栈,并跳转到第一个 C 函数 init_c

BEGIN_FUNC(_start)
	mrs	x8, mpidr_el1
	and	x8, x8,	#0xFF
	cbz	x8, primary

	/* Wait for bss clear */
wait_for_bss_clear:
	adr	x0, clear_bss_flag
	ldr	x1, [x0]
	cmp     x1, #0
	bne	wait_for_bss_clear

	/* Set cntkctl_el1 to enable cntvct_el0.
         * Enable it when you need to get current tick
         * at EL0, e.g. Running aarch64 ROS2 demos
	mov	x10, 0b11
	msr	cntkctl_el1, x10 */

	/* Turn to el1 from other exception levels. */
	bl 	arm64_elX_to_el1

	/* Prepare stack pointer and jump to C. */
	mov	x1, #INIT_STACK_SIZE
	mul	x1, x8, x1
	adr 	x0, boot_cpu_stack
	add	x0, x0, x1
	add	x0, x0, #INIT_STACK_SIZE
	mov	sp, x0

wait_until_smp_enabled:
	/* CPU ID should be stored in x8 from the first line */
	mov	x1, #8
	mul	x2, x8, x1
	ldr	x1, =secondary_boot_flag
	add	x1, x1, x2
	ldr	x3, [x1]
	cbz	x3, wait_until_smp_enabled

	/* Set CPU id */
	mov	x0, x8
	b 	secondary_init_c

	/* Should never be here */
	b	.

primary:

	/* Turn to el1 from other exception levels. */
	bl 	arm64_elX_to_el1

	/* Prepare stack pointer and jump to C. */
	adr 	x0, boot_cpu_stack
	add 	x0, x0, #INIT_STACK_SIZE
	mov 	sp, x0

	b 	init_c

	/* Should never be here */
	b	.
END_FUNC(_start)

思考题 4

说明为什么要在进入 C 函数之前设置启动栈。如果不设置,会发生什么?

进入 init_c 函数后,第一件事首先通过 clear_bss 函数清零了 .bss 段,该段用于存储未初始化的全局变量和静态变量(具体请参考附录)。

思考题 5

在实验 1 中,其实不调用 clear_bss 也不影响内核的执行,请思考不清理 .bss 段在之后的何种情况下会导致内核无法工作。

初始化串口输出

到目前为止我们仍然只能通过 GDB 追踪内核的执行过程,而无法看到任何输出,这无疑是对我们写操作系统的积极性的一种打击。因此在 init_c 中,我们启用树莓派的 UART 串口,从而能够输出字符。

kernel/arch/aarch64/boot/raspi3/peripherals/uart.c 已经给出了 early_uart_initearly_uart_send 函数,分别用于初始化 UART 和发送单个字符(也就是输出字符)。

void uart_send_string(char *str)
{
        /* LAB 1 TODO 3 BEGIN */
        /* BLANK BEGIN */
        /* BLANK END */
        /* LAB 1 TODO 3 END */
}

练习题6

kernel/arch/aarch64/boot/raspi3/peripherals/uart.cLAB 1 TODO 3 处实现通过 UART 输出字符串的逻辑。

第一个字符串

恭喜!我们终于在内核中输出了第一个字符串!
感兴趣的同学请思考early_uart_send究竟是怎么输出字符的。

启用 MMU

在内核的启动阶段,还需要配置启动页表(init_kernel_pt 函数),并启用 MMU(el1_mmu_activate 函数),使可以通过虚拟地址访问内存,从而为之后跳转到高地址作准备(内核通常运行在虚拟地址空间 0xffffff0000000000 之后的高地址)。

关于配置启动页表的内容由于包含关于页表的细节,将在本实验下一部分实现,目前直接启用 MMU。

在 EL1 异常级别启用 MMU 是通过配置系统寄存器 sctlr_el1 实现的(Arm Architecture Reference Manual D13.2.118)。具体需要配置的字段主要包括:

  • 是否启用 MMU(M 字段)
  • 是否启用对齐检查(A SA0 SA nAA 字段)
  • 是否启用指令和数据缓存(C I 字段)

练习题7

kernel/arch/aarch64/boot/raspi3/init/tools.SLAB 1 TODO 4 处填写一行汇编代码,以启用 MMU。

由于没有配置启动页表,在启用 MMU 后,内核会立即发生地址翻译错误(Translation Fault),进而尝试跳转到异常处理函数(Exception Handler), 该异常处理函数的地址为异常向量表基地址(vbar_el1 寄存器)加上 0x200。 此时我们没有设置异常向量表(vbar_el1 寄存器的值是0),因此执行流会来到 0x200 地址,此处的代码为非法指令,会再次触发异常并跳转到 0x200 地址。 使用 GDB 调试,在 GDB 中输入 continue 后,待内核输出停止后,按 Ctrl-C,可以观察到内核在 0x200 处无限循环。


important

以上为Lab1 Part1 的内容

Last change: 2024-09-07, commit: debf8d0