跳转到内容

Avatar OS Kernel — RISC-V 64 课前准备 ​

本文档面向学习本课程的学员,聚焦 RISC-V 64(RV64IMAC,Sv48 分页,S-mode) 这一架构实现, 整理「课程内容速览」「寄存器与指令(含特权)」「速查手册」三部分,作为每一节课动手前的背景速查。

文中所有寄存器编号、位域、指令、内存布局均取自 kernel/ 真实实现 (boot/riscv64/、include/riscv64/、mm/riscv64/、kernel/task/riscv64/、driver/)与 Makefile,可与课程正文对照。参考权威细节请查阅《The RISC-V Instruction Set Manual, Volume II: Privileged Architecture》。


0. 一句话背景 ​

本内核运行在 QEMU virt 机器 + OpenSBI(-bios default) 之上:OpenSBI 跑在 M-mode, 把控制权以 S-mode 移交给我们的内核(_start)。内核自己开启 Sv48 分页、安装异常向量、 接管定时器与 PLIC 中断,并实现多任务、用户态、系统调用与多核(SMP)。

  • DRAM 物理基址:0x80000000
  • 内核加载地址(OpenSBI 跳转入口):0x80200000
  • 内核高半区直接映射偏移:KERNEL_VMA_OFF = 0xFFFFFFFF00000000(即 VA = PA + 0xFFFFFFFF00000000)
  • 工具链:riscv64-linux-musl-gcc;运行:qemu-system-riscv64

1. 课程内容速览 ​

24 节课分 5 个阶段,4 个里程碑。下表标注每课在 RISC-V 64 上的关键实现文件,便于课前定位。

第一阶段:裸机基础(97–102)—— 最小内核骨架 ​

课标题RISC-V 64 关键文件 / 知识点
97Bare-Metal Helloboot/riscv64/boot.S:直接写 UART 物理地址 0x10000000 输出 "Hello"
98Freestanding Typesinclude/types.h:自定义 uint64_t、size_t、vaddr_t/paddr_t、NULL、ALIGN_UP;无标准库
99Kernel Logginginclude/klog.h、lib/klog.c、lib/vsnprintf.c:KLOG_* 宏 → kprintf → UART
100Linker Scriptboot/riscv64/link.ld:双 VMA/LMA(KERNEL_LOAD_BASE=0x80200000,KERNEL_VMA_OFF)
101Boot Assemblyboot/riscv64/boot.S:_start 关中断、设引导栈、建页表、开 MMU、trampoline 到高半区
102Arch Abstractioninclude/arch.h(__riscv + __riscv_xlen==64 → ARCH_RISCV64)、include/barrier.h/riscv64/barrier_impl.h

第二阶段:内存与同步(103–108)—— 虚拟内存完备(里程碑 M2 @108) ​

课标题RISC-V 64 关键文件 / 知识点
103Memory Barriersriscv64/barrier_impl.h:fence rw,rw / fence r,r / fence w,w
104Spinlocksriscv64/spinlock_impl.h:LR/SC(lr.w/sc.w)+ fence;中断开关用 csrrci sstatus,2
105Physical Memorykernel/mm/pmm.c:物理页帧分配(里程碑 M1 基础设施)
106Page Tablesriscv64/mmu.h:PTE 标志位(V/R/W/X/U/G/A/D)、make_satp、pte_table/pte_leaf
107MMU Enablemm/riscv64/mmu.S:csrw satp + sfence.vma;mm/riscv64/vm_early.c:2 级页表(恒等 + 高半区)
108Kernel Address Spaceinclude/mm_vm.h:virt_to_phys/phys_to_virt 直接映射;main.c 反推 kernel_main 的 PA/VA

第三阶段:中断与定时器(109–112)—— 让内核有"心跳" ​

课标题RISC-V 64 关键文件 / 知识点
109Exception Vectorsboot/riscv64/trap.S:trap_vector(.align 4,direct 模式写入 stvec=0x105)
110Exception Handlingboot/riscv64/exception.c:handle_exception 解析 scause,分发 timer/plic/ecall/ebreak
111Timer Driverdriver/timer/riscv64/timer.c:经 SBI legacy set_timer(EID=0) 编程 ACLINT;irq_install(5,...)
112Interrupt Controllersdriver/irq/riscv64/plic.c:PLIC claim/complete(IRQ 10=UART),sie 置 SEIE(bit9)

第四阶段:多任务(113–116)—— 抢占式并发(里程碑 M3 @116) ​

