【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.
1
2 unsigned int i = 0x00646c72;
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?
1 printf("x=%d y=%d", 3);
第二个参数,没有给寄存器正确的值,则如果函数调用第二个参数应当是当时寄存器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:
-
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 |