..

[Lab Report] MIT 6.S081 Lab: traps (v2022)


RISC-V assembly

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

Here are the contents of main and its corresponding assembly code:

// call.c
void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}
# call.asm
void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7d850513          	addi	a0,a0,2008 # 800 <malloc+0xe8>
  30:	00000097          	auipc	ra,0x0
  34:	62a080e7          	jalr	1578(ra) # 65a <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	298080e7          	jalr	664(ra) # 2d2 <exit>

a0, a1…, a7 contain arguments to functions. In this case, it is a2 that holds 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.)

Here are the contents of f, g and their corresponding assembly code:

// call.c
int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}
# call.asm
int g(int x) {
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
}
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

int f(int x) {
   e:	1141                	addi	sp,sp,-16
  10:	e422                	sd	s0,8(sp)
  12:	0800                	addi	s0,sp,16
  return g(x);
}
  14:	250d                	addiw	a0,a0,3
  16:	6422                	ld	s0,8(sp)
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

From above we can tell that the compiler directly calculates the result of “f(8)+1=12” and stores it in a1.

At what address is the function printf located?

000000000000065a <printf>:

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

According to the RISC-V manual, the indirect jump instruction jalr (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then setting the least-significant bit of the result to zero. The address of the instruction following the jump (pc+4) is written to register rd. Register x0 can be used as the destination if the result is not required.

Specifically, the instruction jalr rd, offset(rs1) will store the value of pc+4 in rd. If rd is ra instead, the instruction essentially becomes a function call. Otherwise, it is just a simple jump. The supported range is within ±2KB (-2048 ~ 2047) based on rs1. By combining the high 20 bits of auipc with the low 12 bits of jalr, all functions within the 32-bit range of pc can be called.

  34:	62a080e7          	jalr	1578(ra) # 65a <printf>
  exit(0);
  38:	4501                	li	a0,0

So in this case the value in ra after jalr to printf is 0x38.

Run the following code.

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

What is the output?

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?

The output is: He110 World. If the RISC-V is big-endian, setting i as dlr would yield the same output. And since 57616 will always be 0xe110 no matter the RISC-V is little-endian or big-endian, we don’t need to change 57616 to a different value.

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);

The printed value for y is undefined. Before calling printf, the compiler will place the input parameters into registers a0-a7. When jumping to printf, the values are then read from the registers. However, since we did not specify a value for the second placeholder, a2 was not modified. Therefore, the value of y printed out is an arbitrary value that was present in a2 before the function call.

Backtrace

The task becomes trivial following the hints:

  1. Add the prototype for backtrace() to kernel/defs.h.

    // printf.c
    void            printf(char*, ...);
    void            panic(char*) __attribute__((noreturn));
    void            printfinit(void);
    void            backtrace(void);
    
  2. The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add r_fp() to kernel/riscv.h:

    ...
    static inline uint64
    r_ra()
    {
      uint64 x;
      asm volatile("mv %0, ra" : "=r" (x) );
      return x;
    }
       
    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
       
    // flush the TLB.
    ...
    
  3.  Layout of stack frames
                        .
                        .
           +->          .
           |   +-----------------+   |
           |   | return address  |   |
           |   |   previous fp ------+
           |   | saved registers |
           |   | local variables |
           |   |       ...       | <-+
           |   +-----------------+   |
           |   | return address  |   |
           +------ previous fp   |   |
               | saved registers |   |
               | local variables |   |
           +-> |       ...       |   |
           |   +-----------------+   |
           |   | return address  |   |
           |   |   previous fp ------+
           |   | saved registers |
           |   | local variables |
           |   |       ...       | <-+
           |   +-----------------+   |
           |   | return address  |   |
           +------ previous fp   |   |
               | saved registers |   |
               | local variables |   |
       $fp --> |       ...       |   |
               +-----------------+   |
               | return address  |   |
               |   previous fp ------+
               | saved registers |
       $sp --> | local variables |
               +-----------------+
    
    • $sp defines the end of the current stack frame, while $fp defines the beginning of the current stack frame (which is also the end of the last stack frame). Specifically, $fp contains the value of $sp just before the current function was called.
    • The stack grows downward from high addresses to low addresses, so although $fp is the starting address of the frame, its address is higher than that of $sp.

    Implement backtrace() in kernel/printf.c:

    ...
    void
    backtrace(void)
    {
      uint64 fp = r_fp();
      while (fp != PGROUNDDOWN(fp)) { // PGROUNDDOWN(fp) always indicates the starting position of the page where fp is located
        uint64 r_addr = *(uint64*)(fp - 8); // the return address lives at a fixed offset (-8) from the frame pointer of a stackframe
        printf("%p\n", r_addr);
        fp = *(uint64*)(fp - 16); // the saved frame pointer lives at fixed offset (-16) from the frame pointer
      }
    }
    

