Horbynz hub

⌈xv6-fall2021⌋ lab 4:Traps

Word count: 6.3kReading time: 27 min
2022/03/29

LAB

传送门:Lab: traps


RISC-V assembly (EASY)

要求

There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.

“在 xv6 仓库里有一个 user/call.c 文件,运行 make fs.img 编译后会生成 user/call.asm,这是一个更加可读的汇编语言版本”

Read the code in call.asm for the functions g, f, and main.

“在 call.asm 中阅读 g()、f() 和 main() 的函数代码”

Here are some questions that you should answer (store the answers in a file answers-traps.txt)

“回答这里的一些问题(将答案保存在 answers-traps.txt 里面)”

这道题不是编程题,所以直接给答案就行。但是另一方面,我对 riscv 不是很感兴趣(起码目前是这样),我只是想学 operating system principles 而已,而且我手边还有一个 x86 arch kernel 在烂尾。所以在此之前,我只会根据 x86 assembly 的知识大概分享下 user/call.asm 的含义,仅供参考


Summary

真正开始之前先分享几个参考资料:

  • 第一个是 RISC-V ISA,遇到什么指令不懂就查它,需要注意的是 xv6 实验架构是 RV32I/RV64I
  • 第二个是 调用约定,这是上一个 lab 的 preparation,此处是我自己翻译的版本,当然,原文会引用上

现在我们开始吧

首先打开 user/call.asm 找到 g(),我会把简化版拷贝到这里(即不关注机器指令),大概如下:

1
2
3
4
5
6
7
8
9
10
11
# g()
int g(int x) { # 1
0: addi sp, sp, -16 # 2
2: sd s0, 8(sp) # 3
4: addi s0, sp, 16 # 4
return x + 3; # 5
} # 6
6: addiw a0, a0, 3 # 7
8: ld s0, 8(sp) # 8
a: addi sp, sp, 16 # 9
c: ret # 10

忽略第 1、5、6 行的 C 代码,只关注汇编代码

假设现在刚进入 g(),那么 g() 的栈帧(stack frame)大概如下:

g() 的栈底保存着返回地址,假设是 main() 调用了 g() 那么返回地址就是 main() 里的某一句代码。再往上是函数参数,这里 g() 只有一个参数,所以只占用一个栈元素。如果,g() 是 g(int x, int y) 这么定义的,那么栈帧就会像下面这个样子:

我想再强调一次,这里只是 x86 汇编角度来粗略了解下 call.asm 的函数思路。如果想真真正正学习 risc-v 汇编的那还是踏踏实实学题干给出的 MIT 6.004 这门课吧

现在来到第二行代码,addi sp, sp, -16,分配栈空间

按照调用约定,每个 callee 负责自己的栈分配与清理,因为你要用什么寄存器,用多少只有你自己清楚。如果还是用上面那个 main() 调用 g() 来举例子,caller 是 main(),callee 是 g()

可以发现 g() 将 sp 往下移动了两个栈元素(由于我是 64 位机器,一个 pointer-word 是 8 字节),虽然我也不清楚编译器让 g() 到底用了什么寄存器,但其实我们不用关心,只需知道现在栈分配了两个元素,栈帧看起来像这个样子

现在来到第三行代码,sd s0, 8(sp),保护 s0 寄存器,栈帧如下

第四行代码,addi s0, sp, 16 取出形参 x,存放于 s0,如下

现在来到第七行代码,addiw a0, a0, 3a0 = a0 + 3,考虑到 risc-v 以 a0、a1 作为返回值,并且 g() 返回 x + 3,所以大致猜出这行代码是设置返回值的

第八行代码 ld s0, 8(sp) 恢复 s0

第九行代码 addi sp, sp, 16 清理栈,这使得调用栈又变回原来的样子

再来看第二个函数,f(),其实都是大同小异,就不详细列出了,仅添加注释

