2020 6.s081——Lab4:traps

我会等冬的雪融化 

蒲扇里的夏

守着深夜里的星星

眨眼不说话

——我会等

完整代码见:SnowLegend-star/6.s081 at traps (github.com)

RISC-V assembly (easy)

Q1:Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?

A: 用户代码将exec的参数放在寄存器a0和a1中,并将系统调用放在a7中。当系统调用返回时,其返回值记录在p->trapframe->a0中。
a7存放编号为13的系统调用。

Q2: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.)

A:    汇编代码main函数中没有找到调用f的地方,应该是被内联取代函数调用了,直接设置为12。


Q3:At what address is the function printf located?

A:   printf位于“0x630”处。

Q4:What value is in the register ra just after the jalr to printf in main?

A:   ra 通常指的是 RISC-V 架构中的返回地址寄存器,用于存储函数调用后的返回地址。所以这里ra的值应为“0x38”。


Q5:Run the following code.

    unsigned int i = 0x00646c72;
    printf("H%x Wo%s", 57616, &i);

Q5.1:What is the output? Here's an ASCII table that maps bytes to characters. 

A:      输出是“Hello World”。

Q5.2: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?

A:   因为xv6是小端存储,所以i在内存中实际的存放值应该是“0x726c6400”。同时,系统从地址低位往地址高为打印字符,这恰好对应“rld”。如果机器是大端存储(更符合日常书写习惯),则需要把i改为“0x726c6400”。
       而我们不需要修改 57616 的值,因为它是以十六进制形式打印的,并且十六进制的表示方式不受字节序影响。无论系统的字节序如何,57616 在十六进制中表示的值始终是 0xe110。

Here's a description of little- and big-endian and a more whimsical description.

Q6: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: x=3 y=-80204024。
       y的值从内存残留的值里面进行读取。

Backtrace (moderate)

果然moderate难度还是令人舒适,有点难度但不多。这个lab的主旨就是一句话——“用栈指针打印每个函数调用的返回地址”。 下面我们分析下hints:

1、Add the prototype for backtrace to kernel/defs.h so that you can invoke backtrace in sys_sleep.

让我们在defs.h中添加backtrace的函数名,都是老生常谈了。 

2、The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:

static inline uint64

r_fp()

{

  uint64 x;

  asm volatile("mv %0, s0" : "=r" (x) );

  return x;

}

and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.

GCC 编译器将当前正在执行的函数的帧指针存储在寄存器 s0 ”这句话还是比较关键的。怪不得对alarm部分进行debug的时候总感觉s0应该不是拿来存变量,而是存与函数执行有关的地址。原来前文已经说过了。 

3、These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.

 

Lecture055的这张图太重要了,建议狠狠背下来。根据这张图也可以看出来ra位于fp的-8偏移处,而saved fp(即caller函数的fp)位于当前fp的-16偏移处。而且根据图来看caller函数和callee函数的栈帧貌似是有排列规律的,我在CSDN看到了这样一句话“一个 Stack Frame 称为一个函数栈空间,每一个函数均有自己独立的 Stack Frame,并且调用链之间的 Stack Frame 是连续的”问了下GPT确实如此:

连续的栈帧:当一个函数调用另一个函数时,被调用函数的栈帧会被压入栈中,形成一个新的栈帧。这些栈帧是连续的,因为它们按照调用顺序依次存放在栈上。也就是说,每一个新的栈帧都位于上一个栈帧的下方,形成了一个函数调用链。

 

