NKU-EmbeddedSystem / ucore_plus_riscv

ucore plus os on RISC-V

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

uCore plus on RISC-V

启动

使用 QEMU 自带的 OpenSBI 固件作为 bootloader。OpenSBI 运行在机器模式,提供一些接口供编写内核使用。

OpenSBI 的代码位于从 0x80000000 到 0x80200000 的物理内存,所以内核代码放在 0x80200000 之后的一段连续内存空间,在链接脚本中定义内核程序的基址为 0x80200000。在 bss 段前后分别定义 edata 和 end 变量,记录下它在内存中的范围。为了实现对寄存器的精细控制,入口处函数采用汇编编写,命名为 kern/init/entry.S。链接器需要指定一个符号作为入口,在汇编中手动定义一个全局符号.globl kern_entry,实际上就是一个地址。

进程运行时都有自己的堆栈段。在汇编的数据段,按 4KiB 对齐后,定义内核栈的起始位置,空出 8KiB 的栈空间。在这段内存空间的最后再定义一个符号作为栈顶的记录,并将它赋给 sp 寄存器。这样就构造好了内核栈的空间。

C 语言的入口函数为 kern_init,位于 kern/init/init.c,在这里进行操作系统内核各个模块的初始化。作为内核的主函数,相信它不会返回,所以在函数声明时利用 GCC 的特性指定函数属性为 noreturn,可以帮助编译器生成更加优化的汇编代码。利用了链接脚本中记录下的 bss 段在内存中的位置,将 bss 段的数据全部置零。

串口输出

OpenSBI 固件运行机器模式,需要通过 ecall 命令来调用它的接口,通过寄存器可以传递调用编号和参数,OpenSBI 默认实现了 0-8 号调用,包括时钟、输入、输出和处理器间中断等功能。通过层层封装 ecall 来实现输出函数。

使用 GCC 扩展内联汇编先编写一个通用的 ecall 调用函数 sbi_call,根据 OpenSBI 的调用约定,a7 保存调用编号,a0-a2 保存参数,调用结果的返回值存在 a0 中。此外,需要借助扩展内联汇编将寄存器中的指拿到 C 语言变量中,并作为 sbi_call 函数的返回值。内联汇编需要添加 volatile 修饰符,取消编译器优化以防止意外情况。

封装 sbi_console_putchar 函数,将输出字符的调用编号和要输出的字符作为 ecall 参数。然后在设备驱动模块下编写控制台代码,将 OpenSBI 字符输出接口封装为控制台字符输出函数。

时钟中断

中断入口的代码采用汇编编写,因为采用非向量模式处理中断,所以要定义一个所有中断的入口标签,同时借助 .align(2) 实现入口地址 4 字节对齐。先保存上下文,实际上就是把 trapframe 中定义的寄存器保存到栈上,相当于手动拼装了一个结构体。减小 sp 以延申栈,将通用寄存器存入栈。RISC-V 指令集不支持直接将 CSR 写入内存,需要用通用寄存器作为中转。CSR 的读写依赖专用的指令 csrrw 或它的简化伪指令。随后用 jal 指令跳转到 C 语言中的中断处理函数,根据调用约定,需要将 sp 赋给作为参数寄存器的 a0,这样 C 语言部分才能获得 trapframe 的数据。

处理结束后恢复上下文,基本是保存上下文的逆过程,但中断处理结束后 stval 和 scause 没有必要恢复了,它们的值会在下一个中断产生时自动被硬件设置。最后使用 sret 指令从中断返回,实际上是把 sepc 的值赋给 pc,回到产生中断的位置继续执行。

操作系统启动后并不知道中断入口的地址,所以操作系统初始化时应初始化中断信息。将 sscratch 置零,表明中断来自内核,通过 C 语言的 extern 关键字显式声明汇编代码定义的中断入口,并将它写入 stvec。然后需要将 status 的 SIE 置为 1,使能中断。C 语言的中断处理函数需要分发中断实现针对处理,如果 scause 的最高位是 0 说明这是一个异常,否则是一个中断。时钟属于中断,编号为 5。

mtime 寄存器是 RISC-V 的计时器,用 rdtime 指令读取。使用 OpenSBI 的 sbi_set_timer 接口模拟物理上的时钟中断触发,参数是一个 64 位无符号整数,当 mtime 的值达到这个数就会触发时钟中断。在时钟中断的处理函数中调用它,相当于每次处理时钟中断都会相应地设置下一次时钟中断,以实现源源不断的时钟中断。

虚拟内存空间建立

satp 是地址翻译和保护寄存器,控制监督模式的地址翻译与保护功能。在 64 位 RISC-V 架构下,0 到 44 位的 PPN 字段存储根页表的物理页号。60-63 位的 MODE 字段控制当前的地址翻译模式,目前 RISC-V 支持 Bare、Sv39 或 Sv48 模式。

Bare 模式不启用地址翻译或保护,虚拟地址就等于物理地址。虚拟内存管理采用 Sv39 模式,即基于页的 39 位虚拟内存系统(page-based 39-bit virtual-memory system)。当 Sv39 被写入 satp 寄存器的 MODE 字段时,监督模式就会启用 39 位虚拟内存系统。Sv39 支持 39 位的虚拟地址空间,即 512GiB,页的粒度是 4KiB。

页表项的 V 位代表页表项是否有效,一旦为 0 则代表整个页表的各字段都失去含义。R、W 和 X 三个位记录页表项对应的页的权限,即可读、可写和可执行。规定当三者同时为零时代表此页表项指向下一级页表,否则就是一个叶子页表项。

为操作系统添加虚拟地址的功能需要改写操作系统入口的汇编代码,添加手动构造页表和启用分页机制的代码。实现一个最基础的线性映射规则,物理地址等于虚拟地址减 0xFFFFFFFF40000000。Sv39 三级页表的一个页表项可以映射 1GiB 的虚拟空间,假设这足够内核使用,我们通过一个三级页表将 0xFFFFFFFFC0000000-0xFFFFFFFFFFFFFFFF 的虚拟地址映射为 0x80000000-0xC00000000 的物理地址。页表本身也需要内存,所以在 kern/init/entry.S 中要分配 4KiB 内存用于存放一个三级页表,要对齐 4KiB 内存。实际只需要三级页表的一个页表项,所以把前 511 项借助 .zero 全部设为 0。最后一个页表项设置物理页号为 0x80000,DAXWRV 均设为 1。用三级页表的首地址减去之前规定的虚拟地址偏移量 0xFFFFFFFF40000000,再左移 12 位就可以得到它的物理页号。把代表 Sv39 模式的 8 和物理页号拼接并写入 satp,启动 Sv39 分页模式。虚拟内存空间的构建完成。

编译运行

实验环境:

  • 主机:Ubuntu 18.04.1 Desktop AMD64
  • 交叉编译工具链:riscv64-unknown-elf-gcc 8.3.0
  • 模拟器:qemu-system-riscv64 4.1.1
  • RISC-V 机器:virt

使用 QEMU 指定固件和磁盘镜像,运行验证 ucore plus:

qemu-system-riscv64 -machine virt \
	-nographic -bios default \
	-device loader,file=obj/ucore.img,addr=0x80200000

About

ucore plus os on RISC-V


Languages

Language:C 95.0%Language:ASL 1.5%Language:Assembly 1.0%Language:Yacc 0.8%Language:Makefile 0.5%Language:Lex 0.4%Language:Roff 0.4%Language:C++ 0.3%Language:Shell 0.2%Language:Python 0.0%Language:Batchfile 0.0%