RISC-V assembly (easy)
这部分没有代码实现,只需要查看对照call.c
查看call.asm
的内容,回答几个问题即可。
- Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
函数参数通过寄存器a0-a7(整数类型,指针)和寄存器f0-f7(浮点数)传递。
寄存器a2保存了main调用printf时的第三个参数的值13。 - 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,在
26: 45b1 li a1,12
处将立即数12传入了a1。
同样的对g也没有调用,在编译期直接计算好了结果。 - At what address is the function printf located?
printf的地址在0x616。 - What value is in the register ra just after the jalr to printf in main?
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.
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?
Here’s a description of little- and big-endian and a more whimsical description.
程序会输出 ‘HE110 World’
如果是大端序的机器,则需要将i设置为 0x726c6400,这是因为
0x72=‘r’
0x6c=‘l’
0x64=‘d’
0x00=‘\0’
57616不需要换成其他数字,不管在大端序还是小端序中,它的十六进制都是0xE110。
是
- 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);
y=后面输出的值取决于调用printf时寄存器a2的值。
Backtrace (moderate)
为了实现打调用栈上函数的地址的功能,需要用到寄存器fp的值,寄存器fp的值指向的是当前函数栈帧的栈底,实验指导已经提供了一个读取fp值的函数r_fp
。将它放到riscv.h中。
// riscv.h
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
从下图可以看到,对于每一个函数栈帧,地址(fp - 8)指向的是当前函数的返回值地址,也就是上一个函数的地址,这就是我们需要打印的东西。(fp - 16)处则是上一个栈帧的栈底的地址。所以读取当前fp的值后,可以类似于遍历链表那样遍历当前的调用栈。由于xv6中的栈大小只有一个page。所以最底层的栈帧的栈底不会超过PGROUNDUP(fp)
。
于是backtrace实现如下
// printf.c
void
backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 limit = PGROUNDUP(fp);
while (fp <= limit) {
printf("%p\n", *(uint64 *)(fp - 8));
fp = *(uint64 *)(fp - 16);
}
}
然后将它的声明加入到def.h中,在sys_sleep
函数中调用,这个实验就可以通过了。
// sys_proc.c
uint64
sys_sleep(void)
{
int n;
uint ticks0;
// 随便找个地方调用,比如这里
backtrace();
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}
backtrace
能够正常工作后,我们还可以将其将其放到panic
中,这样xv6出现panic时就会打印出当前的调用链信息方便debug(虽然用gdb就可以了,不过实验指导部分让加上就加上吧)。
Alarm (hard)
这部分要求实现两个系统调用:sigalarm
和sigreturn
。添加系统调用的过程这里不赘述。
sigalarm接受两个参数:interval和handler。用户进程调用sigalarm后,在进程执行时每发生interval个时间中断,执行流就应该跳转到handler去执行。
用户进程调用sigalarm
时,sigalarm
应该做两件事情,一是把自己的参数保存在某个地方,以便将来在发生interval
次时钟中断后执行流能够找到handler
,所以首先需要在struct proc
结构体中添加对应的字段(增加的字段的初始化如无特殊说明都是在allocproc
中被置0)。
// proc.h
struct sigalarm_param {
int ticks;
uint64 handler;
};
struct proc {
// ...
// these are private to the process, so p->lock need not be held.
struct sigalarm_param sigalarm_param;
int lefttick;
struct trapframe sigreturn_context;
int sigalarm_isrunning;
// ...
};
在执行sys_sigalrm
时,将参数保存到sigalarm_parm
结构体中,同时将lefttick
置为ticks
,表示还有lefttick
次时钟中断就该执行handler
了。
// sysproc.c
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->sigalarm_param.ticks = ticks;
p->sigalarm_param.handler = handler;
p->lefttick = ticks;
return 0;
}
用户进程执行时,如果发生时间中断,执行流会进入到usertrap
函数中,在这里判断发生trap的原因,如果是时钟中断,再让出CPU之前,需要判断此时是否该执行handler
。要让进程在返回
用户态时执行handler
,只需要将进程的trapframe中epc的值设为handler的地址即可。同时将lefttick
重新设为最开始的值。为了让handleir在执行完后能够正确回到此时用户进程的地方,需要将当前的trapframe保存到sigreturn_context
中。为了防止在handler返回前再被内核调用,设置p->sigalarm_isruning
表示handler
还在执行。
// trap.c
void
usertrap(void)
{
// ...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if (p->sigalarm_param.ticks != 0) {
p->lefttick--;
if (p->lefttick == 0 && p->sigalarm_isrunning == 0) {
memmove(&p->sigreturn_context, p->trapframe,
sizeof(p->sigreturn_context));
p->trapframe->epc = p->sigalarm_param.handler;
p->lefttick = p->sigalarm_param.ticks;
p->sigalarm_isrunning = 1;
}
}
yield();
}
usertrapret();
}
sigreturn
在handler
末尾被调用,它在内核中的实现如下:
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
memmove(p->trapframe, &p->sigreturn_context, sizeof(p->sigreturn_context));
p->sigalarm_isrunning = 0;
return 0;
}
它做的事情很简单,只是在返回用户态时将trapframe的状态恢复到执行handler之前的状态。并将sigalarm_isrunning
置零表示handler
执行完了。
测试一下效果,执行alarmtest
再执行下usertests
lab trap部分的实验就结束了。