Lab4: traps

Lab4: traps

RISC-V assembly (easy)

阅读***user/call.asm***中生成可读的汇编版,回答一下问题

  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?
  2. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)
  3. printf函数位于哪个地址?
  4. mainprintfjalr之后的寄存器ra中有什么值?
  5. 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?这是将字节映射到字符的ASCII码表

输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

这里有一个小端和大端存储的描述和一个更异想天开的描述

  1. 在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);
/* call.c */
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}
  1. 哪些寄存器保存函数的参数?例如,在mainprintf的调用中,哪个寄存器保存13?

a0保存寄存器返回值 0,a1寄存器保存12 (编译器计算好了), a2寄存器保存13

  1. main的汇编代码中对函数f的调用在哪里?对g的调用在哪里(提示:编译器可能会将函数内联)

编译器直接用计算结果替换了函数调用过程,因为始终未出现两函数地址而出现了计算结果

  1. printf函数位于哪个地址?

0000000000000630 <printf>:

  34:	608080e7          	jalr	1536(ra) # 630 <printf>

auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值+8。因此jalr会跳转1536+48=1594=0x630处,观察汇编代码可知确实在0000000000000630处。

  1. mainprintfjalr之后的寄存器ra中有什么值?

0x30+8=0x38

  1. 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);

程序的输出是什么?输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i设置成什么?是否需要将57616更改为其他值?

输出为HE110 World
因为riscv为小端存储,从&i开始字节分别为0x72,0x6c,0x64, 0x00.分别对应’r’,‘l’,‘d’,'0’的ascii码,0x00作为字符串结束标志。
57616=0xE110
i应该设置为0x726c6400

  1. 运行printf(“x=%d y=%d”, 3);在y=后面输出什么?为什么会这样?

输出y=1。取决于寄存器a2(第3个参数)的值。

Backtrace(moderate) —— 打印每个栈的返回地址

回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。

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

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

bttest退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel(或riscv64-unknown-elf-addr2line -e kernel/kernel),并将上面的地址剪切粘贴如下:

$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

你应该看到类似下面的输出:

kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的backtrace应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。

提示

  • 在***kernel/defs.h***中添加backtrace的原型,那样你就能在sys_sleep中引用backtrace
  • GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器,将下面的函数添加到***kernel/riscv.h***
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

并在backtrace中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取s0

  • 这个课堂笔记中有张栈帧布局图。注意返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置
  • XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过PGROUNDDOWN(fp)PGROUNDUP(fp)(参见***kernel/riscv.h***)来计算栈页面的顶部和底部地址。这些数字对于backtrace终止循环是有帮助的。

一旦你的backtrace能够运行,就在***kernel/printf.c***的panic中调用它,那样你就可以在panic发生时看到内核的backtrace

第一步

按照提示,在kernel/defs.h添加定义,在kernel/riscv.h增加r_fp()

第二步

第五课的图,理解这个图就理解了这个函数怎么写,xv6里一个栈帧大小是16个字节

image-20210917201847800

backtrace这一个lab可以说是为了能够深刻理解stack machine机制的设计的,每当调用函数时,首先需要将返回地址,之前的栈帧地址入栈,而栈空间的地址也是从高地址往低地址增长的,所以当前的栈帧的偏移8个字节即为return address,我们需要每次打印出返回地址,同时偏移16个字节则为前一个栈帧的地址,我们依次往前寻找,直到当前的栈帧的起始地址为PGROUNDUP(fp)

***kernel/printf.c***中实现名为backtrace()的函数

void 
backtrace(void)
{
  uint64 sp = r_fp();
  // 找到栈底结束
  uint64 bottom = PGROUNDUP(fp);
  uint64 return_addr;
  
  printf("backtrace:\n");
  while (sp < bottom)
  {
    return_addr = *(uint64*)(sp-8);
    sp = *(uint64*)(sp-16);
    printf("%p\n", return_addr);
  }
}

第三步

在kernel/sysproc.c的sys_sleep()函数中调用backtrace()

uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  if(argint(0, &n) < 0)
    return -1;
  acquire(&tickslock);
  ticks0 = ticks;
  while(ticks - ticks0 < n){
    if(myproc()->killed){
      release(&tickslock);
      return -1;
    }
    sleep(&ticks, &tickslock);
  }
  release(&tickslock);
  backtrace();
  return 0;
}

