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 开放架构设计之道》