ChCore_lab1 做题笔记

简介

文档链接:Lab1: 机器启动 - IPADS OS Course Lab Manual

本实验作为 ChCore 操作系统课程实验的第一个实验,分为三个部分。

  1. RTFSC: 代码导读,由于是Lab1,我们主要注重于Chcore的构建系统,这部分没有习题。
  2. 机器启动:介绍aarch64结构启动时的关键寄存器以及关键的启动函数。
  3. 页表配置:介绍aarch64页表结构,以及针对树莓派3平台的内存布局编写页表配置。

调试指北

在开始实验之前,请务必读完调试指北,以帮助你快速上手调试。

思考题 1

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

1
2
3
4
	# the begin of _start
	mrs	x8, mpidr_el1
	and	x8, x8,	#0xFF
	cbz	x8, primary

从中我们可以知道,在函数的开头会从mpidr_el1寄存器中读取cpu的编号,当cpu的编号为0时(即当前核心是主核心时)就会跳转到primary进行初始化

练习2

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

arm64_elX_to_el1,顾名思义,从特权级elX降级到el1,其中X代表任意特权级

根据ChCore文档中的提示,我们可以从CurrentEL寄存器中获取当前的特权级,又根据Arm文档中的介绍:

我们可以很简单的知道,应该把CurrentEL中的值获取到x9寄存器里,即

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
	# the begin of arm64_elX_to_el1
	
	/* LAB 1 TODO 1 BEGIN */
	/* BLANK BEGIN */
	mrs x9, CurrentEL
	/* 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

练习题 3

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

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

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

  1. elr_el3,参考el1时的操作,从el3返回el1时同样应该回到.Ltarget处来返回

    可以通过指令adr,获取标签的地址并存入寄存器中

  1. spsr_el3,根据下图

    并且结合arm文档中的解释:Inject Undefined Instruction exception. Set to 0 on taking an exception to EL3, and copied to PSTATE.UINJ on executing an exception return operation in EL3.(注入未定义的指令异常。将异常带入 EL3 时设置为 0,在 EL3 中执行异常返回操作时复制到 PSTATE.UINJ。)

    DAIF四类中断的位数是 [9:6],而M[3:0]指定返回特权级的sp指针

    由于lab1贴心的定义好了相应的变量,我们直接使用变量就好了

1
2
3
4
5
6
7
8
9
	# Set the return address and exception level.
	/* LAB 1 TODO 2 BEGIN */
	/* BLANK BEGIN */
	adr x9, .Ltarget
	msr elr_el3, x9
	mov x9, SPSR_ELX_DAIF | SPSR_ELX_EL1H
	msr spsr_el3, x9
	/* BLANK END */
	/* LAB 1 TODO 2 END */

思考题 4

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

因为调用函数需要使用到栈来保存状态,如果不启用栈,会导致函数调用失败

思考题 5

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

当内核中的某个函数依赖全局变量的初始值为0时,不清理.bss可能会导致出现无法预料的异常

练习题6

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

1
2
3
4
5
6
7
8
9
void uart_send_string(char *str)
{
        /* LAB 1 TODO 3 BEGIN */
        /* BLANK BEGIN */
        for (int i = 0; str[i] != 0; i++)
                early_uart_send(str[i]);
        /* BLANK END */
        /* LAB 1 TODO 3 END */
}

练习题7

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

我们可以在kernel/include/arch/aarch64/arch/machine/registers.h中找到相应的变量定义

很容易知道应该答案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
	mrs     x8, sctlr_el1
	/* Enable MMU */
	/* LAB 1 TODO 4 BEGIN */
	/* BLANK BEGIN */
	orr		x8, x8, #SCTLR_EL1_M
	/* BLANK END */
	/* LAB 1 TODO 4 END */
	/* Disable alignment checking */
	bic     x8, x8, #SCTLR_EL1_A
	bic     x8, x8, #SCTLR_EL1_SA0
	bic     x8, x8, #SCTLR_EL1_SA
	orr     x8, x8, #SCTLR_EL1_nAA
	/* Data accesses Cacheable */
	orr     x8, x8, #SCTLR_EL1_C
	/* Instruction access Cacheable */
	orr     x8, x8, #SCTLR_EL1_I
	msr     sctlr_el1, x8

这里关闭了对齐检查,启用了指令和数据缓存

变量SCTLR_EL1_M表示启动MMU

思考题 8

请思考多级页表相比单级页表带来的优势和劣势(如果有的话),并计算在 AArch64 页表中分别以 4KB 粒度和 2MB 粒度映射 0~4GB 地址范围所需的物理内存大小(或页表页数量)。

多级页表优势是不需要一次性地将所有空间都映射,尽管在映射大空间时占用内存多,但一般场景下需要映射地空间都很小,减少了内存的浪费。劣势是翻译需要多次访问内存,性能较差。

4KB粒度需要1024*1024个条目,则需要2048个3级页表,其中这2048个3级页表又需要4个2级页表项,总共需要2048+3个页表(考虑L0~L2)。

2MB粒度下需要2048个条目,需要4个2级页表,总共需要4+2个页表(考虑L0~L1)。


