MIT 6.S081 2021: Lab traps
RISC-V assembly
Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf
?
第一道题目是RISC-V汇编的相关内容。课程网页上reference里给了一些参考资料,我搜了一下第一本The RISC-V Reader,没想到还有中文版,建议看一下第三章:The RISC-V Reader: An Open Architecture Atlas (riscvbook.com)
下图是RISC-V各寄存器的用途:
函数各参数显然是在a0-a7中传递的,再看一下printf附近的汇编代码,根据"li a2,13"显然可知是a2。
Where is the call to functionf
in the assembly code for main? Where is the call tog
? (Hint: the compiler may inline functions.)
显然没有调用,编译器直接优化了,把f(8)+1的值直接计算出来传入printf中。
At what address is the function printf
located?
这个auipc是把0x0左移12位,再加上PC值0x30存到ra里。jalr是先根据ra算出跳转地址,再把现在的PC+4存入ra,作为稍后printf的返回地址。所以printf的位置是0x30+1552=0x640。(后面注释也写了)注意这个值是会变的,不同的环境可能不一样。
What value is in the registerra
just after thejalr
toprintf
inmain
?
把运行到jalr处的PC+4存入ra,也就是0x38。
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.
打印结果是HE110 World。第一个(57616)2=(E110)16 这个显然。i=110 0100 0110 1100 0111 0010,在内存里每个字节还要反过来存储。因此直接查询ascii表就行了。
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);
结果是x=3 y=1。y的值取决于printf第三个参数,a2寄存器的值。为了验证,我们在0x38处下断点:
运行到断点处,执行一下info reg看看寄存器内容,$a2=1:
Backtrace
在kernel/printf.c里实现函数backtrace(),要求是打印栈里存储的frame pointer。Hints里提示了risc-v栈帧的结构,很明显是一个链表结构,因此我们需要做的事情就只是遍历链表。
void backtrace(void)
{
struct proc* p=myproc();
printf("backtrace:\n");
uint64 fp=r_fp();
while(1)
{
fp=fp-16;//入栈的上一级frame pointer
uint64 ret=*((uint64*)(fp+8));
fp=*((uint64*)fp);//访问这个地址
if(PGROUNDUP(fp) != p->kstack+PGSIZE)
{
break;
}
printf("%p\n",ret);//打印一下返回的PC值
}
}
被调用者的栈帧保存的frame pointer存储在距离栈底16B的位置,占8B空间(记住栈的空间是从高到低扩展的),指向调用者的栈底。因此,首先把r_fp()得到的寄存器值减去16,得到frame pointer的地址;然后读出存储在距离栈底8B的return address 也就是fp+8。
这里用了一点技巧,先把fp+8转换成uint64类型的指针,然后再使用*运算符把fp+8里面存储的值读出来。用相同的方法访问fp得到上一级被调用者栈帧的开头,如果fp所处的页表最上端已经不是p->kstack指向的页表最上端,说明fp已经脱离了内核分配给它的栈帧,循环终止。
多说一句:对于一个C结构体或者C++类
typedef struct e
{
int x;
int y;
int a;
int b;
int c;
int d;
}Entity;
想计算某一个成员相对结构体开头的偏移量,怎么办?以b为例:
printf("%d",&(((Entity*)0)->b));
先把0强制转换成Entity*类型的指针,也就是欺骗编译器,告诉它:有一个从地址0开始的Entity类,并使用&运算符计算这个类中b的地址,就可以得到结果12。
Alarm
这个实验的要求还是比较复杂的,做之前一定要看一下视频https://www.bilibili.com/video/BV19k4y1C7kA?p=5。
先梳理一下用户进程进入内核态的过程;
1.调用ecall,ecall负责把进程从user mode提升到kernel mode,把PC当前值存进sepc里,把stvec寄存器里的地址复制到PC
2.stvec里是trampoline.S中uservec的地址,CPU执行下面的指令,切换用户页表到内核页表,保存各寄存器到tramframe,从trapframe中恢复kernel stack地址等参数,跳转到usertrap()
3.usertrap执行系统调用或者处理中断,调用usertrapret()。如果执行系统调用的话,就将p->trapframe->epc加上4,这样稍后返回用户态的时候就会从调用trap的下一条指令开始执行。
4.usertrapret存储kernel相关信息,跳转到trampoline.S中的userret执行下面的汇编代码
5.userret恢复寄存器和页表,最终返回用户态。
搞清楚系统调用是怎么进行的之后,我们来看程序要求。要求实现一个sigalarm函数,传入一个函数指针handler,每过若干次时钟中断就执行一次handler。根据提示,我们用if(which_dev == 2)来检测时钟中断。需要注意的是,程序传入的是用户地址空间中的函数指针,是不能在内核中执行的,需要返回到用户态中执行。所以,当需要执行handler时,应该做的事情是把函数指针赋给p->trapframe->epc,这样返回用户态的时候CPU就会从handler的位置执行指令,而不是试图在usertrap()等地方调用函数指针。有了这个基本思路就可以写代码了。先给struct proc添加几项:
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct trapframe *alarm_save; // 保存调用sigalarm之前的寄存器的原始数据
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int alarm_int; //tick时间间隔
uint64 handler; //sigalarm()需要处理的handler
int tick_passed; //上一次执行handler之后过去的时间
int flag; //是否正在调用handler
};
在allocproc()里初始化:
found:
p->pid = allocpid();
p->state = USED;
p->tick_passed=0;
p->alarm_int=-1;
p->handler=0;
p->flag=0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
//分配alarm_save
if((p->alarm_save = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
freeproc():
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->alarm_save)//记得释放alarm_save
kfree((void*)p->alarm_save);
p->alarm_save = 0;
修改后的usertrap:
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
intr_on();
syscall();
} else if((which_dev = devintr()) != 0){
// ok
//intr_on();
if(which_dev == 2&&p->alarm_int!=0)
{
p->tick_passed+=1;
if(p->tick_passed==p->alarm_int)
{
if(p->flag==0)
{
memmove(p->alarm_save,p->trapframe,PGSIZE);
p->trapframe->epc=p->handler;
p->flag=1;//只有sigreturn可以将它置为0
}
p->tick_passed=0;
}
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
{
yield();
}
usertrapret();
}
两个系统调用:
uint64 sys_sigalarm(void)
{
struct proc* p=myproc();
//获取参数
int ticks;
uint64 handler_ptr;
argint(0,&ticks);
argaddr(1,&handler_ptr);
//传入参数
p->alarm_int=ticks;
p->handler=handler_ptr;
return 0;
}
uint64 sys_sigreturn(void)
{
struct proc* p=myproc();
p->flag=0;//记得p->flag重置为0
memmove(p->trapframe,p->alarm_save,PGSIZE);
return 0;
}
代码说明:
1.alarm_int是指定的时间间隔,在proc中初始置为-1,tick_passed初始置为0,所以不调用sigalarm就永远不会执行下面memmove等操作。sys_sigalarm()唯一的作用是把时间间隔和函数指针存入struct proc之中。
2.如果alarm_int==0,则不执行任何操作。
3.如何保证执行完handler之后回到原来trap的位置? 在proc中设置新参数struct trapframe *alarm_save。初始化进程时给这个指针分配一页内存。在修改epc到handler之前,把整个trapframe复制到alarm_save指向的页。(当然这个页只能在kernel mode中访问)
4.系统调用sigreturn()的作用就是:handler完成后,再进入usertrap,复制这个alarm_save到trapframe中,这样稍后再执行usertrapret()时,trapframe和刚执行sigalarm()时的trapframe就完全相同了。
5.如何保证执行handler时不再次执行sigalarm():在proc中声明一个变量flag,初始化为0。只有flag==0时才允许执行修改epc和memmove的操作,执行完之后flag置1,只有sigreturn才有权限把它重新置为0。这样,如果在handler执行时遇到时钟中断,当tick_passed等于alarm_int时就不会执行handler操作,但是仍然会执行重置或累加tick_passed的操作。