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
2
3
4
5
6
7
8
9
10
11
12
13
14
// proc.h
struct trapframe {
/* 0 */ uint64 kernel_satp; // 内核页表
/* 8 */ uint64 kernel_sp; // 内核栈指针
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // 用户程序异常发生时的PC
/* 32 */ uint64 kernel_hartid; // 当前硬件线程id
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
...
/* 280 */ uint64 t6;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# trampoline.S
uservec:
# sscratch的值指向trapframe的地址
# 下面的代码将a0和sscratch交换,所以现在a0指向trapframe的地址
csrrw a0, sscratch, a0

# 接下来将用户寄存器保存到trapframe中
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)

# 将a0原来的值也保存到trapframe中
csrr t0, sscratch
sd t0, 112(a0)

# 恢复内核栈指针
ld sp, 8(a0)

# 获取当前硬件线程id
ld tp, 32(a0)

# 加载usertrap()的地址
ld t0, 16(a0)

# 切换内核页表。由于trampoline在所有页表的位置都相同,所以切换页表后能够正常继续执行
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero

# 跳转到usertrap()
jr t0

usertrap 会判断异常的原因,并做出相应的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// trap.c
void
usertrap(void)
{
int which_dev = 0;

// 判断异常是否来自用户模式,如果不是则说明出错了
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");

// 因为现在位于内核空间,所以将异常处理程序设置kernelvec,以便在内核空间出现异常时进行处理
w_stvec((uint64)kernelvec);

struct proc *p = myproc();

// 保存用户程序PC
p->trapframe->epc = r_sepc();

if(r_scause() == 8){
// 异常原因是系统调用的情况

if(p->killed)
exit(-1);

// 将用户程序的PC指向下一条指令,因为后面要返回到那里继续执行
p->trapframe->epc += 4;

// 启用中断
intr_on();

// 执行系统调用
syscall();
} else if((which_dev = devintr()) != 0){
// 用devintr()处理设备中断
} else {
// 其他异常情况,直接退出
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}

if(p->killed)
exit(-1);

// 如果是时钟中断,则让出CPU
if(which_dev == 2)
yield();

usertrapret();
}

usertrap 处理完异常后,会调用 usertrapret 函数,将用户程序的寄存器值恢复,并跳转回用户程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void
usertrapret(void)
{
struct proc *p = myproc();

// 关闭中断,避免在恢复时受到干扰
intr_off();

// 将uservec的地址写入stvec,以便下次用户程序发生异常时调用uservec
w_stvec(TRAMPOLINE + (uservec - trampoline));

// 保存从用户空间进入内核空间时所需的寄存器值
p->trapframe->kernel_satp = r_satp(); // 内核页表
p->trapframe->kernel_sp = p->kstack + PGSIZE; // 内核栈大小为一页,生长方向向下,所以加上PGSIZE
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // 硬件线程id

unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // 将SPP置零,恢复用户模式
x |= SSTATUS_SPIE; // 恢复中断使能状态
w_sstatus(x);

// 恢复PC
w_sepc(p->trapframe->epc);

// 用户根页表地址
uint64 satp = MAKE_SATP(p->pagetable);

// trampoline.S中userret的地址,并进行调用
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# trampoline.S

userret:
# userret(TRAPFRAME, pagetable)
# 恢复寄存器值,返回用户空间
# a0: TRAPFRAME的地址
# a1: 用户根页表地址

# 恢复用户页表。由于trampoline在所有页表的位置都相同,所以切换页表后能够正常继续执行
csrw satp, a1
sfence.vma zero, zero

# 读出原来a0的值存到sscratch
ld t0, 112(a0)
csrw sscratch, t0

# 从trapframe中恢复其他寄存器
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)

# 交换a0和sscratch,现在a0恢复了原来的值,sscratch指向trapframe,方便下次异常处理
csrrw a0, sscratch, a0

# specusertrapret()中设置了sstatus和sepc的值
# 现在调用sret回到用户空间
sret

内核空间中发生的异常

内核空间中发生的异常直接在内核空间进行处理。

kernelvec 会直接将寄存器的值保存到内核栈上。如果将寄存器值保存到内存中,由于在异常处理中可能会切换线程,这样就有可能导致寄存器的值被覆盖。