4、Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using PGROUNDDOWN(fp) and PGROUNDUP(fp) (see kernel/riscv.h. These number are helpful for backtrace to terminate its loop.

本实验要实现一个内核函数 backtrace,该函数用来回溯调用者的函数调用链,将所有的调用者的 pc 打印出来,也就子函数的 ra。只需要从当前 Stack Frame 开始,通过 Prev 找到调用者的 Stack Frame,直至调用链结束为止。

接下来,在 kernel/printf.c 中实现 backtrace,调用 r_fp 来获得栈指针 fp,打印出 *(fp - 8),然后跳转到 *(fp - 16) 即可,以此循环。那么问题来了,循环结束点(最后一个 Stack Frame)在哪呢?

整个栈空间是有个范围的,所有的函数的 Stack Frame 均在其中,并且每一个栈指针都是 4k 对齐的,因此如果 fp 不是 4k 地址对齐,那么就说明超过范围了。

换句话说,在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。

r_fp()实现如下

//实现backtrace
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

bacetrace()如下
 

//通过栈指针来追踪函数调用的层次结构
//打印每个函数调用的返回地址
void backtrace(){
  uint64 fp=r_fp();
  uint64 return_address;
  while(fp!=0 && fp> PGROUNDDOWN(fp) && fp<PGROUNDUP(fp)){
    return_address=*(uint64*)(fp-8);    
    printf("%p\n",return_address);
    fp=*(uint64*)(fp-16);     //返回到上一个调用栈的栈帧中
  }
}

Alarm (hard)

这个部分需要添加的代码并不算多,主要还是理清楚函数调用和中断处理。本 lab 的直接要求时实现 sigalarm,什么意思呢?就是当进程调用 sigalarm 时,就会按照 CPU 时钟来定时的执行某一个函数。比如,test0调用了 sigalarm(2, func) 那该进程就会每隔 2 CPU 时钟调用一次 func。这里sigalarm类似于CSAPP的信号机制,而不是说test0中只调用了一次sigalarm( 2 , func ),那就只会执行一次func,而是每次中断都会执行func。一共分为 test0、test1、test2、test3,又浅入深的逐步实现该操作。

test0: invoke handler

还是从hitns入手:

1、You'll need to modify the Makefile to cause alarmtest.c to be compiled as an xv6 user program.

2、The right declarations to put in user/user.h are:

int sigalarm(int ticks, void (*handler)());

int sigreturn(void);

3. Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest to invoke the sigalarm and sigreturn system calls.

上面几个步骤类似的已经完成好多遍了,故不再赘述。

4、For now, your sys_sigreturn should just return zero.

test0的主要目的是更改中断结束后返回的地址。原本中断结束后,会返回到中断指令的下一条指令。而sigalarm( n, fn )则是让中断返回到函数fn处。但是本阶段还不涉及sigreturn的更改。

5、Your sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h).

这里提到我们要在sys_sigalarm()内部把用户空间调用的n和fn这两个参数接收过来。当然,先要为每个进程添加time_interval和handler_function两个参数。

6、You'll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process's alarm handler; you'll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.

这个hint说要给进程添加一个新的参数ticks_passed,以此来记录从上一次调用handler_function已经过去了多少个ticks。在allocproc()可以进行ticks_passed的初始化,其实我感觉在sys_sigalarm内部也可以就进行这个初始化。有一点我是比较费解的,要在哪里进程ticks_passed++的操作呢?

7、Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.

这个提示就能较好地回答上面那个问题。每次tick时,都会产生一个时钟中断,所以可以倒推当发生中断时,就已经过了一个ticks。其实不够严谨,因为有三种情况都可以产生中断

  • 系统调用;
  • 程序出现了类似page fault、运算时除以0的错误,即 panic;
  • 一个设备触发了中断使得当前程序运行需要响应内核设备驱动;

hint8会解释这一点。所以,我们只需要在usertrap()内部找个地方就可以添加ticks_passed++的操作了。

8、You only want to manipulate a process's alarm ticks if there's a timer interrupt; you want something like

    if(which_dev == 2) ...

这里提到,只有进行timer interrupt的时候才会处理ticks,所以ticks_passed++的位置就应该放在if(which_dev==2)这个判断的内部。

9、Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user's alarm function might be 0 (e.g., in user/alarmtest.asm, periodic is at address 0).

 

我感觉这个hint好像没怎么用到,描述也是含糊其辞。

10、You'll need to modify usertrap() so that when a process's alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

我们知道,系统是通过trapframe内部的epc寄存器来存储ra的。所以我把epc寄存器的值改为handler_function的地址就可以了。

11、It will be easier to look at traps with gdb if you tell qemu to use only one CPU, which you can do by running

 make CPUS=1 qemu-gdb

这个hint也没怎么用到。在以后的多线程调试的时候可能会用到

其实test0并不困难,但我还是被test0卡了好久,一直报下列错误

最后发现居然是下列问题。当真是对函数调用流程的理解不能有一点含糊啊。

test1/test2(): resume interrupted code

什么时候会发生中断?

  1. 在for循环执行时被中断
  2. 在调用foo程序时候中断
  3. 在调用periodic时中断(几乎不可能,两次时钟中断肯定会执行完这部分代码)

Hints分析如下:

1、Your solution will require you to save and restore registers---what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).

test0的作用是让中断结束后返回到handl_function()继续执行,而这个部分则是让函数调用变得完整,可以顺利返回到导致中断的指令的下一句指令。我们需要维护好几个寄存器。

2、Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.

大致流程如下:假设在x指令出发生了中断,而sigalarm将epc返回到periodic中,periodic中又声明了sigreturn这个信号处理函数,借助sigreturn又可以返回到x的下一条指令中继续执行。相当于绕一圈再回到原地。

我们看这个图,可以发现callee函数要保存的寄存器是ra、sp、s0~s11。具体要保存哪些寄存器的值还要仔细分析哪些寄存器被改变了。还有个epc寄存器也要维护。

直接运行test0的代码时,会发现j的值一直是0。这就需要我们修改代码了。

首先保存一个新的epc值,因为epc被sigalarm修改指向了handler_function。我们要令epc最后指向原本应返回的地址epc+4。