课标题RISC-V 64 关键文件 / 知识点
113Task Control Blockkernel/task/task.h、task.c:任务结构体、就绪队列
114Context Switchkernel/task/riscv64/switch.S:仅保存 callee-saved(ra + s0–s11,104 字节),csrs sstatus,0x2 开 SIE
115Round-Robin Schedulerkernel/task/sched.c:协作式 schedule(),显式 ebreak/yield 让出
116Preemptive Schedulingkernel/task/preempt.c:定时器中断置 need_resched,返回路径 sched_check_and_yield() 延迟切换

第五阶段:用户态与多核(117–120)—— 完整 OS(里程碑 M4 @120) ​

课标题RISC-V 64 关键文件 / 知识点
117User Modekernel/task/riscv64/user.S:arch_user_enter 经 sret 进 U-mode;sscratch 区分陷入来源
118System Callsboot/riscv64/exception.c:cause==8(U-mode ecall)→ syscall_dispatch,a7=调用号,sepc+=4
119ELF Loaderkernel/loader/elf_loader.c + apps/riscv64/welcome.c:加载独立 ELF 到 U-mode 地址空间运行
120SMP Multicoreboot.S _secondary_start:BSP 经 SBI hart_start 唤醒次级核,复用页表 csrw satp+sfence.vma

里程碑 ​

  • M1 (105) 最小内核基础设施:UART + 日志 + 栈 + 锁 + 物理内存
  • M2 (108) 虚拟内存完备:页表 + MMU + 地址空间隔离
  • M3 (116) 抢占式多任务:定时器驱动的上下文切换
  • M4 (120) 完整操作系统:用户态 + 系统调用 + 多核

2. RISC-V 64 通用寄存器 ​

RV64 有 32 个 64 位通用寄存器 x0–x31,约定(ABI 名)如下。内核全程使用 S-mode,调用约定遵循 RISC-V ELF psABI。

寄存器ABI 名用途是否易失(调用约定)
x0zero恒为 0(写无效)—
x1ra返回地址调用者保存
x2sp栈指针被调用者保存
x3gp全局指针(本内核基本不用,靠 lla PC 相对寻址)—
x4tp线程指针(SMP 中可放 per-hart 信息)—
x5t0临时寄存器调用者保存
x6t1临时寄存器调用者保存
x7t2临时寄存器调用者保存
x8s0/fp被保存寄存器 / 帧指针被调用者保存
x9s1被保存寄存器被调用者保存
x10a0函数参数 1 / 返回值调用者保存
x11a1函数参数 2 / 返回值(64 位高/次)调用者保存
x12–x17a2–a7函数参数 3–8调用者保存
x18–x27s2–s11被保存寄存器被调用者保存
x28–x31t3–t6临时寄存器调用者保存

关键点(与内核代码强相关):

  • 参数/返回值:C 函数前 8 个参数走 a0–a7(x10–x17);系统调用号走 a7(x17);返回值走 a0。
  • callee-saved 何处保存:trap.S 保存全部 x1–x31(跳过 x0);switch.S 只保存 ra + s0–s11(13 个,104 字节),因为 a/t 由编译器在调用点自己处理。
  • gp 不用:本工具链下全局符号走 GOT 在 -kernel 加载时不稳定,故引导期一律用 lla(PC 相对,auipc+addi) 取地址;高 VMA 符号靠链接脚本提供的 *_pa 别名。
  • fp(s0) 在这里只是普通被保存寄存器,没有开启栈回溯帧指针。

3. RISC-V 64 通用指令速查 ​

下面列出本内核及裸机开发最常用指令(RV64I + 原子扩展 + 系统扩展)。

3.1 寄存器搬运 / 算术 / 逻辑(imm 版 = i 后缀) ​

指令含义
mv rd, rs伪指令,等价 addi rd, rs, 0
li rd, imm加载 32/64 位常量(自动拆 lui+addi)
la rd, sym加载符号地址(GOT,引导期慎用)
lla rd, symPC 相对加载符号地址(auipc+addi),引导期首选
add/sub rd,rs,rt / addi rd,rs,imm加 / 减
sll/srl/sra / slli/srli/srai逻辑/算术移位
and/or/xor / andi/ori/xori位运算
slt/sltu / slti/sltiu有/无符号比较置 1
lui rd, imm加载到高 20 位(rd = imm<<12)
auipc rd, immrd = pc + (imm<<12),PC 相对基址

3.2 访存(load/store,均按 imm(rs) 寻址) ​

