【MIT 6.S081】Lab4: Traps
MIT 6.S081的Lab4: Traps题解
RISC-V assembly (easy)
- 目标:认识RISC-V的汇编代码
- 方法:执行make fs.img编译,在user文件夹中找到call.c的汇编文件call.asm
问题一
Which registers contain arguments to functions? For example, which register holds 13 in main’s call to
printf?
即函数的参数存放在哪些寄存器?例如,哪个寄存器存放了call.c的main函数中的参数13。
分析
| 1 | printf("%d %d\n", f(8)+1, 13); | 
可以看到,13这个参数放在了a2寄存器,f(8)+1已经被计算出来是12放在a1寄存器。
回想起很多年前(并不)的计组知识(学的是mips),那这个即存在大概是a0~a7
回答
a0~a7 ,a2
问题二
Where is the call to function
fin the assembly code for main? Where is the call tog? (Hint: the compiler may inline functions.)
main函数里调用函数f读到代码在哪?g呢?(提示:编译器可能会将函数变成inline的)
分析
可以看到代码中并没有专门的跳转,所以是inline了。
答案
没有,编译器将其转成内联函数。
问题三
At what address is the function
printflocated?
printf函数的在哪?
分析
| 1 | 34: 600080e7 jalr 1536(ra) # 630 <printf> | 
jalr是跳转,所以位置在1536(ra),再往前找ra:
| 1 | 30: 00000097 auipc ra,0x0 | 
auipc指令:将立即数左移12位加到PC上,这里立即数是0所以ra=pc=30。
则1536(ra)=0x600+0x30=0x630 。
答案
0x630
问题四
What value is in the register
rajust after thejalrtoprintfinmain?
main函数中跳转printf后的时刻ra值是多少?
查资料可知jalr指令将下一条指令的位置也就是pc+4赋给ra,所以应该是0x38。
问题五
Run the following code.
2
printf("H%x Wo%s", 57616, &i);What is the output? Here’s an ASCII table that maps bytes to characters.
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
ito in order to yield the same output? Would you need to change57616to a different value?Here’s a description of little- and big-endian and a more whimsical description.
跑以下代码:
| 1 | unsigned int i = 0x00646c72; | 
输出是什么?可以查ASCII对应表。输出取决于RISC-V是小端编址,如果它是大端编址,如何修改i得到相同输出?57616是否需要改?
分析
直接放到main.c的代码里去跑,输出结果是He110 World ,合理的。
因为%x输出57616 的十六进制数即E110。
而字符串输出按地址读,由于小端编址,最右作为字地址(计组应软件接口教材里的说法,着实抽象),就是从最右开始存完整的一个字(高位先存),则有地址从小到大:72 6c 64 00 ,对应ASCII查表得r l d \0 。
大端编址存储会是00 64 6c 72,为了输出一致,i=0x726c6400 即可。
答案
He110 World ,大端编址i=0x726c6400 ,57616不用改。
问题六
In the following code, what is going to be printed after
'y='? (note: the answer is not a specific value.) Why does this happen?
第二个参数,没有给寄存器正确的值,则如果函数调用第二个参数应当是当时寄存器a2中的值。
Backtrace (moderate)
要求
考虑到debug时经常需要查看到错误发生时之前的函数栈,在kernel/printf.c 中实现一个backtrace() 函数。
编译器在每个栈帧中放一个帧指针指向调用者的帧指针。
在sys_sleep中调用这个函数并运行bttest 验证,输出应是:
| 1 | backtrace: | 
也许会有些许不同,但输入:
| 1 | addr2line -e kernel/kernel | 
应该会看到:
| 1 | kernel/sysproc.c:74 | 
提示:
- 
记得把函数加到 kernel/defs.h中以便sys_sleep调用
- 
GCC编译器将当前的帧指针存在 s0中,在kernel/riscv.h中加入以下函数:1 
 2
 3
 4
 5
 6
 7static inline uint64 
 r_fp()
 {
 uint64 x;
 asm volatile("mv %0, s0" : "=r" (x) );
 return x;
 }在 backtrace()中可以利用这个读当前帧指针,这个函数用内联汇编读s0,
- 
栈帧如图,则return address在 fp-8,调用者的栈帧在fp-16:![image-20230413233731538]()  
- 
xv6给每个栈一个page, kernel/riscv.h中的PGROUNDDOWN(fp)andPGROUNDUP(fp)可以帮忙结束循环。
- 
成功后, 在 kernel/printf.c的panic中调用该函数。
分析
题面写的很清楚了,关于栈帧的具体分类和内容可以看xv6手册,这里不做总结了。
fp-8 指向return address,所以*(fp-8)就是地址,但直接输出十六进制会有问题,所以再取一次变成**(fp-8)输出%p即可。
*(fp-16)指向调用者的fp的位置,所以fp迭代为**(fp-16)。
但是有一个细节,从题面给的r_fp()可以看出fp是uint64格式的,所以第一次*引用时要转一下uint64*(好坑)。
实现
在kernel/defs.h 中添加:
| 1 | // printf.c | 
在kernel/riscv.h 中添加:
| 1 | static inline uint64 | 
在kernel/sysproc.c 中补充sys_sleep :
| 1 | uint64 | 
在kernel/printf.c中添加:
| 1 | void | 
测试
| 1 | make qemu | 
输出:
 