1
2
3
4
5
6
7
8
9
10
11
# f()
int f(int x) {
e: addi sp, sp, -16 # 设置调用栈
10: sd s0, 8(sp) # 保护 s0
12: addi s0, sp, 16 # 取形参:s0 = x
return g(x);
}
14: addiw a0, a0, 3 # 返回值 a0 = a0 + 3
16: ld s0, 8(sp) # 恢复 s0
18: addi sp, sp, 16 # 清理栈
1a: ret

值得一提的是,在编译地址 14 处编译器作出了优化,这里是调用 g(),理论上应该有设置参数,入栈的代码的,但这里编译器将 g() 展开成 inline function 了,所以直接是套用上面 g() 那句返回值的代码

最后来到 main(),和 f() 一样我这里只摆出注释了

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
void main(void) {
1c: addi sp,sp,-16
1e: sd ra,8(sp)
20: sd s0,0(sp)
22: addi s0,sp,16
# 调用 printf(),共三个参数,第一个参数传入
# a0,第二个参数 a1,第三个参数 a2
# 而第一个参数是字符串,根据调用约定:
# 超过两个指针字的参数要传入引用,此处即指针
# 这里编译器给的指针地址是 0x7e8,结合 call.asm
# 开头给出是 .text 段,结尾最后一个地址是 0x7e0
# 大概可以猜出代码段逻辑上是紧邻这个文件后面的,
# 并且 "%d %d\n" 这个字符串就在代码段开头
printf("%d %d\n", f(8)+1, 13);
24: li a2,13
26: li a1,12
28: auipc a0,0x0
2c: addi a0,a0,1984 # 7e8 <malloc+0xea>
30: auipc ra,0x0
34: jalr 1552(ra) # 640 <printf>
# 调用 exit(),共一个参数,传入 a0
exit(0);
38: li a0,0
3a: auipc ra,0x0
3e: jalr 638(ra) # 2b8 <exit>

Solution

现在让我们重新回到题目中,不难得出:

注:第一问参考我上面给出的我翻译的 《调用约定》一文;第三问的编译地址取决于你自己的 asm 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Eng
1. a0-a7; a2
2. at the beginning of main(), but it is optimized to inline function; no g()
3. 0x640
4. 0x38
5. "HE110 World"; variable i need to change to 0x726C6400; i will change 57616 to 0x10e1
6. y is unknown; because y is passed with a2 while a2 not set, as a result, what variable y to use is a unknown value of a2

// Chn
1. a0-a7; a2
2. main() 开头处,但已被优化成 inline 了; 没有 g()
3. 0x640
4. 0x38
5. "HE110 World"; 变量 i 需要变成 0x726C6400; 需要将 57616 变成 0x10e1
6. y 是未知值; 因为 a2 没被设置,却会传递给 y 一个值,最后变量 y 使用了 a2 里面保存的未知值

Backtrace (MODERATE)

For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.

“能过追踪回来的痕迹对于调试来说是非常有用的,这个追踪痕迹说的是栈帧上面的系统调用列表,而这个列表又是错误发生前调用的所有函数的集合”

Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep, and then run bttest, which calls sys_sleep. Your output should be as follows:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

After bttest exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the above addresses as follows:
    $ addr2line -e kernel/kernel
    0x0000000080002de2
    0x0000000080002f4a
    0x0000000080002bfc
    Ctrl-D

You should see something like this:
    kernel/sysproc.c:74
    kernel/syscall.c:224
    kernel/trap.c:85

"在 kernel/printf.c 实现一个 backtrace(),然后在 sys_sleep() 插入一个对该函数的调用,最后调用 bttest,由它调用 sys_sleep()。你的输出会像上面第一个 shell 输出那样

在执行完 bttest 之后,如果你运行 addr2line -e kernle/kernelriscv64-unknown-elf-addr2line -e kernel/kernel,你的 terminal 上显示的地址信息可以稍微有点不同。但不用担心,只要你复制粘贴像上面第二个 shell 输出这样的地址

你应该可以看到一些东西像上面第三个 shell 输出那样(才算对)"


提示

The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.

“编译器为每个栈帧存放一个保存 caller 的 frame pointer 地址。你的 backtrace() 应该要使用这些 frame pointer 以呈现出追踪痕迹,然后打印每个栈帧里保存的返回地址” ———— 注:栈里面 frame pointer 是编译器负责的,而我们的任务只是打印栈帧里的 “saved return address” 而已

These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

“这些 课程笔记 有一些关于栈帧结构的图片。注意栈帧的返回地址位于 frame pointer 固定的 -8 偏移处;而 saved frame pointer 位于 frame pointer 固定的 -16 偏移处”

我想先上我对 “课程笔记” 的理解,然后再具体说清楚这个提示究竟是什么意思

如图所示是我对 “课程笔记” 栈帧的理解

栈底是返回地址 “Return Addr”,再往前是前一个函数调用相关地址 “To Prev. Frame”(本题即 saved frame pointer 的含义),再往前的寄存器 “Saved Regis” 其实是函数参数,最后的本地变量 “Local variables” 就是只有在当前函数有效的变量

这里有三个概念,分别是 “返回地址”、“frame pointer”(下文出现的 fp 即是它)和 “saved frame pointer”

  • 第一个 “返回地址” 其实我也不知道是什么(有可能是字面意思?),但这就是题目要求打印的东西,固定在相对 fp 的 -8 偏移处
  • 第二个 “frame pointer” 其实是栈底,对于 x86 arch 即 bp/ebp/rbp 的指向
  • 第三个 “saved frame pointer” 其实是上一个系统调用的 fp 的地址,即通过这个 saved fp 可以找到前一个函数

如果你还是对 frame pointer 感到疑惑我这边也推荐几个资料:

最后总结下上面的提示究竟想表达什么意思?答:通过 fp - 8 找到要打印的内容;通过 fp - 16 找到上一个函数调用

Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h). These number are helpful for backtrace to terminate its loop.

