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 function f in the assembly code for main? Where is the call to g? (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 register ra just after the jalr to printf in main?

把运行到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

这个实验的要求还是比较复杂的,做之前一定要看一下视频

先梳理一下用户进程进入内核态的过程;

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的操作。

posted @ 2021-11-19 22:42  LunaCancer  阅读(873)  评论(2编辑  收藏  举报