指令含义
lb/lbu lh/lhu lw/lwu ld加载 8/16/32/64 位(有/无符号)
sb sh sw sd存 8/16/32/64 位
例:sd x1, 1*8(sp)把 x1 存到 sp + 8

3.3 控制流 ​

指令含义
jal rd, offset / jalr rd, rs, imm跳转并链接(jal 用于 call/la 等;jalr 用于 ret/jr)
call sym / ret / jr rd伪指令(基于 jal/jalr ra)
beq/bne blt/bltu bge/bgeu分支(相等/不等/有符号小于/…)
bnez t0, 1f / bgeu t0,t1,2f1f/2f 表示前方标号(页内)

3.4 原子指令(自旋锁核心,见 104 课) ​

指令含义
lr.w rd, (rs)Load-Reserved:读 32 位并"预约"该地址
sc.w rd, rs, (rt)Store-Conditional:若期间无人改过则写,成功 rd=0,否则 rd≠0
amoswap.w/sw.aqrl 等若后续扩展到更重锁会用到(本内核只用 LR/SC)

4. RISC-V 64 特权级与特权资源 ​

4.1 特权级(Privilege Modes) ​

级别名本内核中的角色
0U-mode用户态程序(117+,ELF loader 加载的应用)
1S-mode内核运行于此(OpenSBI 移交后)
3M-modeOpenSBI 固件(我们不直接写,仅用 SBI 调用)
  • 进入内核前:csrw sie, zero / csrw sip, zero 关中断,避免无向量表时收中断崩溃。
  • 切换到内核态或用户态的"开关"靠 sstatus 的 SPP 位 + sret 完成(见 117)。

4.2 特权寄存器(CSR)—— 本内核用到的全部 ​

CSR 地址为 12 位;读写用 CSR 指令(见 4.3)。下表是课程中实际出现的 CSR:

CSR地址全称在内核中的用途
sstatus0x100Supervisor Status全局中断开关 SIE(bit1)、返回态 SPP(bit8)、SPIE(bit5)、SUM(bit18)
satp0x180Supervisor Address Translation & Protection页表根 + 分页模式(MODE=9 → Sv48)
sie0x104Supervisor Interrupt Enable使能 STIE(bit5)/SEIE(bit9)/SSIE(bit1)
sip0x144Supervisor Interrupt Pending中断挂起位(清 SSIP 用 csrci sip,0x2)
stvec0x105Supervisor Trap Vector异常入口地址(.align 4 → direct 模式)
sepc0x141Supervisor Exception PC陷入前 PC;ecall/ebreak 后需 sepc += 4 跳过
scause0x142Supervisor Cause陷入原因(最高位=中断;低 8 位=异常/中断号)
stval0x143Supervisor Trap Value附加信息(如 page fault 的故障地址)
sscratch0x140Supervisor Scratch用户态陷入时存内核陷阱栈顶;0 表示当前在内核态
time0xC01(U/S 可读)定时器计数器读当前时间,配合 SBI set_timer 设定下次中断
cycle/instret0xC00/0xC02周期 / 指令数可选性能计数(本内核未强依赖)

注:本工具链(riscv64-linux-musl-gcc)中 stvec、satp、sie、sip、sepc、scause、stval、sscratch 等助记符可直接用,无需写裸地址。

4.3 特权指令 ​

指令含义本内核出现处
ecall触发环境调用(陷入 S/M,取决于当前特权级)用户态 syscall;SBI 调用(sbi_set_timer)
sret从 S-mode 陷阱返回(PC←sepc,特权级←sstatus.SPP)trap.S 返回;user.S 进入用户态
sfence.vma刷新 TLB(地址/ASID 可选)开 MMU 后、切页表后
wfiWait For Interrupt,进入低功耗直到中断内核 idle 循环、hang 兜底
fence内存屏障(前序访存对后续可见)自旋锁、barrier 实现
csrr rd, csr读 CSR 到寄存器csrr t0, sepc 等
csrw csr, rs写寄存器到 CSRcsrw stvec, t0、csrw satp, a0
csrs csr, rs置位 CSR 指定位csrs sstatus, 0x2(开 SIE)、csrs sie, t0
csrc csr, rs清位 CSR 指定位csrc sstatus, t0(清 SPP)
csrrw rd, csr, rs读改写(原子交换)trap.S 用 csrrw sp, sscratch, sp 切栈
csrrs/csrrc rd, csr, rs读并置位/清位csrrci sstatus,2(读并清 SIE)
csrci/csrsi csr, imm立即数清/置位(仅 5 位立即数)csrci sip, 0x2(清 SSIP)

