【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
f
in 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
printf
located?
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
ra
just after thejalr
toprintf
inmain
?
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
i
to in order to yield the same output? Would you need to change57616
to 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 |