“xv6 为每个栈在页对齐的地址上分配一页,你可以通过 PGROUNDDOWN(fp) 和 PGROUNDUP(fp)(详见 kernel/riscv.h)计算栈顶和栈底地址。这些数值对于终止 backtrace() 的循环非常有帮助” ———— PGROUNDDOWN() 相当于向下取整(比如 4.2、4.8 都取整为 4)也即是栈顶;PGROUNDUP() 相当于向上取整(比如 4.2、4.8 取整为 5)也即是栈底

Once your backtrace is working, call it from panic in kernel/printf.c so that you see the kernel’s backtrace when it panics.

“一旦你完成 backtrace(),你可以在 kernel/printf.c 的 panic() 中调用,这样你就可以在内核遭遇 panic 级别错误时看到它的函数调用痕迹”

Summary

现在整理下思路吧:

  • 首先牢记核心任务,就是从当前调用开始,依次打印返回地址,直至追踪到第一个函数
  • 怎么往前追踪上一个函数?就依赖 fp - 16 这个偏移了
  • 迭代边界怎么处理?因为一个栈最大最大只有一页,而且还是页对齐的,所以每取得一个 fp 地址后,就检查它是不是在 PGROUNDDOWN() 和 PGROUNDUP() 区间内(其实只需判比 PGROUNDUP() 小就可以了,因为往回找其实是一个栈地址不断增加的过程)

理论基础是栈帧结构


Solution

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
// path: kernel/defs.h
// printf.c 注释处
void backtrace(void);

// path: kernel/sysproc.c
uint64
sys_sleep(void) {
...
sleep(&ticks, &tickslock);
}
backtrace();// +
release(&tickslock);
...
}

// path: kernel/printf.c
void
panic(char *s) {
...
backtrace(); // +
panicked = 1; // freeze uart output from other CPUs
...
}

void
backtrace(void) {
printf("backtrace:\n");
// 当前函数 frame pointer 的地址
uint64 s0 = r_fp();

// 遍历栈帧
uint64 end = PGROUNDUP(s0);
uint64 framep = s0;
for (;framep < end; ) {
// 取偏移 -8 的 return addr
uint64 *retaddr = (uint64 *)(framep - 8);
printf("%p\n", *retaddr);
// 取偏移 -16 的 saved frame pointer
uint64 *savframep = (uint64 *)(framep - 16);
framep = *savframep;
}
}

