本Lab主要熟悉在trap中系统调用的实现。
笔者用时约4h
概述
首先讲讲我对系统调用时trap的理解,用户程序通过系统调用进入内核态,在汇编代码中的体现就是ecall
指令,会把系统调用参数保存在寄存器a7中。
在用户态中执行ecall
指令之后,会将pc寄存器的内容复制到sepc中,并将模式转换为监督者模式,且会将stvec复制到pc中。
由于stvec的值为函数uservec
(定义在kernel/trampoline.S:37)的地址,故在执行完ecall
指令之后,会跳转到uservec
函数中。由于ecall
指令并没有切换页表的功能,于是在用户页表中必须要维护uservec
函数虚拟地址的映射,这也就是trampoline页的功能啦。
在uservec
函数中,首先需要将用户态中的所有寄存器内容保存到该用户进程的trapframe中,一开始trapframe的首地址保存在sscratch寄存器中,首先代码将a0寄存器与sscratch寄存器进行交换,然后开始保存所有其他寄存器的值(根据提前规定好的偏移量,定义在kernel/proc.h:44)。然后需要把当前进程trapframe中的一些字段保存到对应的寄存器中,比如kernel_sp保存到sp寄存器之类的。最后需要将该内核页表地址写入satp寄存器中,切换页表为内核页表。这里再一次体现了,内核页表和进程页表共同映射了trampoline页的原因。
uservec
函数最后调用了usertrap
函数(定义在kernel/trap.c:37中),在该函数中,首先向stvec
寄存器写入kernelvec
函数(定义在kernel/kernelvec.S中)的地址(因为这个时候在内核态,于是trap由kernelvec
函数进行处理),并将当前sepc寄存器中的值保存到trapframe的epc字段中,然后将epc加4,即跳过了ecall
指令,指向用户态的下一条指令。然后调用syscall
函数进行系统调用的操作。
当syscall
函数返回时,会调用usertrapret
函数(定义在kernel/trap.c:103中),该函数将uservec
重新写入stvec
寄存器中,将trapframe中的一些字段设置好,并将模式设置为用户模式,以便处理下一次的用户态trap。最后,将trapframe的地址作为第一个参数(保存在a0中),将用户进程跟页表地址作为第二个参数(保存在a1中),调用userret
函数(定义在kernel/trampoline.S:88中)。
在函数userret
中,首先切换页表为用户进程页表,且将trapframe页的地址保存到寄存器a0中,然后恢复所有寄存器的值,将原始a0寄存器的值(在系统调用情况下,trapframe中的a0字段被设置为系统调用返回值)放在sscratch寄存器中,最后交换a0寄存器和sscratch寄存器的值,并使用sret
指令(将模式变为用户模式,将sepc寄存器中的内容复制到pc寄存器中)回到用户态。
RISC-V assembly
回答一些问题
- Q: 哪些寄存器保存函数的参数?例如,在main对printf的调用中,哪个寄存器保存13?
A: 寄存器a0-a7保存函数参数,比如printf的调用中,第一个参数是字符串地址保存在a0中,f(8)+1保存在a1中,13保存在a2中。 - Q: main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
A: 被编译器优化掉了,减少了函数的调用,从li a1, 12可以看出 - Q: printf函数位于哪个地址?
A: 0x630, 在call.asm中搜一下就可以看到 - Q: 在main中printf的jalr之后的寄存器ra中有什么值?
A: 在main中,printf的jalr指令会将下一条指令的地址(也就是0x38)保存在寄存器ra中,于是jalr之后的寄存器ra中的值为0x38 - Q: 运行以下代码。
程序的输出是什么?unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i);
A: HE110 World
Q: 输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?
如果是大端存储,那么将i设置为0x726c6400即可,不需要将57616改为其他值 - Q: 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
A: y将打印成什么取决于上一次a2寄存器中的内容printf("x=%d y=%d", 3);
Backtrace
这一部分需要实现一个函数backtrace
,在调用处打印当前栈中所有栈帧保存的返回地址信息。
那么首先需要获取当前栈帧的帧尾指针fp,根据文档提示,我们可以在文件kernel/riscv.h中添加一个函数进行获取。
// read the frame pointer
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
在xv6中,栈的数据分布如下,当进行函数调用时,会将函数的返回地址、上一个栈帧的帧尾地址、被保存的寄存器和局部变量等压入栈中形成栈帧。于是,当前栈帧的返回地址信息在fp-8指向的地址中,上一个栈帧的帧尾地址在fp-16指向的地址中。
那么怎么停止循环呢。由于在xv6中,每一个栈的大小都是一页,且根据文档中提示,可以使用PGROUNDOWN和PGROUNDUP求出当前栈的首地址和末地址,如果当前栈帧中上一个栈帧的帧尾地址不在当前栈的地址范围内,则退出循环,不再继续打印,所写的函数如下。
void
backtrace()
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 l = PGROUNDDOWN(fp), r = PGROUNDUP(fp);
while (l <= fp && fp < r) {
printf("%p\n", *((uint64*)(fp - 8)));
fp = *((uint64*)(fp - 16));
}
}
Alarm
这一部分主要是实现两个系统调用sigalarm
和sigreturn
。
test0: invoke handler
sigalarm
系统调用接收两个参数ticks
和handler
,表示在经过ticks
个CPU时钟中断之后便调用函数handler
进行处理。
那么需要我们在进程结构体中添加几个字段,保存系统调用的参数ticks
和handler
,并记录当前进程已经经过多少个CPU时钟中断。
...
// for sigalarm
int ticks;
void (*handler) ();
int tita;
...
如下是sigalarm
系统调用的实现
uint64
sys_sigalarm(void)
{
int ticks;
uint64 handler;
struct proc* p = myproc();
if(argint(0, &ticks) < 0 || argaddr(1, &handler) < 0)
return -1;
p->ticks = ticks, p->handler = (void (*)())handler;
return 0;
}
当遇到CPU时钟中断时,在usertrap
函数中有相关处理(即在if(which_dev == 2)
处,我们累加当前进程的tita字段即可,且如果当前tita已经为ticks,则通过修改trapframe的epc字段来调用handler函数。(注意题目规定如果ticks为0则不要进行处理)
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->ticks != 0) {
p->tita ++;
if (p->tita == p->ticks) {
p->tita = 0;
p->trapframe->epc = (uint64)p->handler;
}
}
yield();
}
test1/test2(): resume interrupted code
在通过test0之后,需要考虑的是在运行完处理函数handler之后,继续运行原始程序,文档为我们提供了一种解决方案,即通过系统调用sigreturn
来恢复原始trapframe。
由于在handler处理过程中,进程的trapframe可能发生改变(其实这里对于什么时候发生改变还不是很清晰),于是在改变trapframe中的epc为handler之前,先将trapframe缓存一下,在进程结构体中添加一个字段即可。
struct trapframe *copyframe;
并且在进程分配时初始化该字段,在进程销毁时释放该字段,像trapframe字段一样即可。
// allocproc
if((p->copyframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
// freeproc
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
还要注意的是,文档要求如果当前handler函数如果还在调用中,那么不需要重复调用。于是需要加一个字段表示当前是否正在调用中。
int calling;
于是,在usertrap函数中,如果当前CPU时钟中断次数到达阈值,则缓存trapframe并设置calling为1和trapframe的epc以调用handler。
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if (p->ticks != 0) {
p->tita ++;
if (p->tita == p->ticks) {
p->tita = 0;
if (p->calling == 0) {
memmove(p->copyframe, p->trapframe, sizeof (struct trapframe));
p->trapframe->epc = (uint64)p->handler;
p->calling = 1;
}
}
}
yield();
}
在系统调用sigreturn
中,则需要恢复缓存下来的trapframe,并设置calling为0.
uint64
sys_sigreturn(void)
{
struct proc* p = myproc();
memmove(p->trapframe, p->copyframe, sizeof (struct trapframe));
p->calling = 0;
return 0;
}