【操作系统实验】ChCore lab1 - 机器启动

ChCore lab1 - 机器启动

1. 内核构建

aarch64 汇编

慢慢学。

构建 ChCore 内核

课程使用 QEMU 模拟 Raspberry Pi 3 启动 Chcore。使用 docker 进行交叉编译:

1
make build

对应 Makefile 中的:

1
2
3
4
5
6
 ... ...

build: FORCE
./scripts/docker_build.sh

... ...

docker_build.sh 内容如下:

1
2
3
4
#!/bin/bash
rm -rf ./build

docker run -it --rm -u $(id -u ${USER}):$(id -g ${USER}) -v $(pwd):/chos -w /chos ipads/chcore_builder:v1.0 ./scripts/build.sh "$@"

build.sh 与编译相关的内容如下:

1
2
3
4
5
6
7
8
cmake -DCMAKE_LINKER=aarch64-linux-gnu-ld -DCMAKE_C_LINK_EXECUTABLE="<CMAKE_LINKER> <LINK_FLAGS> <OBJECTS> -o <TARGET> <LINK_LIBRARIES>" .. -G Ninja "$@"
echo "before ninja"
ninja
echo "after ninja"
if [ ! -f "kernel.img" ]; then
echo "kernel.img is not exist"
fi
aarch64-linux-gnu-nm -n kernel.img > kernel.sym

可以看到,是使用 ninja 编译的。细节不深究了。

2. 内核的引导和加载

bootloader 的功能

Raspi 3 从闪存中加载 .img 镜像的 bootloader 并执行。bootloader 主要负责两个功能:

  1. 调用函数 arm64_elX_to_el1() 将处理器的异常级别切换到 EL1。
  2. 初始化引导 UART,页表和 MMU,随后跳转到实际的内核。

bootloader 的源文件由汇编文件 boot/start.S 和 C 语言文件 boot/init_c.c 组成。

内核的加载和执行

加载:将程序的 ELF 文件按照连接规则从存储器按照段加载地址拷贝到内存的制定地址上。

执行:将 ELF 文件中的段放到虚拟内存地址,然后执行 ELF 中的代码。

3. 内核态基础功能

为了支持在引导阶段内核执行过程中进行调试,所以需要以下支持两种功能。

内核态输入输出

本课程第一个需要编程的点,在 kernel/common/printk.c 中实现 printk_write_num() 函数以实现格式化输出。

在 common/printk.c 的 printk_write_num() 函数中补全以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
s = &print_buf[PRINT_BUF_LEN];
*s-- = 0;
while (u > 0) {
t = u % base;
if (t >= 10) {
*s-- = letbase + t - 10;
} else {
*s-- = '0' + t;
}
u /= base;
}
s++;

函数栈

栈顶:Stack Pointer,SP 寄存器;

栈底:Frame Pointer,FP 寄存器。

gdb 调试运行,在 stack_test() 函数下断点:

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) b stack_test
Breakpoint 1 at 0xffffff000008c030: file ../kernel/main.c, line 27.
(gdb) c
Continuing.

Thread 1 hit Breakpoint 1, stack_test (x=5) at ../kernel/main.c:27
27 ../kernel/main.c: No such file or directory.
(gdb) x/10xg $sp
0xffffff00000920c0 <kernel_stack+8128>: 0xffffff00000920e0 0xffffff000008c0c0
0xffffff00000920d0 <kernel_stack+8144>: 0x0000000000000000 0x00000000ffffffc0
0xffffff00000920e0 <kernel_stack+8160>: 0x00000000000887e0 0xffffff000008c018
0xffffff00000920f0 <kernel_stack+8176>: 0x0000000000000000 0x00000000000873c8
0xffffff0000092100 <kernel_stack+8192>: 0x0000000000000000 0x0000000000000000

打印函数栈信息就能看到,arm 架构和 x86 架构对于函数栈的处理是类似的。0xffffff00000920e0 就是主调函数的栈基地址,0xffffff000008c0c0 就是当前函数返回地址。再后面的值就是当前函数的参数,包括 0x0000000000000000 等。单独从函数栈没办法看出函数参数有几个,只能把后面的都当作参数打印了。

在 monitor.c 中填写下面代码:

1
2
3
4
5
6
// Your code here.
u64 *fp = (u64 *)read_fp();
while (*fp){
printk("LR %lx FP %lx Args %lx\n", ((u64 *)fp[0])[1], fp[0], fp[2]);
fp = (u64 *)fp[0];
}

需要注意的是,每一行打印的是:主调函数 LR,主调函数 FP,参数。fp[0] 表示主调函数 FP,而主调函数返回地址保存在主调函数栈,所以是 ((u64 *)fp[0])[1]

附:基础知识

AArch64 特权级

主要是和 x86 架构的 ring0~4 进行区分。

image-20210906132853098

  • EL0:用户态,应用程序运行在该特权级,是最低的特权级。
  • EL1:内核态,操作系统运行的特权级。
  • EL2:虚拟化场景需要,VMM 运行的特权级。
  • EL3:与 TrustZone 相关,负责普通世界和安全世界的切换。

寄存器

除了 31 个通用寄存器(x0~x31, pc)其中 x29 别名 fp(保存函数栈帧),x30 别名 lp(保存函数返回地址)。