// path: kernel/riscv.h
static inline uint64
r_fp() {
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

Alarm (HARD)

要求

In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.

“在这个练习中你要为 xv6 增加一个周期性提醒正在使用 cpu 时间的进程的功能。这对于那些处于执行边界的进程限制自己到底使用了多少 cpu 时间是非常有用的,又或者是那些既想执行运算又想周期性执行其他某一功能的程序也是非常有用。一般来说,你要实现这样一个功能是通过用户中断/错误例程原语的形式实现的,这样的原语使得你可以用在其他程序的页错误事件的处理上。如果你通过了 alarmtest 和 usertest 那么你的答案就是正确的”

You should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application calls sigalarm(0, 0), the kernel should stop generating periodic alarm calls.

“你需要新增一个 sigalarm(interval, handler) 系统调用。如果一个应用程序调用 sigalarm*(n, fn),那么程序每消耗 CPU 的 n 个 ‘tick’ 之后,内核就会调用函数 fn()。当 fn() 返回时,应用程序应该在它离开的地方继续执行。一个 tick 时 xv6 里面一个相对独立的时间单元,由硬件计时器生成中断的频率决定。如果一个应用程序调用 sigalarm(0, 0) 那么内核不会生成周期性的 alarm 调用” ———— 注:这一大段内容里面 “任务型描述” 只有首位两句,即 “每过 interval 个时间片,内核会调用 handler” 以及 “sigalarm(0, 0) 不调用 handler”

You’ll find a file user/alarmtest.c in your xv6 repository. Add it to the Makefile. It won’t compile correctly until you’ve added sigalarm and sigreturn system calls (see below).

“你需要在你本地 xv6 repo 里找到 user/alarmtest.c,将(要实现的 sigalarm())增加到 Makefile。你将不能通过编译直至你已经增加了 sigalarm() 和 sigreturn()(详见下文)两个系统调用”

这个 assignment 稍微有些不同,分成了 test0 和 test1/test2 两部分,但我真正做完后发现其实应该合起来看才算一个完整的闭环。test0 负责更改执行流(调用 handler),test1/test2 负责返回。所以我会将 test0、test1/test2 两部分内容合在一起,以下是它们的要求:

Get started by modifying the kernel to jump to the alarm handler in user space, which will cause test0 to print “alarm!”. Don’t worry yet what happens after the “alarm!” output; it’s OK for now if your program crashes after printing “alarm!”.

“通过修改内核跳转至用户空间的 alarm handler 开始,这会使 test0() 打印 ‘alarm!’。不要担心在 test0() 输出 ‘alarm!’ 之后发生的东西,其实你的程序在打印 ‘alarm!’ 之后崩溃也是正常的(注:因为现在只完成了 test0)”

Chances are that alarmtest crashes in test0 or test1 after it prints “alarm!”, or that alarmtest (eventually) prints “test1 failed”, or that alarmtest exits without printing “test1 passed”. To fix this, you must ensure that, when the alarm handler is done, control returns to the instruction at which the user program was originally interrupted by the timer interrupt. You must ensure that the register contents are restored to the values they held at the time of the interrupt, so that the user program can continue undisturbed after the alarm. Finally, you should “re-arm” the alarm counter after each time it goes off, so that the handler is called periodically.

“(上面那个测试的)现象是正常的,比如:alarmtest 执行完 test0() 或 test1() 并打印完 ‘alarm!’ 之后崩溃;或者 alarmtest 打印 ‘test1 failed’,又或者 alarmtest 没有打印 ‘test1 passed’ 就退出。为了修复这个问题,你必须确保 alarm handler 完成之后控制流能够返回程序原来被时间片中断打断的地方。同时你也要确保寄存器内容能够恢复回中断的时候保存的值,因为这样做才能使得用户程序在 alarm 之后能够不被扰乱地继续向后执行。最后,你应该在每次离开后复位 alarm counter(注:alarmtest.c 里面的一个 static 变量),以便 handler 可以周期性地调用”

As a starting point, we’ve made a design decision for you: user alarm handlers are required to call the sigreturn system call when they have finished. Have a look at periodic in alarmtest.c for an example. This means that you can add code to usertrap and sys_sigreturn that cooperate to cause the user process to resume properly after it has handled the alarm.

“我们为你设计了一个解决思路,你可以从这一点着手考虑。这就是,用户 alarm handler 在它们每次执行完成后,都需要调用 sigreturn() 系统调用返回。详细情况请阅读 user/alarmtest.c 里面的 periodic()。这意味着你需要为 usertrap() 和 sys_sigreturn() 增加的代码是能够相互协作起来,最终使用户进程在处理完 alarm 之后在恰当的地方继续执行下去”

一口气做了一年的阅读理解,现在来总结下这个 assignment 的任务要求:

  • 首先核心功能是,每经过一段时间,内核需要跳转至另外的函数调用即 handler
  • test0 负责处理怎么跳转的,其实 test0 只需管跳转就好,不需要考虑返回的问题就已经能够 pass 了
  • test1/test2 负责处理怎么返回,要求能在中断的地方返回

理论基础是 traps 的调用过程,也即是 lec 6: Isolation & system call entry/exit 的内容


提示

以下是 test0 的提示:

Your sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h).

