跳转到内容

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_tsize_tvaddr_t/paddr_tNULLALIGN_UP;无标准库
99Kernel Logginginclude/klog.hlib/klog.clib/vsnprintf.cKLOG_* 宏 → kprintf → UART
100Linker Scriptboot/riscv64/link.ld:双 VMA/LMA(KERNEL_LOAD_BASE=0x80200000KERNEL_VMA_OFF
101Boot Assemblyboot/riscv64/boot.S_start 关中断、设引导栈、建页表、开 MMU、trampoline 到高半区
102Arch Abstractioninclude/arch.h__riscv + __riscv_xlen==64ARCH_RISCV64)、include/barrier.h/riscv64/barrier_impl.h

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

标题RISC-V 64 关键文件 / 知识点
103Memory Barriersriscv64/barrier_impl.hfence rw,rw / fence r,r / fence w,w
104Spinlocksriscv64/spinlock_impl.hLR/SClr.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_satppte_table/pte_leaf
107MMU Enablemm/riscv64/mmu.Scsrw satp + sfence.vmamm/riscv64/vm_early.c:2 级页表(恒等 + 高半区)
108Kernel Address Spaceinclude/mm_vm.hvirt_to_phys/phys_to_virt 直接映射;main.c 反推 kernel_main 的 PA/VA

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

标题RISC-V 64 关键文件 / 知识点
109Exception Vectorsboot/riscv64/trap.Strap_vector.align 4,direct 模式写入 stvec=0x105)
110Exception Handlingboot/riscv64/exception.chandle_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),sieSEIE(bit9)

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

标题RISC-V 64 关键文件 / 知识点
113Task Control Blockkernel/task/task.htask.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.Sarch_user_entersret 进 U-mode;sscratch 区分陷入来源
118System Callsboot/riscv64/exception.ccause==8(U-mode ecall)→ syscall_dispatcha7=调用号,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 位通用寄存器 x0x31,约定(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–x17a2a7函数参数 3–8调用者保存
x18–x27s2s11被保存寄存器被调用者保存
x28–x31t3t6临时寄存器调用者保存

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

  • 参数/返回值:C 函数前 8 个参数走 a0a7(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 关中断,避免无向量表时收中断崩溃。
  • 切换到内核态或用户态的"开关"靠 sstatusSPP 位 + 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中断挂起位(清 SSIPcsrci 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)中 stvecsatpsiesipsepcscausestvalsscratch助记符可直接用,无需写裸地址。

4.3 特权指令

指令含义本内核出现处
ecall触发环境调用(陷入 S/M,取决于当前特权级)用户态 syscall;SBI 调用(sbi_set_timer
sret从 S-mode 陷阱返回(PC←sepc,特权级←sstatus.SPPtrap.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, t0csrw 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.Scsrrw 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(PLICplic_external_irq
3异常Breakpoint(ebreaka7==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-savedra, 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, zerosfence.vma(省略操作数依赖汇编器)
stvec direct 模式.align 4csrw 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 而不是 laboot_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 += 4ebreakyield 用时要靠哪个寄存器区分?
  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.