Appearance
traps
开启新实验
git fetch
git checkout traps
make cleanRISC-V assembly
The code in call.asm for the functions g, f, and main.



The instruction manual for RISC-V is on the reference page.
Q1
Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
RISC-V的函数调用过程参数优先使用寄存器传递,即a0~a7共8个寄存器。返回值可以放在a0和a1寄存器。main函数printf调用的13保存在a2。
Q2
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.)
汇编代码中直接将12移入a2作为printf的参数,12已经是f(8)+1的结果了。
Q3
At what address is the function printf located?
30: 00000097 auipc ra,0x0
34: 5f8080e7 jalr 1528(ra) # 628 <printf>auipc 指令将当前指令地址加上一个立即数(0x0)存储到ra寄存器。auipc 指令的全称是 “Add Upper Immediate to PC”,用于计算当前 PC 地址的偏移。
jalr 指令是 “Jump And Link Register”,它将跳转到 ra + 1528 的地址,并将返回地址存储到 ra 寄存器。结合 auipc 和 jalr,我们知道这实际上是一个跳转到 printf 函数的调用。
当前 PC 地址是 0x30,那么 auipc ra,0x0 将 ra 设置为 0x30 + 0x0(即 0x30)。jalr 1528(ra) 将跳转到 0x30 + 1528,这就是printf函数的实际地址。
0x30 + 1528 = 0x30 + 0x5f8 = 0x628
因此,printf函数的地址是0x628。
Q4
What value is in the register ra just after the jalr to printf in main?
在main函数中调用printf之后,ra(返回地址寄存器)中将存储返回到main函数中的地址。这是因为jalr指令的作用是跳转到目标地址并将返回地址存储到ra中。
在执行jalr指令时,ra寄存器将被设置为跳转指令的下一条指令的地址,也就是printf调用后的下一条指令的地址。
根据代码,jalr指令的下一条指令是:
38: 4501 li a0,0`因此,ra寄存器将在执行jalr指令后包含0x38,这是printf返回时程序将继续执行的地址。
Q5
Run the following code.
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.
printf 语句将打印两个格式化的字符串:
"H%x Wo"- 打印字符H,然后是十六进制格式的数字57616,再是字符串Wo"%s"- 打印字符串,该字符串从i的地址开始
57616 的十六进制表示为 0xe110。
小端
i地址由低到高分别是:72 6c 64 00
从 i 的地址开始的字符串,转换为 ASCII 字符为:rld
所以输出He110 World。
大端
要保持输出不变,i地址由低到高应该依旧是:72 6c 64 00
在大端模式下,这个数字是:0x726c6400
即i应该设置为0x726c6400来保持与小端相同的输出。
对于 57616,它的值不受大端或小端影响,因为它只是一个直接的数值参数,不涉及内存存储顺序,所以不需要更改。
Q6
In the following code, what is going to be printed after
'y='? (note: the answer is not a specific value.) Why does this happen?printf("x=%d y=%d", 3);
应该打印出寄存器a2的值,因为printf会从a2寄存器中读取第三个参数作为y的值。
Backtrace
对于调试,回溯跟踪通常很有用:在错误发生点上方的堆栈上调用函数的列表。
任务描述:在kernel/printf.c中实现backtrace()函数。并在sys_sleep中插入对此函数的调用。
- GCC 编译器将当前正在执行的函数的帧指针存储在寄存器 s0 中。在
kernel/riscv.h中添加以下函数
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}r_fp() 这个函数读出了 s0 这个寄存器的值,然后储存在 x 中,最后又把 x 返回了。
但是我们要读取的明明是fp这个寄存器,为什么这个函数里写的是s0呢,具体可以看看下面这个表:

在 ABI Name 那一列,可以看到 s0 其实就是 fp 的别名。
- 在
kernel/printf.c中实现backtrace()
编译器在每个堆栈帧中放置一个帧指针,该指针保存调用方帧指针的地址。backtrace()应使用这些帧指针在堆栈中向上移动,并在每个堆栈帧中打印保存的返回地址。

请注意,返回地址位于与堆栈帧的帧指针的固定偏移量 (-8) 处,而上一个函数的帧指针保存于距当前帧指针的固定偏移量 (-16) 处。
Xv6 在 xv6 内核的每个堆栈中与 PAGE 对齐的地址处分配一个页面。可以使用 PGROUNDDOWN(fp) 和 PGROUNDUP(fp) 计算堆栈页面的顶部和底部地址。
需要使用 PGROUNDDOWN 和 PGROUNDUP 是因为,一连串的函数调用最多放在一个页中。那么如果我们在递归打印的时候,超出了这一页的范围,就可以说明已经是最底层的函数,可以停止了。
void backtrace(void)
{
printf("backtrace:\n");
uint64 *fp = (uint64*)r_fp();
uint64 *top = (uint64*)PGROUNDUP((uint64)fp);
uint64 *bot = (uint64*)PGROUNDDOWN((uint64)fp);
while(fp < top && fp > bot){
printf("%p\n", fp[-1]);
fp = (uint64*)fp[-2];
}
}类型转换好麻烦
可以看到这里用了一些很奇怪的写法,好像是负数下标的数字,其实这个 fp[-1] 等价于 *(fp - 1)。并且,因为这里 fp 是六十四位的指针,所以 *(fp - 1) 是读取 fp 前八个字节位置的数据。
- 将
backtrace()添加到kernel/defs.h中,以便在sys_sleep中调用。
void backtrace(void);- 在
kernel/sysproc.c的sys_sleep函数中插入对backtrace()的调用。
Alarm
任务描述:向 xv6 添加一个功能,该功能会在进程使用 CPU 时间时定期发出警报。添加一个新的sigalarm(interval, handler)系统调用,消耗每interval个 CPU 时间之后,内核应该导致应用程序函数handler被调用,返回时从中断的地方继续。此外还要实现一个sigreturn()系统调用,如果时间到了handler调用了sigreturn(),就应该停止执行handler,然后恢复正常的执行顺序。如果说 sigalarm 的两个参数都为 0,就代表停止执行handler函数。
test0:invoke handler
user/alarmtest.c已经实现,将其添加到 Makefile。在
user/user.h添加如下声明
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);- 在
user/usys.pl进行添加
entry("sigalarm");
entry("sigreturn");- 在
kernel/syscall.h中添加以下定义
#define SYS_sigalarm 22
#define SYS_sigreturn 23- 修改
kernel/syscall.c,用 extern 全局声明新的内核调用函数,并且在 syscalls 映射表中,加入从前面定义的编号到系统调用函数指针的映射。
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
}- 修改
kernel/sysproc.c,sys_sigreturn()暂时只需返回0
uint64 sys_sigreturn(void)
{
return 0;
}- 在
kernel/proc.h中,struct proc需要添加一些新成员
struct proc{
...
int interval;
uint64 handler;
int passed;
}- 修改
kernel/sysproc.c,sys_sigalarm()需要保存一下相关信息
uint64 sys_sigalarm(void)
{
struct proc *p = myproc();
int interval;
uint64* handler;
if(argint(0, &interval) < 0) return -1;
if(argaddr(1, &handler) < 0) return -1;
p->interval = interval;
p->handler = handler;
p->passed = 0;
return 0;
}- 修改
kernel/proc.c,在allocproc()和freeproc()中的初始化和释放
p->interval = 0;
p->handler = 0;
p->passed = 0;- 修改
kernel/trap.c中的usertrap(),以便当进程的警报间隔到期时,用户进程将执行处理程序函数。
每个时钟周期,硬件时钟都会强制中断,该中断在usertrap()中处理。
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval){
p->passed = 0;
p->trapframe->epc = p->handler;
}
}
yield();
}这样我们就能顺利的跳转到handler,并且通过 test0,当然也毫无悬念的报错了。
报错的主要原因是还没实现 sys_sigreturn(),这样在执行完handler函数之后就不知道返回哪里了。
test1/test2(): resume interrupted code
- 在
struct proc再加一个struct trapframe类的属性,用于备份执行 handler 前的环境
// kernel/proc.h
struct trapframe *alarmframe;- 修改
kernel/proc.c,在allocproc()和freeproc()中的初始化和释放
// allocproc()
if((p->alarmframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// freeproc()
if(p->alarmframe)
kfree((void*)p->alarmframe);
p->alarmframe = 0;- 在
kernel/trap.c里的usertrap()需要执行handler的时候,先备份一下环境,然后再执行
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval){
p->passed = 0;
*p->alarmframe = *p->trapframe;
p->trapframe->epc = p->handler;
}
}
yield();
}- 修改
kernel/sysproc.c,在sys_sigreturn()按照alarmframe恢复trapframe
uint64 sys_sigreturn(void){
struct proc *p = myproc();
*p->trapframe = *p->alarmframe;
return 0;
}到这里,再去运行alarmtest,会发现还是不能过。
如果 handler 执行的特别慢,自从上次调用 handler 已经过去了规定的时钟周期,但是 handler 还没执行好,这个时候我们又去改一遍 epc,这个 handler 又从头开始执行了,那着不就出大问题了,因为我们每次都会去改 epc,然后就永远执行不完 handler 了。
所以我们需要在 struct proc 里再加一个属性,就是 alarm_state。如果这个属性为 1,就表示,handler 程序正在执行,这个时候就算又过了 tick 个时钟周期,我们也不能去改 epc 让 handler 重复执行。
- 在
kernel/proc.h添加属性 - 在
kernel/proc.c添加初始化与释放
这两步很简单就跳过了,还要修改kernel/trap.c里的usetrap()
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if(p->interval > 0){
p->passed++;
if(p->passed > p->interval && !p->alarm_state){
p->passed = 0;
*p->alarmframe = *p->trapframe;
p->alarm_state = 1;
p->trapframe->epc = p->handler;
}
}
yield();
}只有alarm_state为0才允许跳转到handler,跳转时将其置1,标志着handler正在执行。
最后,修改kernel/sysproc.c中的sigreturn()。
uint64 sys_sigreturn(void){
struct proc *p = myproc();
*p->trapframe = *p->alarmframe;
p->alarm_state = 0;
return 0;
}不再执行handler时调用sigreturn(),所以alarm_state要恢复为0。
The End

复习了系统调用,了解了一点点汇编,学习了xv6的Traps。感觉自己并不是非常的理解陷入的过程,只是在一点一点地跟着实验指导做。而且还是没去仔细扒一下xv6的源码,对于一些结构不是很清楚都储存了什么信息,一些方法也不太会用,所以在实现上还是有点茫然。