init_kernel_pt 为内核配置从 0x000000000x800000000x40000000 后的 1G,ChCore 只需使用这部分地址中的本地外设)的映射,其中 0x000000000x3f000000 映射为 normal memory,0x3f0000000x80000000映射为 device memory,其中 0x000000000x40000000 以 2MB 块粒度映射,0x400000000x80000000 以 1GB 块粒度映射。

思考题 9

请结合上述地址翻译规则,计算在练习题 10 中,你需要映射几个 L2 页表条目,几个 L1 页表条目,几个 L0 页表条目。页表页需要占用多少物理内存?

0x0000_0000到0x4000_0000总共1024G,以2MB为粒度,需要524288个L2条目,1024个L1条目,2个L0条目

0x4000_0000到0x8000_0000总共1024G,以1G为粒度,需要1024个L1条目,需要2个L0条目

总共1028*4KB内存(不考虑L0)

练习题 10

init_kernel_pt 函数的 LAB 1 TODO 5 处配置内核高地址页表(boot_ttbr1_l0boot_ttbr1_l1boot_ttbr1_l2),以 2MB 粒度映射。

这里低位和高位的映射都是一样的,我们可以直接copy低位的配置来直接修改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* TTBR1_EL1 0-1G */
/* LAB 1 TODO 5 BEGIN */
/* Step 1: set L0 and L1 page table entry */
/* BLANK BEGIN */
vaddr = KERNEL_VADDR + PHYSMEM_START;
boot_ttbr1_l0[GET_L0_INDEX(vaddr)] = ((u64)boot_ttbr1_l1) | IS_TABLE
                                     	| IS_VALID;
boot_ttbr1_l1[GET_L1_INDEX(vaddr)] = ((u64)boot_ttbr1_l2) | IS_TABLE
                                     	| IS_VALID;
/* BLANK END */

/* Step 2: map PHYSMEM_START ~ PERIPHERAL_BASE with 2MB granularity */
/* BLANK BEGIN */
for (; vaddr < KERNEL_VADDR + PERIPHERAL_BASE; vaddr += SIZE_2M) {
	boot_ttbr1_l2[GET_L2_INDEX(vaddr)] =
		(vaddr - KERNEL_VADDR) | UXN /* Unprivileged execute never */
			| ACCESSED /* Set access flag */
			| INNER_SHARABLE /* Sharebility */
			| NORMAL_MEMORY /* Normal memory */
			| IS_VALID;
}
/* BLANK END */

/* Step 2: map PERIPHERAL_BASE ~ PHYSMEM_END with 2MB granularity */
/* BLANK BEGIN */
for (vaddr = KERNEL_VADDR + PERIPHERAL_BASE;
	vaddr < KERNEL_VADDR + PHYSMEM_END;
	vaddr += SIZE_2M) {
	boot_ttbr1_l2[GET_L2_INDEX(vaddr)] =
		(vaddr - KERNEL_VADDR) /* low mem, va = pa */
			| UXN /* Unprivileged execute never */
			| ACCESSED /* Set access flag */
			| DEVICE_MEMORY /* Device memory */
			| IS_VALID;
}
/* BLANK END */
/* LAB 1 TODO 5 END */
  • boot_ttbr0_lx改成boot_ttbr1_lx
  • vaddr = KERNEL_VADDR + 物理地址 在进行映射时记得把 vaddr - KERNEL_VADDR
  • 高地址只有内核执行,不需要通过ASID来区分,所以把所有的NG删掉

思考题11

请思考在 init_kernel_pt 函数中为什么还要为低地址配置页表,并尝试验证自己的解释。

由于启动mmu之后的指令仍然在低地址中执行,如果不为低地址配置页表的话,就会翻译出错而trap在小于0x8000的地址段。直到运行到start_kernel函数时才会进入高地址运行

思考题12

在一开始我们暂停了三个其他核心的执行,根据现有代码简要说明它们什么时候会恢复执行。思考为什么一开始只让 0 号核心执行初始化流程?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void init_c(void)
{
	/* Clear the bss area for the kernel image */
	clear_bss();

	/* Initialize UART before enabling MMU. */
	early_uart_init();
	uart_send_string("boot: init_c\r\n");

	wakeup_other_cores();

	/* Initialize Kernell Page Table. */
	uart_send_string("[BOOT] Install kernel page table\r\n");
	init_kernel_pt();

	/* Enable MMU. */
	el1_mmu_activate();
	uart_send_string("[BOOT] Enable el1 MMU\r\n");

	/* Call Kernel Main. */
	uart_send_string("[BOOT] Jump to kernel main\r\n");
	start_kernel(secondary_boot_flag);

	/* Never reach here */
}

根据init_c函数我们知道在初始化完串口之后,其它的核心就会被唤醒。使用一个核心有利于简化初始化过程,在串行的初始化中我们也容易知道出错的位置,从而更好地找到问题所在。并且在启动过程中,一些操作通常需要按特定顺序执行,包括时钟、调度器、锁等,使用单个核心可以更容易地控制这一过程。