“你的 sys_sigalarm() 需要在 proc 结构体(位于 kernel/proc.h)的新字段中保存 alarm 的时间间隔和 handler 函数指针”

You’ll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; you’ll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.

“你需要为一个进程的 alarm handler 保持追踪,记录下自从上次结束后过去了多少个 ticks,或者距下次调用开始时还剩余多少个 ticks。同时,你也需要在 proc 结构体里为这个功能新增一个字段。你可能要在 kernel/proc.c 的 allocproc() 里面初始化 proc 结构体的这些字段”

Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.

“硬件计时器强制中断的每个 tick 位于 kernel/trap.c 的 usertrap() 里进行处理”

You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like
    if(which_dev == 2) ...

“如果出现了一个时间片中断你只需要处理进程的 alarm tick,像上面那个样子”

Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user’s alarm function might be 0 (e.g., in user/alarmtest.asm, periodic is at address 0).

“只有在时间片到达时间间隔时才调用 alarm,注意用户 alarm 函数的地址可能是 0(例如,在 user/alarmtest.asm 里面,periodic() 的地址是 0)” ———— 注,这里就是说,sigalarm() 第二个参数传入 0 是符合逻辑的

You’ll need to modify usertrap() so that when a process’s alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

“你要修改 usertrap(),以便在进程的 alarm 时间间隔到了,用户进程可以运行 handler。请思考一下这个问题,当一个在 risc-v 的 trap 返回到用户空间时,是什么决定了继续在用户空间执行的指令地址?” ———— 注:epc 寄存器


以下是 test1/test2 的提示:

Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).

“你的实现需要涉及保存和恢复寄存器现场 ———— 也请想一想,到底有什么寄存器你要为中断代码继续执行所要保存以及恢复的(可能很多)”

Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.

“在时间片耗尽的时候 usertrap() 在 proc 结构体里面保存了足够的状态吗?以使 sigreturn() 能够正确返回到被中断的用户代码?”

Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again. test2 tests this.

“阻止重复调用未完成的 handler,即如果一个 handler 尚未返回,内核不应该再次调用。test2 测试这一步骤”


Summary

