目录
1.RISC-V assenbly
2.Backtrace
3.Alarm
1.RISC-V assenbly
第一个任务是阅读理解,一共有6个问题。
1.Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
具体来说就是a0,a1几个寄存器存了相应的系统调用参数,a7存的是系统调用号。
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.)
这个好像汇编代码直接内联了。
3.At what address is the function printf located?
这里很明显了,跳到的地址是1552+ra=1552+0x30=0x640
4.What value is in the register ra just after the jalr to printf in main?
这个auipc噢,用那个跳转的16进制地址减去1552就可以算出来,实际上就是存的0x097当前的地址,这个auipc就是把当前pc和后面那个立即数的低12位取0相加得到的。
jalr会在跳转之后把ra加8,也就是ra+8=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 change57616
to a different value?
这考察大端小端的问题,第一个就是10进制转8进制,第二个是小端的输出,输出726c64
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);
这里输出的就是寄存器a1里边的值,是什么值就是啥。
2.Backtrace
实现一个系统调用,用来打印调用的栈的系统信息的。据说对debug有用,虽然我还没想到哪里可以用。
首先我们得熟悉一下系统里边调用的层次结构:
我们来看一下这张图,从上往下看,高地址在高位,但是栈是向低地址增长的。首先呢是存个return address,也就是返回执行的地址,然后存的就是prev frame辣,这就是我们要打印的东西,xv6还提示我们可以用一条内联汇编去取出当前的栈帧指针,这个指针是指向那个return address的上一个位置的,意味着我们可以用-8,-16取到返回地址以及上一个栈帧的指针,那么我们写个循环就好了对吧。
那么循环条件是什么?我们在给一个程序分配栈时,按照页为单位,其中每个页面之间有个保护页面。
Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address.
意思就是我们的栈桢指针应该是指向一个页的顶部的,所以当他不等于的时候就是循环结束的条件。
void
backtrace(void)
{printf("backtrace:\n");// fp保存当前栈帧指针uint64 fp=r_fp();while(fp!=PGROUNDUP(fp)){uint64 ret_addr=*(uint64 *)(fp-8);printf("%p\n", ret_addr);fp=*(uint64 *)(fp-16);}
}
最后在sys_sleep里边backtrace一下就行了,当然,系统调用号以及入口一些细节按照xv6里边来就行了。
执行bttest如下所示:
3.Alarm
这里的任务就是加个系统调用来处理信号。这个信号处理流程大概是这样的:
sigalarm(time,handler),设置每隔time的ticks执行handler,这个handler是用户态的函数,在函数末尾我们要用sigreturn返回。
其实这里有个东西我想了挺久的,如果说在进入trap之前已经存下来了所有寄存器的话,为什么还要用sigreturn处理用户的返回呢?这个其实是因为,首先啊,在time到的时候,我们为了要返回用户态执行handler,我们要设置返回的trapframe是handler的地址,然后呢,如果没有sigreturn会怎么样?trap返回然后恢复寄存器,handler继续执行,然后返回到原来中断的函数里边执行,那么,原来的context其实可能已经打乱了,所以,我们得用一个sigreturn来恢复sigalarm之前的上下文。
具体思路及步骤如下所示:
1.首先还是处理系统调用号以及一些入口
2.在sysproc.c里边写这两个函数:
uint64
sys_sigalarm(void)
{int ticks=0;uint64 handler=0;struct proc *p=myproc();if (argint(0, &ticks)<0){return -1;}if (argaddr(1, &handler)<0){return -1;}p->ticks=ticks;p->has_return=1;p->pass_ticks=0;p->handler=(void (*)())handler;// printf("In sys_sigalarm pass_ticks:%d ticks:%d epc:%p\n",p->pass_ticks,p->ticks,p->trapframe->epc);return 0;
}uint64
sys_sigreturn(void)
{struct proc *p=myproc();memmove(p->trapframe,p->signal_trapframe,sizeof(struct trapframe));p->pass_ticks=0;p->has_return=1;// printf("In sys_sigreturn pass_ticks:%d ticks:%d epc:%p\n",p->pass_ticks,p->ticks,p->trapframe->epc);return 0;
}
sigalarm接收时间戳和handler两个参数,并且添加到进程的数据结构里面。
sigreturn重新设置时间戳,并且标记目前正在执行handler,试着想一下,如果一个handler要执行很久,那么我们在handler里面又会handler,这是我们不期望的,也就是说,当执行handler时,就一直执行完而屏蔽sigalarm设置的handler。
3.在trap.c里边修改如下:
// give up the CPU if this is a timer interrupt.if(which_dev == 2){// printf("In trap pass_ticks:%d ticks:%d epc:%p\n",p->pass_ticks,p->ticks,p->trapframe->epc);if (p->ticks==0){yield();usertrapret();}else{int pass_ticks=p->pass_ticks+1;if (pass_ticks>=p->ticks&&p->has_return==1){// 记录时钟中断前上下文p->signal_trapframe=(struct trapframe*)kalloc();memmove(p->signal_trapframe,p->trapframe,sizeof(struct trapframe));// 调整返回执行地址p->trapframe->epc=(uint64)p->handler;p->has_return=0;}else{p->pass_ticks=pass_ticks;}}yield();}
这里的逻辑就是应该已经很清晰了~
执行alarmtest如下所示:
这个实验通过backtrace以及一些信号系统调用让我们理解了系统调用的流程以及一些汇编指令的作用,当然,收获最大的是这个信号处理流程。