1813 字
9 分钟
Lab:traps
2024-09-27
无标签

Lab:traps#

RISC-V assembly (easy)#

  1. 理解 RISC-V 汇编
  2. 函数内联
  3. 大端小端存储

Backtrace(moderate)#

题目:#

backtrace 是一个**存放于栈上用于指示错误发生位置的函数调用列表。**

kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:

backtrace:

0x0000000080002cda

0x0000000080002bb6

0x0000000080002898

解决方案:#

编译器在每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。通过使用这些帧指针我们可以遍历栈,并在每个栈帧中打印保存的返回地址。

tips:

  • kernel/defs.h **中添加 ****backtrace ****的原型,那样你就能在sys_sleep中引用 **backtrace
  • GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器,将下面的函数添加到kernel/riscv.h(并在backtrace中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取**<font style="color:rgb(51, 51, 51);background-color:rgb(247, 247, 247);">s0</font>**
  • XV6在内核中以页面对齐的地址为每个进程的栈分配一个页面****。你可以通过PGROUNDDOWN(fp) PGROUNDUP(fp)(参见kernel/riscv.h)来计算栈页面的顶部和底部地址。这些数字对于**backtrace**终止循环是有帮助的。
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

void backtrace()
{
  printf("backtrace:\n");
  uint64 this_fp = r_fp();
  backtrace_help(&this_fp);
  return;
}
void backtrace_help(uint64 *this_fp)
{
  // 判断是否超出栈,若超出栈,停止
  if ((PGROUNDUP(*this_fp) - PGROUNDDOWN(*this_fp)) != PGSIZE)
  {return;}
  // 返回地址保存在 -8 偏移的位置
  printf("%p\n", *(uint64 *)(*this_fp - 8));
  // 前一个帧指针保存在-16偏移的位置
  uint64 last_fp = *this_fp - 16;
  backtrace_help(&(*(uint64 *)last_fp)); // 注意这里的指针的转换为什么是必要的
  return;
}

Alarm(Hard)#

题目:#

你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。这对于那些希望限制CPU 占用时间的的进程,或者计算的同时执行某些周期性操作的进程很有用。

更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。

例如,你可以在应用程序中使用类似的一些东西处理页面故障。

  1. **应当添加一个新的 ****sigalarm(interval, handler) **系统调用
  2. **如果一个程序调用了 sigalarm(n, fn),那么程序每消耗了CPU时间达到 n 个“滴答”,内核应当使应用程序函数 ****fn **被调用。
  3. ******fn **返回时,应用应从它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。
  4. 如果一个程序调用了 sigalarm(0, 0),系统应当停止生成周期性的报警调用。

第一步:****invoke handler(调用处理程序)

首先修改内核以跳转到用户空间中的报警处理程序:

  • 修改Makefile 以使alarmtest.c被编译为xv6用户程序。
  • 放入 user/user.h 的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 更新user/usys.pl(此文件生成user/usys.S)、kernel/syscall.hkernel/syscall.c 以允许alarmtest调用sigalarmsigreturn系统调用。
  • 目前来说,你的sys_sigreturn系统调用返回应该是零。

  • 现在我们已经有了**sigalarmsigreturn****系统调用,但不具有实际的功能。**
  • 我们需要存储**sigalarm**调用的参数
  • sys_sigalarm()****将报警间隔和指向处理程序函数的指针存储在**<font style="background-color:rgb(247, 247, 247);">struct proc</font>**的新字段中(位于kernel/proc.h)。
  • 需要在struct proc新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;(用于循环,当经历了足够滴答后报警)

  • 现在我们已经存储了报警间隔和处理程序函数,我们需要在报警后调用处理程序
  • 每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的**<font style="background-color:rgb(247, 247, 247);">usertrap()</font>**中处理,下面是处理代码
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();
    /** 判断是什么原因调用了usertrap函数*/ 
    // 是否是系统调用
    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
    }
    // 处理意外的陷阱
    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.
    // 如果这是一个定时器中断,则放弃 CPU。
    if (which_dev == 2){
        
        #ifdef lab_traps
        if (p->alarm_ticks != 0 && p->alarm_status != 1)
        {
            p->total_ticks++;
            if (p->alarm_ticks == p->total_ticks)
            {
                p->total_ticks = 0;
                // void *handler() = (void *)p->handler;
                //void (*handler)(void) = (void (*)(void))p->handler;
                //handler();
                // 需要保存寄存器内容,我们要在执行完报警程序后回到原本的代码执行处
                memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
                // 正常情况下,中断恢复执行epc指向的代码,也就是恢复原本代码,但我们这个lab中
                // 通过更改epc来改变中断后执行的函数,最后再通过 sigreturn 进行恢复
                // 这就是为什么在上面我们保存了寄存器的内容
                p->trapframe->epc = p->handler;	
                p->alarm_status = 1;
            }
        }
        #endif

        yield();
    }
    usertrapret();
}
  • 在跳转执行特定的处理函数后,我们需要返回到原本的代码值
  • **这个特定的处理函数需要配合 **sigreturn 一起使用,处理函数处理完后通过 sigreturn 来返回到原本的代码
void
periodic()
{
  count = count + 1;
  printf("alarm!\n");
  sigreturn();
}
  • sys_sigreturn 和正常的中断恢复不同,是我们人为的恢复数据,和正常的中断恢复的流程不一样
uint64 
sys_sigreturn(void){
  myproc()->alarm_status = 0;
    // 恢复原本的数据,sepc也变为
  memmove(myproc()->trapframe, myproc()->alarm_trapframe, sizeof(struct trapframe));
  return 0;
 }
Lab:traps
https://scudays.github.io/posts/6s081/lab/labtraps/
作者
Days
发布于
2024-09-27
许可协议
CC BY-NC-SA 4.0