好了,现在我们把第二年的阅读理解也做完了,所以到点总结下上面的提示说了什么内容吧(其实把提示一个个做完,就是完成了):

  • 首先要安装系统调用,这里推荐下我前几篇 blog 总结的 系统调用安装流程(位于开头处)
  • proc 结构体里面新增三个字段,分别是:interval(记录时间间隔)、handler(记录 handler 函数调用的指针)以及 ticks(记录 tick 累加和)
  • kernel/proc.c 的 usertrap() 里面关于时间片中断的地方,加入 test0 的处理逻辑 ———— 就是每耗尽一个时间片,tick 累加和累加,然后若 tick 累加和等于时间间隔就跳转去 handler
  • 强行覆盖 epc 寄存器,这样时间片中断执行完后,就能够跳转去 handler 的地址了(后面 test1/test2 再考虑返回的处理逻辑)———— 这是问你哪个寄存器决定了返回到用户空间继续执行那个提示的答案
  • 继续在 proc 结构体里面新增一个字段,用以保存整个 trapframe 结构体 ———— 这是问你要保存和恢复什么寄存器那个提示的答案 ———— 我是这么考虑的,如果你忽略跳转地址的强行覆盖,那这就是一次完整的中断过程。比如 test0() 执行时发生了时间片中断,那么忽略强制跳转 handler,下次返回的时候寄存器仍然是 test0() 离开时候的值。但现在我们强行跳转至 handler,也即时间片中断返回后我们是 “回到” 了 periodic() 函数里面,但是实际上寄存器环境依然是 test0() 的,那这就出大问题了。因为 periodic() 会执行一些指令,这些指令会改变之前 test0() 的现场,就算最终执行流返回 test0() 也没意义了,因为 test0() 的现场已经被破坏了。所以,handler 的现场需要另外保存,所以我们需要新增一整个 trapframe 结构体字段。下图会展示这样一个调用过程
  • 运用一些手段控制未执行完的 handler 重复调用的问题 ———— 我的想法是设一个标志,用以标识可不可以调用 handler。问题就变成了,在 sigreturn() 的时候打开标志,允许下一次 handler 的调用,而在调用 handler 的同时清楚标志,表示现在正在执行 handler,不允许继续调用

以下这张图展示了倒数第二点我想表达的逻辑:

如图所示,黑线表示调用跳转的执行流,紫线表示调用返回的执行流,红框框表示了强制覆盖返回地址的逻辑。实际上最后一步(第 ⑤ 步)应该返回 test0(),此时的寄存器现场仍然是 test0() 的,但实际上是 “返回” 了 handler,所以需要另外开辟一个 trapframe 来保存 handler 自己的现场


Solution

1
2
3
4
# path: ./Makefile
UPROGS=\
...
$U/_alarmtest\
1
2
3
# path: user/usys.pl
entry("sigalarm");
entry("sigreturn");
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// path: kernel/syscall.h
#define SYS_sigalarm 22
#define SYS_sigreturn 23

// path: kernel/syscall.c
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
...
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
...
}

// path: user/user.h
int sigalarm(int, void (*)(void));
int sigreturn(void);

// path: kernel/proc.h
struct proc {
...
// lab traps, test0
int interval;
uint64 handler;
int ticks;

// lab traps, test1
struct trapframe oritf;

// lab traps, test2
int cannot;
};

// path: kernel/proc.c
static struct proc*
allocproc(void) {
...
// lab traps, test0
p->interval = 0;
p->handler = 0;
p->ticks = 0;

return p;
...
}

// path: kernel/trap.c
void
usertrap(void) {
...
if (r_scause() == 8) {
...
} else if((which_dev = devintr()) != 0){
if (which_dev == 2) {
// lab traps, test0
// 当时间片耗尽(即过去了一个 ticks),需要处理 interval
p->ticks++;

// 检查当前 ticks 累加值是否到数了
if (p->interval == p->ticks && p->cannot != 1) {
p->ticks = 0;
// 为什么可以强行改变原来的返回地址?
// 其实需要 test0、test1、test2 连起来一起看
// test0 只管改变执行方向,test1/2 负责返回

// test1/2:
memmove(&p->oritf, p->trapframe, sizeof(struct trapframe));
p->trapframe->epc = p->handler;
// test 2:
p->cannot = 1;// cannot
}
}
} 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;
}
...
}

写在后面

共耗时 15h26m

CATALOG
  1. 1. LAB
  2. 2. RISC-V assembly (EASY)
    1. 2.1. 要求
    2. 2.2. Summary
    3. 2.3. Solution
  3. 3. Backtrace (MODERATE)
    1. 3.1. 提示
    2. 3.2. Summary
    3. 3.3. Solution
  4. 4. Alarm (HARD)
    1. 4.1. 要求
    2. 4.2. 提示
    3. 4.3. Summary
    4. 4.4. Solution
  5. 5. 写在后面