XV6 Lab 4: Traps

notes of MIT6.S081 Lab 4

Page content

RISC-V assembly

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

    Register: a0, a1, a2…, a7 for integer arguments. Register fa0, fa1, fa2…, fa7 for float arguments.

    Register a2 holds 13 when we call printf().

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

    Compiler inlined f(8) and g() in printf() function. And in assembly code it just pass a immediate 12 to register a1.

    Howerver, we can set -O0 to disable advanced optimization and get the call address of function f and g.

  3. At what address is the function printf located?

    The auipc instruction get the PC address and load it to register ra. jalr instruction will combine 1562(0x61A) to 0x30 to get the final result.

    0x30 is 0011 0000 0000 0000 0000

    0x61A is 0110 0001 1010

    And the final result is 0x64A

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

    The PC will reset to the main function(the address called printf() + 1) after printf returned. ra = 0x38.

  5. Output is He110 World, there is no need to change var i when risc-v processors set to big-endian.

  6. The x=3, and y depends on the register setting. (Mostly like a random number).

Backtrace

As mentioned in the question, the compiler will set the frame pointer to register s0. We need to add a piece of code in kernel/riscv.h to get the value of s0.

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

Form hints, we knows that:

  1. 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.

    These lecture nots have a picture of layouts of stack frames.

        Stack
                     .
                     .
        +->          .
        |   +-----------------+   |
        |   | 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 |
            +-----------------+
    

    In that case, if we want to get the live address, we need to add -8 to s0. To get the previous fp, add -16 to s0;

    The fp is the top of one frame.

  2. You can use PGROUNDDOWN(fp) (see kernel/riscv.h) to identify the page that a frame pointer refers to. But what we need to use is PGROUNDUP(fp) here.

In ./kernel/printf.c, we neded to impl backtrace function:

void
backtrace() {
    uint64 fp = r_fp();
    uint64 bound_high = PGROUNDUP(fp); // the page frame pointer ref to.
    while(fp < bound_high) {
        uint64 tmp = *(uint64*)(fp - 8);
        fp = *(uint64*)(fp - 16);
        printf("%p\n", tmp);
    }
    return;
}

Alarm

In this exercise we need to add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action.

We should add a new sigalarm(interval, handler) system call. If an application calls sigalarm(n, fn), then after every n “ticks” of CPU time that the program consumes, the kernel should cause application function fn to be called. When fn returns, the application should resume where it left off. A tick is a fairly arbitrary unit of time in xv6, determined by how often a hardware timer generates interrupts. If an application calls sigalarm(0, 0), the kernel should stop generating periodic alarm calls.

test 0

In oerder to let system knows what function need to execute when a process’s alarm interval expires, we need to add some structs to proc. First, we need to store the alarm interval and functions pointer. Second, we need to backup the trapframe for user/kernel switch.

The figure shown below illuminate the workflow of sigalarm and sigreturn:


Fig 1. workflow

In ./kernel/proc.h, we need to add some infomation to let process know the state of current sigalarm/sigreturn.

struct proc {
    ...
    int alarm_interval; // n ticks per action
    int alarm_ticks_left; // how many times has timer intrupt
    void (*alarm_handler)(); // the function pointer for exec
    struct trapframe *trapframe_bk; // backup, for user/trap switch
    ...
}

Also, we need to modified alloc and free functions for process. In ./kernel/proc.c:

static void
allocproc(void) {
    ...
    if ((p->trapframe_bk = (struct trapframe *)kalloc()) == 0) {
        freeproc(p);
        release(&p->lock);
        return 0;
    }
    ...
    p->alarm_interval = 0;
    p->alarm_handler = 0;
    p->alarm_ticks_left = 0;
    ...
}
static void
freeproc(struct proc* p) {
    ...
    if(p->trapframe_bk)
        kfree((void*)p->trapframe_bk);
    ...
    p->alarm_interval = 0;
    p->alarm_handler = 0;
    p->alarm_ticks_left = 0;
}

And we need to register this two system-call’s definitions in the ./user/usys.pl to let it generate the trap entries(it’s exactly same as previous lab).

Then we need to handle the timer interupt. In ./kernel/trap.c, we need to impl the logic when which_dev==2. First, we need to judge if alarm_interval is zero or not. Then, increase the alarm_ticks_left and compare it with alarm_interval. If timer interupts times equles to n tick, execute the alarm_handler user passed in.

Note that, if we want to execute the alarm_handler, the pc should point to code section in user process. So, we need to figure the epc register manually. When executing alarm_handler, registers may changed. So we need to back up current trapframe before calling alarm_handler. After alarm_handler returned, we can use this backuped trapframe to restore the register state.

In ./kernel/trap.c:

void
usertrap(void) {
    ...
    // give up the CPU if this is a timer interrupt.
    if(which_dev == 2) {
        if (p->alarm_interval) {
        if (++p->alarm_ticks_left == p->alarm_interval) {
            *p->trapframe_bk = *p->trapframe;
            p->trapframe->epc = (uint64)p->alarm_handler;
        }
        }
        yield();
    }
    ...
}

Then, let us impl the sigalarm and sigreturn functions in ./kernel/sysproc.c:

int
sigalarm(int ticks, void (* handler)()) {
   struct proc *p = myproc();
   p->alarm_interval = ticks;
   p->alarm_handler = handler;
   p->alarm_ticks_left = 0;
   return 0;
}

uint64
sys_sigalarm(void) {
    int ticks;
    uint64 func_ptr;
    if (argint(0, &ticks), ticks < 0) return -1;
    if (argaddr(1, &func_ptr), func_ptr < 0) return -1;

    return sigalarm(ticks, (void(*)())func_ptr);
}
int sigreturn() {
   struct proc *p = myproc();
   *p->trapframe = *p->trapframe_bk;
   p->alarm_ticks_left = 0;
   return p->trapframe->a0;
}

uint64
sys_sigreturn(void) {
    return sigreturn();
}

test 1/2/3

The code is shown in previous section.

  • If alarm_handler is still running while timer interupt n ticks. We need to make sure there is only one alarm_handler running in the system emitted by sigalarm.

    In order to impl this, we can add a lock-like flag to record the state(alarm_handler is running or not)

    However, we can reset the alarm_ticks_left to zero in the sigreturn instead of in the function usertrap. The alarm_handler will not be called util user calls sigreturn.

  • The return value of sigreturn is stored in the a0 register, which is not the behavior we expected. This value may cover the original a0 register. So, just return p->trapfraame->a0 is ok. It will set the a0 register to previous value. It’s tricky but useful.