Alarm(Hard) —— 每个n个时钟周期,内核强制调用用户函数fn

在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。**这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。**例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了alarmtestusertests就是正确的。

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

你将在XV6的存储库中找到名为***user/alarmtest.c***的文件。将其添加到***Makefile***。注意:你必须添加了sigalarmsigreturn系统调用后才能正确编译(往下看)。

alarmtesttest0中调用了sigalarm(2, periodic)来要求内核**每隔两个滴答强制调用periodic(),然后旋转一段时间。你可以在*user/alarmtest.asm***中看到alarmtest的汇编代码,这或许会便于调试。当alarmtest产生如下输出并且usertests也能正常运行时,你的方案就是正确的:

$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

当你完成后,你的方案也许仅有几行代码,但如何正确运行是一个棘手的问题。我们将使用原始存储库中的***alarmtest.c***版本测试您的代码。你可以修改***alarmtest.c***来帮助调试,但是要确保原来的alarmtest显示所有的测试都通过了。

test0: invoke handler(调用处理程序)

首先修改内核以跳转到用户空间中的报警处理程序,这将导致test0打印“alarm!”。不用担心输出“alarm!”之后会发生什么;如果您的程序在打印“alarm!”后崩溃,对于目前来说也是正常的。以下是一些提示

  • 您需要修改***Makefile***以使***alarmtest.c***被编译为xv6用户程序。
  • 放入***user/user.h***的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 更新***user/usys.pl***(此文件生成***user/usys.S***)、***kernel/syscall.h***和***kernel/syscall.c***以允许alarmtest调用sigalarmsigreurn系统调用。
  • 目前来说,你的sys_sigreturn系统调用返回应该是零。
  • 你的sys_sigalarm()应该将报警间隔和指向处理程序函数的指针存储在struct proc的新字段中(位于***kernel/proc.h***)。
  • 你也需要在struct proc新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在***proc.c***的allocproc()中初始化proc字段。
  • 每一个滴答声,硬件时钟就会强制一个中断,这个中断在***kernel/trap.c***中的usertrap()中处理。
  • 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
if(which_dev == 2) ...
  • 仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在***user/alarmtest.asm***中,periodic位于地址0)。
  • 您需要修改usertrap(),以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址?
  • 如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
make CPUS=1 qemu-gdb
  • 如果alarmtest打印“alarm!”,则您已成功。

test1/test2(): resume interrupted code(恢复被中断的代码)

alarmtest打印“alarm!”后,很可能会在test0test1中崩溃,或者alarmtest(最后)打印“test1 failed”,或者alarmtest未打印“test1 passed”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。

作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreurn系统调用。请查看***alarmtest.c**中的periodic作为示例。这意味着您可以将代码添加到usertrapsys_sigreurn中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。

提示:

  • 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
  • 当计时器关闭时,让usertrapstruct proc中保存足够的状态,以使sigreurn可以正确返回中断的用户代码。
  • 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。test2测试这个。
  • 一旦通过test0test1test2,就运行usertests以确保没有破坏内核的任何其他部分。

第一步

1、2、3、4按照提示完成即可

5、6 添加:报警间隔和指向处理程序函数的指针(int ticks, void (*handler)() )、经历了多少滴答的字段
并在***proc.c***的allocproc()中初始化proc字段。

// alarm
  void (*handler)();           // alarm函数
  int alarm_interval;          // 报警间隔
  int total_ticks;             // 判断走过滴答数 要初始化的字段

7、8

如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码

// give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    p->total_ticks++;
    yield(); // Give up the CPU for one scheduling round.
  }

第二步

9、10

修改usertrap(),使得当进程的报警间隔期满时,用户进程执行处理程序函数。

添加一下32个寄存器,参考lec 6的内容

// 需要保存的历史寄存器用
  uint64 his_epc;           
  uint64 his_ra;
  uint64 his_sp;
  uint64 his_gp;
  uint64 his_tp;
  uint64 his_t0;
  uint64 his_t1;
  uint64 his_t2;
  uint64 his_t3;
  uint64 his_t4;
  uint64 his_t5;
  uint64 his_t6;
  uint64 his_a0;
  uint64 his_a1;
  uint64 his_a2;
  uint64 his_a3;
  uint64 his_a4;
  uint64 his_a5;
  uint64 his_a6;
  uint64 his_a7;
  uint64 his_s0;
  uint64 his_s1;
  uint64 his_s2;
  uint64 his_s3;
  uint64 his_s4;
  uint64 his_s5;
  uint64 his_s6;
  uint64 his_s7;
  uint64 his_s8;
  uint64 his_s9;
  uint64 his_s10;
  uint64 his_s11;

