6.S081 学习笔记 - Lab-Traps
RISC-V 知识点
RISC-V 寄存器
从表中可以看到,保存寄存器和临时寄存器的编号不是连续的。这是为了支持另一个只有 16 个寄存器的 RISC-V 变种 RV32E。
保存寄存器和栈指针在函数调用前后保持不变,它们的值由被调用者保存和恢复。
临时寄存器、函数参数和返回值在函数调用前后可能会被修改,它们的值由调用者保存和恢复。
关于调用前后是否一致可以这样理解:保存寄存器一般是存一些重要的值,而临时寄存器是存一些不重要的中间结果或临时值。所以被调用者 (Callee) 如果要利用保存寄存器的空间就需要负责在使用后恢复原样,而对临时寄存器无需负责 (因为它认为临时寄存器中的值不重要)。因此如果调用者 (Caller) 在临时寄存器中保存了在函数调用后还需要的值,它就需要自己负责保存,避免被被调用者修改。
32 位和 64 位 RISC-V 的寄存器数量相同,只是寄存器的位宽不同。
RV32I 指令集
指令分类
- R 型:用于寄存器之间的操作
- I 型:用于短立即数和寄存器之间的操作
- S 型:用于存储操作
- B 型:用于条件分支操作
- U 型:用于长立即数的操作
- J 型:用于无条件跳转操作
指令列表
整数计算指令
| 指令名称 | 指令格式 | 描述 |
|---|---|---|
| add | add rd, rs1, rs2 |
将 x[rs2] 和 x[rs1] 相加,结果存入 x[rd] |
| addi | addi rd, rs1, imm |
对 imm 符号扩展后与 x[rs1] 相加,结果存入 x[rd] |
| sub | sub rd, rs1, rs2 |
将 x[rs1] 减去 x[rs2],结果存入 x[rd] |
| and | and rd, rs1, rs2 |
将 x[rs1] 和 x[rs2] 按位与,结果存入 x[rd] |
| andi | andi rd, rs1, imm |
对 imm 符号扩展后和 x[rs1] 按位与,结果存入 x[rd] |
| or | or rd, rs1, rs2 |
将 x[rs1] 和 x[rs2] 按位或,结果存入 x[rd] |
| ori | ori rd, rs1, imm |
对 imm 符号扩展后和 x[rs1] 按位或,结果存入 x[rd] |
| xor | xor rd, rs1, rs2 |
将 x[rs1] 和 x[rs2] 按位异或,结果存入 x[rd] |
| xori | xori rd, rs1, imm |
对 imm 符号扩展后和 x[rs1] 按位异或,结果存入 x[rd] |
| sll | sll rd, rs1, rs2 |
将 x[rs1] 逻辑左移 x[rs2] 位,结果存入 x[rd]。x[rs2] 的低 5 位是移位位数,高位忽略 |
| slli | slli rd, rs1, shamt |
将 x[rs1] 逻辑左移 shamt 位,结果存入 x[rd]。仅当 shamt[5]=0 时指令合法 |
| srl | srl rd, rs1, rs2 |
将 x[rs1] 逻辑右移 x[rs2] 位,结果存入 x[rd]。x[rs2] 的低 5 位是移位位数,高位忽略 |
| srli | srli rd, rs1, shamt |
将 x[rs1] 逻辑右移 shamt 位,结果存入 x[rd]。仅当 shamt[5]=0 时指令合法 |
| sra | sra rd, rs1, rs2 |
将 x[rs1] 算术右移 x[rs2] 位,结果存入 x[rd]。x[rs2] 的低 5 位是移位位数,高位忽略 |
| srai | srai rd, rs1, shamt |
将 x[rs1] 算术右移 shamt 位,结果存入 x[rd]。仅当 shamt[5]=0 时指令合法 |
| lui | lui rd, imm |
将 20 位 imm 符号扩展后左移 12 位,低 12 位置 0,结果存入 x[rd] |
| auipc | auipc rd, imm |
将 20 位 imm 符号扩展后左移 12 位,加上 pc,结果存入 x[rd] |
| slt | slt rd, rs1, rs2 |
如果 x[rs1] 小于 x[rs2],则 x[rd]=1,否则 x[rd]=0 |
| slti | slti rd, rs1, imm |
如果 x[rs1] 小于符号扩展后的 imm,则 x[rd]=1,否则 x[rd]=0 |
| sltiu | sltiu rd, rs1, imm |
如果 x[rs1] 小于无符号扩展后的 imm(视为无符号数),则 x[rd]=1,否则 x[rd]=0 |
控制转移指令
| 指令名称 | 指令格式 | 描述 |
|---|---|---|
| beq | beq rs1, rs2, offset |
如果 x[rs1] 等于 x[rs2],则将 pc 设为当前值加上符号扩展后的 offset |
| bne | bne rs1, rs2, offset |
如果 x[rs1] 不等于 x[rs2],则将 pc 设为当前值加上符号扩展后的 offset |
| bge | bge rs1, rs2, offset |
如果 x[rs1] 大于等于 x[rs2],则将 pc 设为当前值加上符号扩展后的 offset |
| bgeu | bgeu rs1, rs2, offset |
如果 x[rs1] 大于等于 x[rs2](视为无符号数),则将 pc 设为当前值加上符号扩展后的 offset |
| blt | blt rs1, rs2, offset |
如果 x[rs1] 小于 x[rs2],则将 pc 设为当前值加上符号扩展后的 offset |
| bltu | bltu rs1, rs2, offset |
如果 x[rs1] 小于 x[rs2](视为无符号数),则将 pc 设为当前值加上符号扩展后的 offset |
| jal | jal rd, offset |
将下一条指令的地址 (pc+4) 写入 x[rd],然后将 pc 设为当前值加上符号扩展后的 offset。若省略 rd,则默认为 x1 |
| jalr | jalr rd, rs1, imm |
将 pc 设为 x[rs1]+sign-extend(offset),将跳转地址的最低位清零,并将原 pc+4 写入 x[rd]。若省略 rd,则默认为 x1 |
装载存储指令
| 指令名称 | 指令格式 | 描述 |
|---|---|---|
| lb | lb rd, offset(rs1) |
从 x[rs1]+sign-extend(offset) 读取一个字节,符号扩展后存入 x[rd] |
| lbu | lbu rd, offset(rs1) |
从 x[rs1]+sign-extend(offset) 读取一个字节,零扩展后存入 x[rd] |
| lh | lh rd, offset(rs1) |
从 x[rs1]+sign-extend(offset) 读取两个字节,符号扩展后存入 x[rd] |
| lhu | lhu rd, offset(rs1) |
从 x[rs1]+sign-extend(offset) 读取两个字节,零扩展后存入 x[rd] |
| lw | lw rd, offset(rs1) |
从 x[rs1]+sign-extend(offset) 读取四个字节,符号扩展后存入 x[rd] |
| sb | sb rs2, offset(rs1) |
将 x[rs2] 的最低字节存入内存地址 x[rs1]+sign-extend(offset) |
| sh | sh rs2, offset(rs1) |
将 x[rs2] 的最低 2 字节存入内存地址 x[rs1]+sign-extend(offset) |
| sw | sw rs2, offset(rs1) |
将 x[rs2] 的最低 4 字节存入内存地址 x[rs1]+sign-extend(offset) |
其他指令
| 指令名称 | 指令格式 | 描述 |
|---|---|---|
| fence | fence pred, succ |
内存屏障,保证内存操作的顺序性 |
| fence.i | fence.i |
指令屏障,保证指令的顺序性 |
| ebreak | ebreak |
通过抛出断点异常调用调试器 |
| ecall | ecall |
通过抛出环境调用异常调用执行环境 |
| csrrc | csrrc rd, csr, rs1 |
记控制状态寄存器 csr 的值为 t。将 x[rs1] 的反码和 t 按位与,结果写入 csr,再将 t 写入 x[rd] |
| csrrci | csrrci rd, csr, zimm[4:0] |
记控制状态寄存器 csr 的值为 t。将 5 位立即数 zimm 零扩展后的反码和 t 按位与,结果写入 csr,再将 t 写入 x[rd] |
| csrrs | csrrs rd, csr, rs1 |
记控制状态寄存器 csr 的值为 t。将 x[rs1] 的值和 t 按位或,结果写入 csr,再将 t 写入 x[rd] |
| csrrsi | csrrsi rd, csr, zimm[4:0] |
记控制状态寄存器 csr 的值为 t。将 5 位立即数 zimm 零扩展后的值和 t 按位或,结果写入 csr,再将 t 写入 x[rd] |
| csrrw | csrrw rd, csr, rs1 |
记控制状态寄存器 csr 的值为 t。将 x[rs1] 的值写入 csr,再将 t 写入 x[rd] |
| csrrwi | csrrwi rd, csr, zimm[4:0] |
将控制状态寄存器的值复制到 x[rd],再将 5 位立即数 zimm 零扩展后的值写入 csr |
RV64I 指令集
在 RV32I 的基础上,RV64I 增加了图中红色的指令,主要是对字长的扩展。
RV32/RV64 特权架构
特权模式下的异常处理
重要寄存器:
sstatus(Supervisor Status):维护各种状态,其中SIE位控制设置中断使能,SPP位存储异常发生的特权模式 (0=U, 1=S)sip(Supervisor Interrupt Pending):记录当前的中断请求sie(Supervisor Interrupt Enable):维护处理器的中断使能状态scause(Supervisor Exception Cause):指示发生了何种异常stvec(Supervisor Trap Vector):指向异常处理程序的入口地址stval(Supervisor Trap Value):存放当前自陷的额外信息sepc(Supervisor Exception Program Counter):指向发生异常的指令地址sscratch(Supervisor Scratch):向异常处理程序提供一个字的临时存储
处理流程:
- 将发生异常的指令 PC 存入
sepc, 并将 PC 设为stvec - 将异常原因写入
scause,并将故障地址或其他异常相关信息字写入stval - 将
sstatus.SIE置零以屏蔽中断,并将SIE的旧值存放在SPIE中 - 将异常发生前的特权模式存放在
sstatus.SPP,并将当前特权模式设为S sret指令将spec的值复制到 PC
RISC-V 汇编器指示符
xv6 中的异常处理
用户空间中发生的异常
当用户模式下的程序发生异常(如系统调用、中断等)时,处理器记录发生异常的指令,并将当前 PC 设置为 uservec。在上一节的页表实验中,我们知道每个页表的顶端都有一个内容相同的 trampoline 页面。trampoline 页面保存了异常处理的汇编代码,uservec 就是其中一段代码的入口地址。
由于运行异常处理程序要使用寄存器,为避免覆盖掉用户程序的寄存器值,我们需要先将用户程序的寄存器值保存起来,以便在处理完异常后恢复。寄存器保存的位置是在 trampoline 页面下方的 trapframe 页面,其结构在 proc.h 中进行定义。uservec 的主要功能是完成这个保存操作,做好在内核空间执行程序的准备,再跳转到异常处理代码。
1 | |
1 | |
usertrap 会判断异常的原因,并做出相应的处理。
1 | |
usertrap 处理完异常后,会调用 usertrapret 函数,将用户程序的寄存器值恢复,并跳转回用户程序。
1 | |
1 | |
内核空间中发生的异常
内核空间中发生的异常直接在内核空间进行处理。
kernelvec 会直接将寄存器的值保存到内核栈上。如果将寄存器值保存到内存中,由于在异常处理中可能会切换线程,这样就有可能导致寄存器的值被覆盖。
随后跳转到 kerneltrap 函数进行异常处理。
1 | |
kerneltrap 执行完后返回到 kernelvec,将寄存器的值恢复,回到异常发生时的地方继续执行。
系统调用
user.h 在用户空间中声明了各种系统调用,其实现在 usys.S 中,即将系统调用编号存入 a7 寄存器,然后调用 ecall 指令。
1 | |
调用 ecall 后进行上述一系列异常处理的过程,并在 usertrap 中调用 syscall 函数。
然后根据 a7 的寄存器中的系统调用号调用相应的系统调用函数。
题解
RISC-V assembly (easy)
本题是对 RISC-V 汇编的复习。
Q: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
A: a0 存参数,a2 保存了 13
Q: Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
A: 在 0x26 处编译器直接计算出了 f (8)+1,进行了内联优化,所以实际并未调用 f 和 g 函数。
Q: At what address is the function printf located?
A: 0x640
Q: What value is in the register ra just after the jalr to printf in main?
A: ra = 0x38,因为 0x34 处的 jalr 1552 (ra) 指令将 PC+4 的值写入了 ra
Q: Run the following code. What is the output? The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
1 | |
A: 输出 "HE110 World",如果是大字节序应将 i 改为 0x00726c64
1 | |
Q: In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
1 | |
A: y 的值是 a2 寄存器中的值
Backtrace (moderate)
本题要求实现一个函数 backtrace,用于打印函数调用栈中每个返回地址。
思路
首先明确栈帧的结构:
1 | |
帧指针 fp 指向当前栈帧的顶部,-8(fp) 的位置存放着当前函数的返回地址 ra,-16(fp) 的位置存放着上一个栈帧的帧指针 fp。所以可以通过保存的栈指针不断回溯,终止条件是 fp 越过整个栈的底部。具体逻辑用伪代码表示如下:
1 | |
实现代码
首先在 kernel/riscv.h 中添加获取帧指针的函数 r_fp。此处用到了内联汇编,其含义是将 s0 寄存器的值存入 x 中。指令模板中的 %0 占位符表示操作数列表中的第一个操作数,这里为 x。= 为写入操作符,表示这是一个输出操作数。r 表示操作数分配到一个寄存器上进行操作。
1 | |
在 kernel/printf.c 中按照上述思路实现 backtrace 函数。xv6 中栈只有一页且生长方向向下,所以可以通过 PGROUNDUP 宏可以获取栈底地址。
1 | |
在 kernel/defs.h 中声明 backtrace 函数,然后在 sys_sleep 中调用 backtrace 函数。
1 | |
1 | |
Alarm (hard)
本题要求实现一组系统调用,实现定时器功能。
具体来说 sigalarm(interval, handler) 用于设置一个定时器,每隔 interval 个 ticks 就暂停当前函数去调用 handler 函数,当 handler 函数返回后从暂停的地方继续执行。sigalarm(0, 0) 用于取消定时器。
sigreturn 用于从处理函数 handler 中返回到暂停的地方继续执行。
思路:实现 sigalarm
- 需要在
proc结构体中添加定时间隔、距上一次调用经过的 ticks 计数和处理函数的字段。 - 调用
sigalarm时,将这些字段设置为相应的值。 - 每个 CPU 定时器中断,增加当前 ticks 计数,检查是否有定时器到期。若到期则重置 ticks 计数,调用处理函数。
- 添加系统调用按照第二次 Lab 中总结的流程进行。
实现代码
在 Makefile 中添加 alarmtest 的编译规则。
1 | |
在 kernel/proc.h 中添加定时器相关字段。
1 | |
参考资料
《RISC-V 开放架构设计之道》