陷阱:csrsi/csrci 立即数仅 5 位宽。开 STIE(bit5) 与 SEIE(bit9) 已超过范围,故代码先 li t0, (1<<5) 再 csrs sie, t0。

4.4 sstatus 关键位域(最重要) ​

位名含义
1SIE全局中断使能(S 态)。0 = 关中断
5SPIE陷入前的中断使能快照;sret 时恢复为 SIE
8SPP陷入前特权级:1=S-mode,0=U-mode;sret 据此决定返回到哪一级
18SUM1 = S-mode 可执行/读写用户页(write 系统调用读用户缓冲区需要)
  • 进入用户态前:csrc sstatus, (1<<8)(清 SPP→0,回 U-mode)、csrs sstatus, (1<<5)|(1<<18)(开 SPIE、开 SUM)。
  • 首次切入任务前:csrs sstatus, 0x2(开 SIE),否则任务以关中断运行,抢占永不触发(switch.S)。

4.5 scause 编码(异常/中断号) ​

scause 最高位(bit63)=1 表示中断,否则为同步异常。低 8 位为编号。本内核关心的:

值类型名内核处理
1中断Supervisor Software Interrupt(IPI,核间)清 sip SSIP(csrci sip,0x2)
5中断Supervisor Timer Interrupttimer_handler(递减时间片)
9中断Supervisor External(PLIC)plic_external_irq
3异常Breakpoint(ebreak)若 a7==TASK_YIELD_MAGIC 则让出;否则死停
8异常Environment call from U-mode(ecall)系统调用分发,sepc+=4
13异常Load page fault打印 fault 地址
15异常Store/AMO page fault打印 fault 地址(main.c 主动触发做演示)

中断号提取:irq = cause & 0xFF;区分中断/异常:cause & (1ULL<<63)。

4.6 satp 分页模式(RV64) ​

MODE值(<<60)含义
Bare0无转换(物理=虚拟)
Sv3983 级页表,39 位 VA
Sv4894 级页表,48 位 VA(本内核采用)
  • 本内核用 2 级简化映射(root L0 + 1 GiB gigapage L1),但 satp 仍按 Sv48 模式(make_satp 置 MODE=9)。
  • PTE 布局(include/riscv64/mmu.h):V(b0) R(b1) W(b2) X(b3) U(b4) G(b5) A(b6) D(b7);PPN 占 bits[53:10]。
  • 早期页表映射(vm_early.c):
    • 恒等:VA[0,1G)→PA[0,1G)、VA[2G,3G)→PA[2G,3G)(覆盖 UART 0x10000000 与内核 0x80000000)
    • 高半区:VA[0xFFFFFFFF80000000, +1G) → PA[0x80000000, +1G)
    • 只读测试区:VA[1G,2G) 映射为仅 R(触发 store page fault 演示)

4.7 SBI(Supervisor Binary Interface) ​

内核通过 ecall(在 S-mode 即陷入 M-mode 的 OpenSBI)调用固件服务。本内核用到:

调用EID(a7)FID(a6)用途
set_timer00设定下一次定时器中断(必须用 legacy EID=0,v0.2 TIME 扩展在本环境不转发中断)
hart_start0x48534D("HSM")0唤醒次级核(120 课 SMP)

5. 内核中的关键"帧"布局(务必记住) ​

这两块布局是后续 110/114/117 的调试基础。

5.1 Trap 帧 trap_frame_t(288 字节,exception.h) ​

trap.S 在陷入时 addi sp, sp, -288,按如下顺序保存(每个 8 字节):

offset  内容
0..255  x1, x2, x3, ..., x31  (32 个通用寄存器,x0 不存)
256     sepc
264     scause
272     stval
280     sstatus
        ───────── 共 288 字节 ─────────
  • x2(sp) 特殊处理:来自用户态时原用户 sp 在 sscratch;来自内核态时 = sp+288。
  • C 侧 handle_exception(frame_ptr) 直接按此结构读取/改写寄存器(如 f->x[10]=ret 设系统调用返回值,f->sepc+=4 跳过 ecall)。

5.2 上下文切换帧(104 字节,switch.S) ​

arch_task_switch 仅保存 callee-saved:ra, s0–s11(13×8=104 字节)。每个任务 task->sp 指向自己内核栈上的这帧。帧布局:[ra][s0][s1]…[s11]。


6. 速查手册(Cheat Sheet) ​

6.1 CSR 地址速查 ​