sys_alarm的实现

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

sys_sigreturn的实验

uint64
sys_sigreturn(void)
{
  // 重载寄存器
  struct proc *p = myproc();
  p->trapframe->epc = p->his_epc; 
  p->trapframe->ra = p->his_ra; 
  p->trapframe->sp = p->his_sp; 
  p->trapframe->gp = p->his_gp; 
  p->trapframe->tp = p->his_tp; 
  p->trapframe->a0 = p->his_a0; 
  p->trapframe->a1 = p->his_a1; 
  p->trapframe->a2 = p->his_a2; 
  p->trapframe->a3 = p->his_a3; 
  p->trapframe->a4 = p->his_a4; 
  p->trapframe->a5 = p->his_a5; 
  p->trapframe->a6 = p->his_a6; 
  p->trapframe->a7 = p->his_a7; 
  p->trapframe->t0 = p->his_t0; 
  p->trapframe->t1 = p->his_t1; 
  p->trapframe->t2 = p->his_t2; 
  p->trapframe->t3 = p->his_t3; 
  p->trapframe->t4 = p->his_t4; 
  p->trapframe->t5 = p->his_t5; 
  p->trapframe->t6 = p->his_t6;
  p->trapframe->s0 = p->his_s0;
  p->trapframe->s1 = p->his_s1;
  p->trapframe->s2 = p->his_s2;
  p->trapframe->s3 = p->his_s3;
  p->trapframe->s4 = p->his_s4;
  p->trapframe->s5 = p->his_s5;
  p->trapframe->s6 = p->his_s6;
  p->trapframe->s7 = p->his_s7;
  p->trapframe->s8 = p->his_s8;
  p->trapframe->s9 = p->his_s9;
  p->trapframe->s10 = p->his_s10;
  p->trapframe->s11 = p->his_s11;

  p->is_handler_in = 1;
  return 0;
}

然后就是修改usertrap完成最后要求

if(which_dev == 2) {
    p->total_ticks++;
    if (p->is_handler_in)
      {
        p->is_handler_in = 0;

        // 保存当前trampframe上的用户寄存器
        // 用于alarm函数调用完之后
        p->his_epc = p->trapframe->epc; 
        p->his_ra = p->trapframe->ra;
        p->his_sp = p->trapframe->sp;
        p->his_gp = p->trapframe->gp;
        p->his_tp = p->trapframe->tp;
        p->his_t0 = p->trapframe->t0;
        p->his_t1 = p->trapframe->t1;
        p->his_t2 = p->trapframe->t2;
        p->his_t3 = p->trapframe->t3;
        p->his_t4 = p->trapframe->t4;
        p->his_t5 = p->trapframe->t5;
        p->his_t6 = p->trapframe->t6;
        p->his_a0 = p->trapframe->a0;
        p->his_a1 = p->trapframe->a1;
        p->his_a2 = p->trapframe->a2;
        p->his_a3 = p->trapframe->a3;
        p->his_a4 = p->trapframe->a4;
        p->his_a5 = p->trapframe->a5;
        p->his_a6 = p->trapframe->a6;
        p->his_a7 = p->trapframe->a7;
        p->his_s0 = p->trapframe->s0;
        p->his_s1 = p->trapframe->s1;
        p->his_s2 = p->trapframe->s2;
        p->his_s3 = p->trapframe->s3;
        p->his_s4 = p->trapframe->s4;
        p->his_s5 = p->trapframe->s5;
        p->his_s6 = p->trapframe->s6;
        p->his_s7 = p->trapframe->s7;
        p->his_s8 = p->trapframe->s8;
        p->his_s9 = p->trapframe->s9;
        p->his_s10 = p->trapframe->s10;
        p->his_s11 = p->trapframe->s11;

        // 将当前PC值改为alarm函数的地址
        // 当函数从内核返回时调用handler
        p->trapframe->epc = (uint64)p->handler;

        p->total_ticks = 0;
      } else {
        p->total_ticks -= 1;
      }
    yield(); // Give up the CPU for one scheduling round.
  }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值