接下来是看test1中是否还有其他函数调用了ra、sp、s0~s11

foo():涉及s0、s1、sp、ra

periodic():涉及sp、ra、s0

所以我们初步维护上面四个寄存器的值试试。但是sigreturn的作用是什么呢?

带着这个问题,我们在usertrap中维护上述寄存器后,在哪儿把这些保存好的寄存器的值写入到被改变的reg中呢?就是在sigreturn中。逻辑自洽了。

尝试运行下代码,发现i、j的值已经相差很近了:

有个搞笑的事儿,我连着运行了几次alarmtest发现i、j的值越来越近,就想着能不能投机取巧。结果下一次运行结果还是让我的侥幸幻想破灭了,可恶

言归正传,现在开始考虑是不是要维护涉及存储和运算ij的寄存器了。再次检查test1中涉及i、j的汇编代码,同时foo()函数也要重点关注。

test1():a1存j的值,a0存i的值

foo():a5与s1参与j的运算

故把上述寄存器也进行维护,结果可以通过test1,但是没解决重入性的问题。

3、Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again. test2 tests this.

这里我一开始有点没理解。后来发现是当一个中断的handler_function没被执行完时,不可以再被调用。相当于加了把锁。可以给进程添加一个flag,在sigreturn和usertrap中完成flag的互斥操作。

但是不能再sigalarm中初始化flag,因为sigalarm是可重入的。我们用flag的目的是为了维护好count的值,而sigalarm是中断一开始就调用的,sigalarm调用结束后才返回到periodic进行count++。我们要在count++彻底完成后才解开flag设置的锁,故在sigreturn初始化flag。

proc.h的修改如下

struct proc {
  struct spinlock lock;

  // p->lock must be held when using these:
  enum procstate state;        // Process state
  struct proc *parent;         // Parent process
  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

  // 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 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 time_interval;                    //alarm interval
  void (*handler_function)();           //指向处理函数的指针
  int ticks_passed;                     //上次调用警报处理程序(alarm handler)以来经过了多少个 tick

  struct trapframe *new_trapframe;      //建立一个新的栈帧来维护寄存器的值
  uint64 re_epc;                        //原本应该返回到epc+4
  uint64 s0;
  uint64 s1;
  uint64 sp;
  uint64 ra;
  uint64 a1,a0,a5;                   //test1和foo汇编代码中涉及计算i,j的部分
  int flag;                          //flag=0的时候说明可以调用handler_function   完成可重入性检查
};

usertrap()修改如下
 

  if(p->killed)
    exit(-1);

  // give up the CPU if this is a timer interrupt.
  // if(which_dev == 2)
  //   yield();

  //记录ticks_passed
  if(which_dev==2){
    //如果是timer interrupt才处理进程的alarmtest
    // printf("The ticks_passed is: %d\n",p->ticks_passed);
    p->ticks_passed++;
    // if(p->time_interval==p->ticks_passed)
    //   printf("The handler_function address from user space is: %p\n",p->handler_function);
    if(p->time_interval==p->ticks_passed && p->flag==0){
      // sys_sigalarm();      //西巴!问题原来出在这里

      p->re_epc=p->trapframe->epc;      //存储原本应该返回到的地址(ecall的后一句指令)
      p->s0=p->trapframe->s0;
      p->s1=p->trapframe->s1;
      p->ra=p->trapframe->ra;
      p->sp=p->trapframe->sp;
      p->ticks_passed=0;
      p->a1=p->trapframe->a1;
      p->a0=p->trapframe->a0;
      p->a5=p->trapframe->a5;


      p->trapframe->epc=(uint64)(p->handler_function);

      p->flag=1;
    }
    yield();
  }

  usertrapret();

 sys_sigalarm()如下

uint64
sys_sigalarm(void){
  struct proc *p=myproc();
  uint64 handler_function;
  if(argint(0,&(p->time_interval))<0)
    return -1;
  if(argaddr(1,&handler_function)<0)
    return -1;
  if((p->time_interval)==0 && handler_function==0)
    return -1;
  p->handler_function=(void*)handler_function; 
  p->ticks_passed=0;
  // p->ticks_passed=sys_uptime();
  printf("The time interval from user space is: %d\n",p->time_interval);
  printf("The handler_function address from user space is: %p\n",p->handler_function);
  return 0;
}

 sys_sigreturn()如下

uint64
sys_sigreturn(void){
  struct proc *p=myproc();
  p->trapframe->epc=p->re_epc;      //存储原本应该返回到的地址(ecall的后一句指令)
  p->trapframe->s0=p->s0;
  p->trapframe->s1=p->s1;
  p->trapframe->ra=p->ra;
  p->trapframe->sp=p->sp;  
  p->trapframe->a1=p->a1;
  p->trapframe->a0=p->a0;
  p->trapframe->a5=p->a5;

  p->flag=0;
  return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值