sstatus   0x100   satp      0x180   stvec   0x105   sepc    0x141
sie       0x104   sip       0x144   scause  0x142   stval   0x143
sscratch  0x140   time      0xC01

6.2 sstatus 位(1<<n) ​

SIE=1   SPIE=5   SPP=8   SUM=18

6.3 sie/sip 中断使能/挂起位 ​

SSIE=1 (IPI)   STIE=5 (Timer)   SEIE=9 (PLIC)

6.4 scause 速记 ​

中断(bit63=1):  1=IPI   5=Timer   9=PLIC
异常(bit63=0):  3=ebreak   8=ecall(U)   13=load pf   15=store pf

6.5 常用立即数 / 掩码 ​

关闭 SIE:        csrrci sstatus, 2        ; csrw sstatus, 0 也行(但要先存)
开启 SIE:        csrs sstatus, 0x2
开启 STIE:       li t0,(1<<5);  csrs sie, t0
开启 SEIE:       li t0,(1<<9);  csrs sie, t0
清 SSIP(IPI):    csrci sip, 0x2
satp Sv48:       (9ULL << 60) | (root_ppn)
页表映射偏移:    KERNEL_VMA_OFF = 0xFFFFFFFF00000000
UART 物理地址:   0x10000000
内核加载地址:    0x80200000

6.6 指令模式易错点 ​

想做正确错误
PC 相对取符号(引导期/MMU 关)lla t0, symla t0, sym(走 GOT,不稳定)
开 STIE/SEIEli t0,(1<<5); csrs sie,t0csrsi sie, 0x20(立即数超 5 位)
刷新 TLB(全)sfence.vma zero, zero仅 sfence.vma(省略操作数依赖汇编器)
stvec direct 模式.align 4 后 csrw stvec, addr.align 2(会落入 vectored 模式被 WARL 改写)

6.7 构建与运行命令 ​

课程使用 Makefile 构建与运行内核,需自己实现。

bash
# 在仓库根目录;ARCH 由根 ARCH 文件决定(当前 riscv64),也可用 make ARCH= 覆盖
make -C kernel clean
make -C kernel build          # 生成 kernel/build/kernel.elf
make -C kernel qemu           # QEMU 运行并输出串口(含 -bios default)

# 评测
bash grading/grade.sh -l 107 -v     # 单课,显示串口完整输出与匹配行
bash grading/grade.sh -l 97,98,99   # 多课

# 调试(GDB 远程)
make -C kernel qemu-gdb            # 加 -s -S,另起 gdb 连 localhost:1234

工具链与 QEMU:

项值
交叉编译器riscv64-linux-musl-gcc
QEMUqemu-system-riscv64 -machine virt -m 128M -nographic -bios default
加载地址0x80200000
编译选项-ffreestanding -nostdlib -nostdinc(无标准库/头)

7. 课前自测(Checklist) ​

开始写代码前,请确认你能在脑中/纸上回答:

  1. _start 为什么在 MMU 关闭时要用 lla 而不是 la 取 boot_stack_top?
  2. 双 VMA/LMA 设计里,KERNEL_VMA_OFF=0xFFFFFFFF00000000 解决了什么问题?
  3. trap_vector 为什么必须 .align 4?
  4. sscratch 为 0 和不为 0 分别意味着什么?trap.S 开头那段 csrrw sp,sscratch,sp 在做什么?
  5. switch.S 只保存 ra + s0–s11,为什么不保存 a0–a7/t0–t6?
  6. 用户态 ecall 后为什么 sepc += 4?ebreak 当 yield 用时要靠哪个寄存器区分?
  7. 开 STIE(bit5) 用 csrsi sie, 0x20 会怎样?正确写法是什么?
  8. satp 的 MODE 位写 9 代表什么?写 0 呢?
  9. 定时器为什么必须用 legacy SBI set_timer(EID=0) 而非 v0.2 TIME 扩展?
  10. SMP 次级核醒来后,为什么可以直接复用 BSP 已经建好的 satp(页表)?

8. 参考资料(外部链接) ​

权威规范与实用资料,课前/调试时按需查阅:

8.1 官方规范(第一手,最准确) ​

8.2 ABI / 调用约定 ​

8.3 平台 / 固件 / 设备 ​

提示:课程每课的 README.md 「参考资料」小节会给出与该课最相关的手册章节与源码文件,建议配合本速查手册一起使用。


文档随课程演进维护;寄存器/指令细节以 RISC-V 官方手册与本仓库 kernel/ 实现为准。

Released under the MIT License.