XV6学习(6)Lab: traps
这一个实验主要是对RISC-V的汇编、栈帧结构以及陷阱进行简单的了解,难度并不大。
代码放在github上。
RISC-V assembly (easy)
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寄存器。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.
从代码可以看出,这两个都被内联优化处理了。main中的f调用直接使用了结果12,而f中的函数g调用直接内联在f中了。
Q3: At what address is the function printf located?
在0x630的位置
Q4: What value is in the register ra just after the jalr to printf in main?
值应该为0x38,即函数的返回地址。
跳转并链接指令(jal)具有双重功能。若将下一条指令PC + 4的地址保存到目标寄存器中,通常是返回地址寄存器ra,便可以用它来实现过程调用。如果使用零寄存器(x0)替换ra作为目标寄存器,则可以实现无条件跳转,因为x0不能更改。像分支一样,jal将其20位分支地址乘以2,进行符号扩展后再添加到PC上,便得到了跳转地址。
跳转和链接指令的寄存器版本(jalr)同样是多用途的。它可以调用地址是动态计算出来的函数,或者也可以实现调用返回(只需ra作为源寄存器,零寄存器(x0)作为目的寄存器)。Switch和case语句的地址跳转,也可以使用jalr指令,目的寄存器设为x0。
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 i to in order to yield the same output? Would you need to change 57616 to a different value?
结果为:He110 World; 不要修改为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 (moderate)
实现backtrace,递归打印函数调用栈。使用r_fp获取当前栈帧地址,由于栈是由高地址向低地址增长的,因此使用PGROUNDUP获得栈底地址,之后循环打印栈帧的函数的返回地址。
void
backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 base = PGROUNDUP(fp);
while(fp < base) {
printf("%p\n", *((uint64*)(fp - 8)));
fp = *((uint64*)(fp - 16));
}
}
Alarm (hard)
这一个要求添加系统调用sigalarm
来实现当用户程序运行了n个ticks后,触发一次回调函数。由之前的学习可以知道,时钟中断的处理是在usertrap
函数中的if(which_dev == 2)
里面的。
为了实现这个功能,首先在proc
结构体中添加相应字段:
struct proc {
...
// these are used for sys_alarm
int duration; // ticks after last alarm
int alarm; // alarm every n ticks
uint64 handler; // handler for alarm
struct trapframe *alarm_trapframe; // register saved for alarm
};
之后实现sys_alarm
函数,将相关信息填入proc
中:
uint64
sys_sigalarm(void)
{
int ticks;
uint64 handler;
if(argint(0, &ticks) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
struct proc* p = myproc();
p->alarm = ticks;
p->handler = handler;
p->duration = 0;
p->alarm_trapframe = 0;
return 0;
}
而最关键的部分是在usertrap
中,当发生时钟中断时,将p->duration
增加,如果p->duration == p->alarm
,那么就要触发一次回调函数,而触发的方法就是将p->trapframe->epc
设置为回调函数地址,当陷阱处理程序结束后就会跳转到回调函数。
而为了保证回调函数不会破坏原程序的寄存器,需要对trapframe
进行保存;我这里选择的方法是通过kalloc
申请一个新的trapframe
结构体,然后将trapframe
复制一份。
为了保证回调函数执行期间不会重复调用,就可以判断p->alarm_trapframe
是否为0,不为0说明上一次的回调函数还没有调用sigreturn
,即函数未结束。
if(which_dev == 2){
if(p->alarm != 0){
p->duration++;
if(p->duration == p->alarm){
p->duration = 0;
if(p->alarm_trapframe == 0){
p->alarm_trapframe = kalloc();
memmove(p->alarm_trapframe, p->trapframe, 512);
p->trapframe->epc = p->handler;
}else{
yield();
}
}else{
yield();
}
}else{
yield();
}
}
最后就是sigreturn
函数,这个函数要做的工作就是将之前保存的alarm_trapframe
还原到trapframe
中,并将alarm_trapframe
释放掉。别忘了在freeproc
函数中也要对p->alarm_trapframe
进行判断,防止程序异常结束时该页面没有被释放。
uint64
sys_sigreturn(void)
{
struct proc* p = myproc();
if(p->alarm_trapframe != 0){
memmove(p->trapframe, p->alarm_trapframe, 512);
kfree(p->alarm_trapframe);
p->alarm_trapframe = 0;
}
return 0;
}