Alarm

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

We start with registering the system calls sigalarm and sigreturn following the prescribed steps, which will not be elaborated further.

We will need the following entries in the proc structure (in kernel/proc.h):

struct proc {
  ...
  // store the alarm interval and the pointer to the handler function
  int alarminterval;
  void (*alarmhandler)();
  // keep track of how many ticks have passed since the last call
  int passedticks;
  // save registers so that we can restore the states upon return
  struct trapframe *savedtrapframe;
  // prevent re-entrant calls to the handler
  int caninvoke;
};

Initialize and free these entries in kernel/proc.c accordingly:

...
static struct proc*
allocproc(void)
{
  ...

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // Allocate a savedtrapframe page.
  if((p->savedtrapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  p->alarminterval = 0;
  p->alarmhandler = 0;
  p->passedticks = 0;
  p->caninvoke = 1;

  // An empty user page table.
  ...
}

...    
static void
freeproc(struct proc *p)
{
  ...
  p->trapframe = 0;
  if(p->savedtrapframe)
    kfree((void*)p->savedtrapframe);
  p->savedtrapframe = 0;
  if(p->pagetable)
  ...
  p->alarminterval = 0;
  p->alarmhandler = 0;
  p->passedticks = 0;
  p->caninvoke = 1;
}
...

Now we can implement sys_sigalarm and sys_sigreturn in kernel/sysproc.c:

...
uint64
sys_sigalarm(void)
{
  int interval;
  uint64 handler;
  argint(0, &interval);
  argaddr(1, &handler);

  struct proc *p = myproc();
  p->alarminterval = interval;
  p->alarmhandler = (void(*)())(handler);
  p->passedticks = 0;
  return 0;
}

uint64
sys_sigreturn(void)
{
  struct proc *p = myproc();
  *p->trapframe = *p->savedtrapframe;
  p->caninvoke = 1;
  return p->trapframe->a0; // sigreturn is a system call, and its return value is stored in a0
}

Then, modify the usertrap function in kernel/trap.c and we are done:

void
usertrap(void)
{
  ...

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

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if(p->alarminterval > 0 && p->caninvoke) {
      if(++p->passedticks == p->alarminterval) {
        p->passedticks = 0;
        *p->savedtrapframe = *p->trapframe; // save all the registers before jumping to the handler
        p->trapframe->epc = (uint64)p->alarmhandler;
        p->caninvoke = 0;
      }
    }
  }

  usertrapret();
}

For some reason, usertests -q will hang on test preempt on my machine. The source code for this test is provided below:

// meant to be run w/ at most two CPUs
void
preempt(void)
{
  int pid1, pid2, pid3;
  int pfds[2];

  printf(1, "preempt: ");
  pid1 = fork();
  if(pid1 == 0)
    for(;;)
      ;

  pid2 = fork();
  if(pid2 == 0)
    for(;;)
      ;

  pipe(pfds);
  pid3 = fork();
  if(pid3 == 0){
    close(pfds[0]);
    if(write(pfds[1], "x", 1) != 1)
      printf(1, "preempt write error");
    close(pfds[1]);
    for(;;)
      ;
  }

  close(pfds[1]);
  if(read(pfds[0], buf, sizeof(buf)) != 1){
    printf(1, "preempt read error");
    return;
  }
  close(pfds[0]);
  printf(1, "kill... ");
  kill(pid1);
  kill(pid2);
  kill(pid3);
  printf(1, "wait... ");
  wait();
  wait();
  wait();
  printf(1, "preempt ok\n");
}

I’m uncertain whether this issue is due to a kernel malfunction or simply a result of the system running too slowly. If you have experience with this particular problem as well and have been able to identify its cause, I would greatly appreciate hearing about your findings.