Xv6 Lab4: Traps

发布时间 2023-07-13 19:32:59作者: zwyyy456

RISC-V assembly

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

  • a2 寄存器,函数调用时,参数从左到右会依次保存在 a0, a1, a2, a3 寄存器,似乎是一直到寄存器 a7 的。

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

  • 这里的调用都被内联了

At what address is the function printf located?

  • auipc(Add Upper Immediate PC)指令是将一个立即数左移 $12$ 位加上当前指令的地址(pc)中,得到一个绝对地址。

  • 例如 auipc a0, 0x0 就是将 $\text{0x0}$ 左移 $12$ 位加上当前指令的地址 (pc) 中(pc 的值我们一般认为是在指令执行完成时才发生递增,从而指向下一条指令,使得处理器能够按顺序顺利执行指令序列),因此,执行 auipc a0, 0x0 时,加的就是当前指令的地址,即 $\text{0x28}$。

  • Ep4uySNWrac6nwQ

  • auipc 常常会与 jalr(Jump and Link Register) 指令组合在一起,在 jalr 1526(ra) 中,它把 pc 设置为寄存器 ra 的值加上 offset,然后把原先的 $pc + 4$ 的值写入寄存器 ra,即 ra 的值就是函数的返回值,即正常序列里,jalr 的下一条指令,这里 ra 的值应该是 $\text{0x38}$, pc 的值则为 $\text{0x626}$。

unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
  • 上面这段代码的输出结果为 He110 World

Backtrace

这里的关键在于弄懂 stack frame 的结构,参考这张图,其实基本原理与 x86 下 C 语言的 stack frame 是一致的,我们通过 uint64 fp = r_fp() 函数拿到的是栈基地址(联想 x86 中的 rbp),因此 fp + 8 就是 return address 的地址,因此 uint64 ra = *(uint64 *)(fp + 8);,注意 fp + 8 是地址的地址,不是我们要输出的 ra!

我们需要遍历 stack frame,上一个 stack frame 的栈基地址就存放在 fp + 16 处,因此 fp = *(uint64 *)(fp + 16);

循环的终止条件,可以先计算出 fp 的上下界 PGROUNDDOWN(fp)PGROUNDUP(fp)(按照 PGSIZE 大小对齐),又或者当 PGROUNDUP(fp) != p->kstack + PGSIZE 时退出循环。

void backtrace(void) {
    uint64 fp = r_fp();
    printf("backtrace\n");
    uint64 upb = PGROUNDUP(fp);
    uint64 db = PGROUNDDOWN(fp);
    // struct proc *p = myproc();
    while (fp < upb && fp > db) {
        uint64 ra = *(uint64 *)(fp - 8);
        printf("%p\n", ra);
        fp = *(uint64 *)(fp - 16);
    }
}

Alarm

这个实验在了解了 trap 机制的整个流程,如何陷入到内核代码,又如何返回用户代码之后,其实并不难,但是还是有两个地方比较难想:

  1. sigalarm 的调用其实只起到一个开关的作用,当开启 sigalarm 之后,还是要在 usertrap 的外部定时中断部分来执行定时响应函数,我们可以通过 argaddr 函数来获取 sigalarm 传入的作为函数地址的第二个参数,但是,我们并不能直接在 usertrap 中把函数地址强制类型转化为函数指针来调用函数,这是因为 kernel page tableuser page table 是不一样的,我们需要把 p->trapframe->epc 修改为 periodic 的地址,在返回用户态时,pc 会被更新为 p->trapframe->epc 的值,这样返回到用户态时就会马上执行 periodic 函数;

  2. 经过上面的操作,我们返回时会直接执行 periodic 函数,但是执行完函数之后,机器就不知道接下来该执行哪一条指令来,因此提供了 sigreturn 函数,使得可以恢复到发生定时器中断时的状态,这里提到了需要保存很多寄存器的值,因此直接在 struct proc 中添加一个 struct trapframe *save_reg; 字段,在 usertrap 的定时中断响应中,利用 memmovep->trapframe 复制到 p->save_reg,而在 sys_return 中将 p->save_reg 恢复到 p->trapframe

几处关键的代码:

trap.c 中:

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
        if (which_dev == 2 && p->interval != 0) {
            // 说明启用了 alarm
            ++p->tick_num;
            if (p->tick_num == p->interval) {
                if (p->flag == 0) {
                    // typedef void (*Handler)();
                    // printf("p->handler: %p\n", p->handler);
                    memmove(p->save_reg, p->trapframe, PGSIZE);
                    p->flag = 1;
                    p->trapframe->epc = p->handler;
                }
                p->tick_num = 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();
}

sysproc.c 中:


uint64 sys_sigalarm() {
    struct proc *p = myproc();
    int interval;
    if (argint(0, &interval) < 0) {
        return -1;
    }
    uint64 addr;
    if (argaddr(1, &addr) < 0) {
        return -1;
    }
    p->interval = interval;
    p->handler = addr;

    return 0;
}

uint64 sys_sigreturn() {
    struct proc *p = myproc();
    memmove(p->trapframe, p->save_reg, PGSIZE);
    p->flag = 0;
    return 0;
}

同时注意在 proc.callocproc 中给 save_reg 分配空间,在 freeproc 中释放。

以及 kernel/param.h 中,FSSIZE 要修改为至少 $1088$,否则 alarmtest 能过,但是 usertests 过不了,会卡在 writebig 处,报错提示为 "test writebig: panic: balloc: out of blocks",不知道有没有大佬能解惑。