Lab: traps
RISC-V assembly
1.Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
main() :a0=pc value or ret value a1=12, a2=13
f() :a0=ret value
g() a0=ret value
2.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.)
2.1 no call f() and g()
make inline optimization that a1=a2
2.2 the compiler inline func
26 int f(int x) {
27 e: 1141 addi sp,sp,-16
28 10: e422 sd s0,8(sp)
29 12: 0800 addi s0,sp,16
30 return g(x);
31 }
32 14: 250d addiw a0,a0,3
33 16: 6422 ld s0,8(sp)
34 18: 0141 addi sp,sp,16
35 1a: 8082 ret
3.At what address is the function printf located?
line:630
4.What value is in the register ra just after the jalr to printf in main?
ra=pc+4=0x34+0x4=0x38
5.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?
5.1 "He110 World";
0x726c6400;
5.2 no,57616 is 110 in hex regardless of endianness.
6. 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);
A random value depending on what codes there are right before the call.Because printf tried to read more arguments than supplied.
The second argument `3` is passed in a1, and the register for the third argument, a2, is not set to any specific value before the
call, and contains whatever there is before the call.
Backtrace
实现一个函数backtrace(),如果某个程序调用了这个函数,该函数应该输出这个程序的 “函数调用顺序”,也就是把当前栈中的函数地址按照先后顺序全部打印出来。
函数调用
#include<stdio.h>
int add2(int a, int b) {return (a + b);}
int add1(int a, int b) {return (a + add2(a, b));}
int main(){
int c = add1(114, 514);
printf("%d\n", c);
}
在主函数中会调用一个 add1 函数,对于 add1,它又会去调用一个 add2,然后返回计算的结果,最后才在主函数中执行 printf。函数执行顺序main->add1->add2,函数执行完成顺序add2->add1->main。是先进后出的结构。
栈帧
栈的增长方向是高地址到低地址,也就是调用者栈帧的地址比被调用者栈帧的小。
图片的上半部分是调用者的栈帧,可以看到里面存有参数(也就是一种局部变量)。也有当前函数的返回地址,通过这个地址可以找到当前这个函数运行完了应该返回哪里。
返回地址是通过当前栈帧的帧指针确定的,它总是储存在当前帧指针 +8 的位置(在 64 位机器中,如果是图中的 32 位,那就是 +4 的位置)。
下半部分存的是当前函数的栈帧,里面同样存有局部变量。ebp 和 esp 分别标注了这个栈帧的起始和结束位置。
调用函数时栈帧的变化:
- 在调用一个函数时,我们先把函数的返回地址(也就是执行调用时 pc[2] 的值)压入栈中。
- 为了确保返回时能恢复当前帧指针的状态,还需要把帧指针压入栈中。
- 做完了准备工作,可以加入被调用函数的栈帧了。新栈帧当前还没有存放任何数据,所以起始地址和结束地址都是旧栈帧的结束地址。为了达到这一点,需要把帧指针的值(新栈帧起始值)设置成栈指针(老栈帧结束值)的值。
- 现在要把数据存入新栈帧,首先会需要更新栈指针的值,扩大栈的范围,因为栈帧是向低地址增长的,所以要根据局部变量及参数的大小,把栈指针的值减小一些。
- 栈帧已经有了足够的空间,可以放入局部变量并且执行这个函数了。至此,新栈帧的插入完全完成。
函数返回时栈帧的变化:
- 函数返回首先要释放之前占用的所有内存,所以我们直接把栈指针设置成帧指针。也就是把栈帧的结束地址直接改成开始地址,相当于撤销调用函数时的第 4 步。
- 现在需要用到之前备份的原帧指针来恢复函数调用之前的状态。我们需要从栈中弹出这个原帧指针,然后复制到帧指针寄存器。
- 弹出返回地址,赋值到 pc。
- 根据 pc 的值,继续执行原函数。
查看汇编代码的网站。
根据提示添加r_fp()函数,从RISC-V的表中可以看出s0就是fp的别名。
在sys_sleep中调用backtrace
void backtrace(void)
{
uint64 fp = r_fp();
printf("%p, %p\n", PGROUNDUP(fp), PGROUNDDOWN(fp));
}
output:
0x3fffff9000 0x3fffffa000
可以看出栈顶和栈底相差0x1000,即4096=4kb,所以可以用来判断fp是否在一页。
根据提示要返回的地址-8,把当前栈帧变成上一个栈帧-16.
void backtrace()
{
uint64 fp = r_fp();
printf("backtrace:\n");
uint64 ret_addr;
while((PGROUNDUP(fp)-PGROUNDDOWN(fp)) == PGSIZE)
{
ret_addr = *((uint64*)(fp-8));
printf("%p\n", ret_addr);
fp = *((uint64 *)(fp - 16));
}
}
Alarm
trap
在正常的情况下,我们写一个程序,那么这个程序运行起来大概是一个 “线性” 的过程,也就是程序里的内容是一条接着一条的运行下去的。
但在某些特殊情况下,这样“线性”的运行过程会被打破。比如我们熟悉的系统调用,就会暂停用户态程序的状态,跳转到内核态执行一些服务,然后再跳回用户态。
这样在用户态和内核态之间切换,去处理特殊性事件的过程,在 xv6 中称为陷入(trap)。
通常有以下几种情况会发生陷入:
- 系统调用
- 异常,如除以 0
- 设备中断,比如计时器中断
题意:
CPU的每经历一个tick就会触发一次timer interrupt,我们需要一个sigalarm(n, fn),使得xv6在n个ticks之后就能在CPU触发的timer interrupt中调用一次fn,这个fn是handler。
void
periodic()
{
count = count + 1;
printf("alarm!\n");
sigreturn();
}
// tests whether the kernel calls
// the alarm handler even a single time.
void
test0()
{
int i;
printf("test0 start\n");
count = 0;
sigalarm(2, periodic);
for(i = 0; i < 1000*500000; i++){
if((i % 1000000) == 0)
write(2, ".", 1);
if(count > 0)
break;
}
sigalarm(0, 0);
if(count > 0){
printf("test0 passed\n");
} else {
printf("\ntest0 failed: the kernel never called the alarm handler\n");
}
}
这个 sigreturn 的意思就是,我们本来可能在执行这个 for 循环中的代码,然后突然开始执行 periodic() 这个函数(因为时间到了)。如果在 periodic() 函数中调用了 sigreturn()。就应该停止执行 periodic() 里的东西,然后回到 for 循环中执行。
test0: invoke handler
实现调用一次handler,也就是调用一次periodic(),因为可以看到test0只输出了一个alarm!
test0 start
........alarm!
test0 passed
下一次syscall是怎么发生的:
- 硬件执行一些行为,做准备 [CPU]
- 汇编指令准备[vector]
- 在C代码中处理trap[handler]
- 返回原来的mode(kernel/mode)
program count: PC是怎么变化:
-
ecall:
sret是硬件指令,会将SEPC设置为PC,即SEPC <- PC <- ecall,SEPC此时为ecall -
userver: no operation about any PC
-
usertrap:
p->trapfram->epc = r_sepc();,将SEPC传给epc,epc此时为ecall -
usertrap:
p->trapframe->epc += 4;,+4指向下一条指令,epc此时为ret -
usertrapret:
w_sepc(p->trapframe->epc);,将epc传给SEPC,SEPC此时为ret -
userret:
sret是硬件指令,可以将PC设置为SEPC,PC此时为ret -
在用户态执行
PC即ret
回到test0,我们需要满足,n个ticks之后,在CPU触发的timer interrupt中调用一次handler,参照上面的PC变化,以及提示说要修改usertrap,不难想出:在usertrap中添加 p->trapfram->epc = handler;,使得最终处于用户态会去执行handler函数。这条语句当然需要写在if(which_dev == 2)之下,因为这是timer interrupt。
按hint完成前4个。
在proc.h中添加
int ticks; //ticks of alarm
void (*handler)(); //handler func
int passticks; // ticks from the last handler to the current handler
按顺序去初始化,在allocproc和freeproc, proc.c
allocproc(void)
found:
p->passticks = 0;
p->ticks = 0;
p->handler = 0;
p->pid = allocpid();
p->state = USED;
freeproc(struct proc *p)
p->passticks = 0;
p->ticks = 0;
p->handler = 0;
编写sys_sigalarm,获得入参ticks和(void*)handler,handler通过argaddr()传进来时是类型是uint64,要转型为(void*)
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->ticks = ticks;
p->handler = (void*)handler;
return 0;
}
修改usertrap(),满足n个ticks之后,才调用一次handler
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) // 时钟中断的编号为 2
{
p->passticks = p->ticks;
while(p->ticks)
{
p->ticks--;
if(p->ticks == 0)
{
// 直接改 epc,这样回用户态的时候就会执行地址为 epc 的指令
p->trapframe->epc = (uint64)p->handler;
}
}
p->ticks = p->passticks;
yield();
}
test1/test2(): resume interrupted code
大概的意思是,我们需要在执行完 handler 后返回到正确的位置。
先考虑test1,CPU触发timer interrupt之后,调用了一次handler即periodic()
他接着会调用syscall即sigreturn()还是一样:
- 硬件执行一些行为,做准备 [CPU]
- 汇编指令准备[vector]
- 在C代码中处理trap[handler]
- 返回user mode
返回user mode时,它的trapfram是属于periodic()的,但我们希望返回user mode时,它的trapfram是属于timer interrupt的。那我们就需要在调用periodic()之前先保存好timer_trapfram,在sys_sigreturn()返回user mode之前用timer_trapfram替换periodic()的trapfram即可。同时为了满足tset2引入一个标志
proc.h
struct trapframe *timer_trapframe; // saves registers to resume in sigret
int handler_execute; // handler executing => 1, handler no executing => 0
proc.c
allocproc()
p->pid = allocpid();
p->state = USED;
p->passticks = 0;
p->ticks = 0;
p->handler = 0;
p->handler_execute = 0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Allocate a timer_trapframe page.
if ((p->timer_trapframe = (struct trapframe *)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
}
freeproc()
...
p->handler = 0;
if(p->timer_trapframe)
kfree((void*)p->timer_trapframe);
p->timer_trapframe = 0;
p->handler_execute = 0;
...
修改usertrap(),在调用periodic()之前先保存好timer_trapfram,同时满足periodic()的执行没有结束之前,不能调用它
usertrap
if(which_dev == 2)
{
p->passticks = p->ticks;
while(p->ticks)
{
p->ticks--;
if(p->ticks == 0 && p->handler_execute == 0)
{
//trapfram给timer interrupt
memmove(p->timer_trapframe, p->trapframe, sizeof(struct trapframe));
p->handler_execute = 1;
p->trapframe->epc = (uint64)p->handler;
}
}
p->ticks = p->passticks;
yield();
}
修改usertrap(),在sys_sigreturn()返回user mode之前用timer_trapfram替换periodic()的trapfram,同时,此时已经执行periodic()可以视作执行结束,要将handler_execute置为0。
uint64 sys_sigreturn(void)
{
struct proc *p = myproc();
memmove(p->trapframe, p->timer_trapframe, sizeof(struct trapframe));
p->handler_execute = 0;
return 0;
}