地址略有不同,提示给的方法exec fail了,所以直接运行了评价程序:
| 1 | sudo python3 grade-lab-traps backtrace | 
通过:
 
按提示,测试完成后在kernel/printf.c 的panic 中添加:
| 1 | void | 
Alarm (hard)
要求
为xv6加功能,在一个进程使用cpu时周期性提醒它。你需要实现一个用户中断/错误处理的初级形式,这与处理页表错误类似。
加一个新的系统调用sigalarm(interval, handler),应用使用sigalarm(n, fn)时,每 n个CPU时间的ticks停止应用并调用fn函数,该函数返回时原应用继续。xv6的ticks取决于硬件计时器产生中断的频率。调用sigalarm(0, 0)时不再继续。
将user/alarmtest.c加入Makefile,sigalarm和sigreturn正确实现后才能编译通过。
alarmtest调用sigalarm(2, periodic)在test0,alarmtest的汇编代码在user/alarmtest.asm,也许可以帮助debug,正确时输出:
| 1 | $ alarmtest | 
分析
完整代码见实现一节。
如果记忆不清晰了需要重新回顾一下【MIT 6.S081】Lab2: System Calls 中的许多内容。
test0: 调用处理函数
跟着提示走:
将 alarmtest.c 添加到Makefile中:
| 1 | UPROGS=\ | 
将函数声明放到 user/user.h 中:
| 1 | int sigalarm(int ticks, void (*handler)()); | 
更新user/usys.pl、kernel/syscall.h、kernel/syscall.c,可见【MIT 6.S081】Lab2: System Calls 中的第一个实验,基本一致。
kernal/sysproc.c中的sys_sigreturn 函数现在只需返回0:
| 1 | uint64 sys_sigreturn(void) | 
kernal/sysproc.c中的sys_sigreturn 函数需要将两个参数(ticks和函数指针,方法回顾Lab2)传给进程,同时进程也需要记录已经过了多少个ticks,类似Lab2操作kernel/proc.h中的proc结构体,并在kernel/proc.c中初始化:
| 1 | // kernel/proc.h | 
| 1 | // kernel/proc.c | 
| 1 | // kernel/sysproc.c | 
硬件产生的中断在kernel/trap.c的 usertrap() 处理,则每次中断更新进程的ticks计数,如果到达周期则调用对应函数,提示在代码if(which_dev == 2) ...中处理,以及提醒注意需要调用函数的地址可能为0,则只能通过interval为0判断是否需要处理。
在usertrap()中注意到一行关键代码:
| 1 | // sepc points to the ecall instruction, | 
这熟悉的下一条指令"PC=PC+4",显然将p->handler传给p->trapframe->epc即可。
| 1 | // kernel/proc.c | 
于是完成了第一部分,这时候运行$ alarmtest会crashed,但是既然文档说了不用管那就不用管。
 
test1/test2: 返回原来的执行位置
alarm要求结束时调用 sigreturn ,user/alarmtest.c中的periodic可做参考。
为了保存调用函数前的状态,需要保存寄存器现场,这里看了网上提示才恍然大悟,存trapframe就行了,用的是基本的memmove。
| 1 | // kernel/proc.c | 
| 1 | // kernel/sysproc.c | 
 
这时候test2出问题了,test2是要解决如果被调用的函数还没有返回但ticks到了,这时不要再次调用,进程加一个标志就可以了:
| 1 | // kernel/proc.h | 
| 1 | // kernel/proc.c | 
| 1 | // kernel/proc.c | 
| 1 | // kernel/sysproc.c | 
然后就能通过了!usertests一样也通过。
实现
Makefile:
| 1 | UPROGS=\ | 
user/user.h :
| 1 | int sigalarm(int ticks, void (*handler)()); | 
user/usys.pl:
| 1 | ... | 
kernel/syscall.h:
| 1 | ... | 
kernel/syscall.c:
| 1 | ... | 
kernal/sysproc.c:
| 1 | uint64 sys_sigalarm(void) | 
kernel/proc.h:
| 1 | struct proc { | 
kernel/proc.c:
| 1 | static struct proc* | 
kernel/trap.c:
| 1 | void | 
测试
| 1 | sudo python3 grade-lab-traps alarm | 
 
    