随后跳转到 kerneltrap 函数进行异常处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void 
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();

if((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if(intr_get() != 0)
panic("kerneltrap: interrupts enabled");

if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}

// 如果是时钟中断,则让出CPU
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();

// yield()可能导致一些陷阱,改变寄存器的值,所以需要重新设置
w_sepc(sepc);
w_sstatus(sstatus);
}

kerneltrap 执行完后返回到 kernelvec,将寄存器的值恢复,回到异常发生时的地方继续执行。

系统调用

user.h 在用户空间中声明了各种系统调用,其实现在 usys.S 中,即将系统调用编号存入 a7 寄存器,然后调用 ecall 指令。

1
2
3
4
5
6
7
8
9
10
11
12
# usys.S
.global fork
fork:
li a7, SYS_fork
ecall
ret
.global exit
exit:
li a7, SYS_exit
ecall
ret
...

调用 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
2
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

A: 输出 "HE110 World",如果是大字节序应将 i 改为 0x00726c64

1
2
3
4
5
72 6c 64
r l d

64 6c 72
d l r

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
printf("x=%d y=%d", 3);

A: y 的值是 a2 寄存器中的值

Backtrace (moderate)

本题要求实现一个函数 backtrace,用于打印函数调用栈中每个返回地址。

思路

首先明确栈帧的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
高地址  +------------------+
| 调用者栈帧 |
+------------------+
| ... |
+------------------+ <- 当前fp(s0)
fp-8 | 返回地址(ra) |
+------------------+
fp-16 | 保存的上一个fp |
+------------------+
| 保存的其他寄存器 |
+------------------+
| 局部变量 |
+------------------+
低地址 | ... | <- 当前sp
+------------------+

帧指针 fp 指向当前栈帧的顶部,-8(fp) 的位置存放着当前函数的返回地址 ra-16(fp) 的位置存放着上一个栈帧的帧指针 fp。所以可以通过保存的栈指针不断回溯,终止条件是 fp 越过整个栈的底部。具体逻辑用伪代码表示如下:

1
2
3
4
5
fp = r_fp()
while(fp < kstackbottom)
ra = -8(fp)
print(ra)
fp = -16(fp)

实现代码

首先在 kernel/riscv.h 中添加获取帧指针的函数 r_fp。此处用到了内联汇编,其含义是将 s0 寄存器的值存入 x 中。指令模板中的 %0 占位符表示操作数列表中的第一个操作数,这里为 x= 为写入操作符,表示这是一个输出操作数。r 表示操作数分配到一个寄存器上进行操作。

1
2
3
4
5
6
7
8
// kernel/riscv.h
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

kernel/printf.c 中按照上述思路实现 backtrace 函数。xv6 中栈只有一页且生长方向向下,所以可以通过 PGROUNDUP 宏可以获取栈底地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// kernel/printf.c

void
backtrace(void)
{
uint64 fp, ra = 0;
fp = r_fp();

uint64 kstackbottom;
kstackbottom = PGROUNDUP(fp);

printf("backtrace:\n");

while(fp < kstackbottom )
{
asm volatile("ld %0, -8(%1)" : "=r" (ra) : "r"(fp));
printf("%p\n", ra);
asm volatile("ld %0, -16(%1)" : "=r" (fp) : "r"(fp));
}
}

kernel/defs.h 中声明 backtrace 函数,然后在 sys_sleep 中调用 backtrace 函数。

1
2
// kernel/defs.h
void backtrace(void);
1
2
3
4
5
6
7
8
9
// kernel/sysproc.c
uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace();
...
}

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
2
3
UPROGS=\
$U/_alarmtest\
...

kernel/proc.h 中添加定时器相关字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// kernel/proc.h
struct proc {
...
int interval; // 定时器间隔
int ticks; // 上次调用handler后经过的ticks
void (*handler)(); // 定时器处理函数
};

添加了新字段后,自然需要初始化。

```c
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
found:
p->pid = allocpid();

p->interval = 0;
p->ticks = 0;
p->handler = 0;
...
}

参考资料

《RISC-V 开放架构设计之道》


6.S081 学习笔记 - Lab-Traps
http://blog.qzink.me/posts/6.S081学习笔记-Lab-Traps/
作者
Qzink
发布于
2025年3月10日
许可协议