6.s081/6.1810(Fall 2022)Lab4: Traps

前言

这个Lab我觉得比较难,主要是涉及到底层、汇编,这方面我确实接触得少。

其他篇章

环境搭建
Lab1: Utilities
Lab2: System calls
Lab3: Page tables
Lab4: Traps
Lab5: Copy-on-Write Fork for xv6

参考链接

官网链接
xv6手册链接,这个挺重要的,建议做lab之前最好读一读。
xv6手册中文版,这是几位先辈们的辛勤奉献来的呀!再习惯英文文档阅读我还是更喜欢中文一点,开源无敌!
risc-v 指令集
个人代码仓库
官方文档

0. 环境搭建

Lab3步骤一致,不多解释。**记得make clean!!!**上个lab因为没弄这个卡了一个多小时,难绷。
在这里插入图片描述

1. RISC-V assembly (easy)

1.0 简介

在这里插入图片描述
这个Task是一个热身Task,带你熟悉一下RISC-V汇编的,它告诉我们执行make fs.img会生成一个user/call.c的汇编程序user/call.asm,我们来看看吧:

make fs.img && vi user/call.asm

顺便看一下原型代码,很基础(碎碎念:void main不规范啊!):

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

然后题目叫把答案存在answers-traps.txt下,下面来看看这几个问题:

1.1 Q1

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

我们来看一看printf的调用,可以看到,printf的三个参数分别被压入了a0 - a2寄存器,具体到13这个数,是被保存在了a2里。

在这里插入图片描述
另外,这堂课的Lecture中给出了RISC-V各个寄存器的作用,可以看到,a0 - a7寄存器是用于保存函数的参数的,其中头两个寄存器还用于保存函数返回值。
在这里插入图片描述

1.2 Q2

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

我们还是来看刚刚的printf,可以看到,我们虽然本身调用了f(8)+1,但是汇编代码并没有展示给我们8 + 3 + 1的过程,也没有调用函数时的各种压栈。
在这里插入图片描述
结合题目的提示以及我们的常识,我们可以知道这个地方直接被编译器优化了,优化后内联展开了f和g的调用,无疑节省了开销,因此,没有调用函数f和g的汇编代码。

1.3 Q3

At what address is the function printf located?

还是那段代码,这里写的就是printf的地址,这是十六进制的,因此是0x642
在这里插入图片描述
不过我们来看一看这个0x642是怎么算出来的:
在这里插入图片描述
查阅文档我们可以看见,auiqc指令是将传入的立即数左移12位后加上当前的pc存入ra,因此49行程序执行后ra的值为0x30,然后看一看jalr指令的文档:
在这里插入图片描述
可以看到,这个指令的作用就是跳到指定地址去,在这里就是跳到0x30 + 1554 = 0x30 + 0x612 = 0x642。

1.4 Q4

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

从刚刚发的文档就可以看见,jalr会把PC指针往下偏一个,也就是0x34 + 4 = 0x38,因此这时ra中的值为0x38。

1.5 Q5

在这里插入图片描述
这个问题叫我们运行一下这两行代码,问运行结果,结果是HE110 World,是不是很神奇?必须值得指出的一点是,以%s打印unsigned *的行为是未定义的!不过这里也可以分析一下它的成因,其中57616转化为16进制即为0xE110,因此前面没什么好说的,而i的值如果每两个16进制位转化为一个char的话就会变为'\0'dlr,同时注意到题目给出了一个提示,告诉我们RISC-V是小端序的,即从低位开始看,因此就打出来了rld,题目还问如果是大端序机器i应该输入多少,那么只需要反转一下就行了,也就是0x726c6400,而57616的值当然不需要改变,因为这与端序无关。

1.6 Q6

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

依旧需要指出的一点是,printf中后续传入的参数少于format(就是第一个参数)中所需要的数量的行为是未定义的!不过我们依旧在这里研究一下编译器行为学。

实际结果应该是个不确定的值,Q1的时候我们就看见了,对于printf传参,会把入参依此压入从a0开始的寄存器,这里的printf本来需要三个参数,也就是要到a2寄存器,因此这里会输出a2中的值——这个值当然是不确定的、或者说依赖于之前的调用的。

2. Backtrace (moderate)

2.1 简单分析

这个Task叫Backtrace,回溯,经常刷算法题什么的应该对这个词很有印象。这里的backtrace在哲学上和算法中的backtrace类似,不过在具体上又有所不同。在这里题目就告诉你,在出错的时候,backtrace是对debug很有用的,它可以打印出函数的栈帧(stack frame),涉及到这方面确实也非常有用,比如说C++都在p0881r7把栈踪库加入C++23的标准库stacktrace了。
在这里插入图片描述
XV6中stack占据一个页的大小,
在这里插入图片描述
而函数的调用过程,实际上就是往这个栈里面压栈的过程,可以看到这个图里的解说,压栈是从高地址往低地址压的,每次调用都会在现有的基础上往下添加一个stack frame,我们有一个帧指针fp(frame pointer),指向了这个帧的头部,有一个sp,指向了这个帧的底部,其中帧里的内容,第一项是return address,代表帧应当返回的地址,具体一点的话,就是我们调用这个函数的原来的函数的地址。而我们要做的就是使用帧指针遍历这个栈并打印出每个栈帧保存的return address,这样就可以看出调用链,便于我们调试。
在这里插入图片描述

2.2 实现

在这里插入图片描述
搞清楚了需求就可以开始写实现了,实现的逻辑其实很简单,我们只需要使用fp从stack的头部遍历到stack页的尾部即可。具体而言,像我们上文说过的那样,fp指向一个栈帧的头部,栈帧的第一个位置放的是需要我们打印的return addr值,第二个位置放的是前一个帧的fp值,根据文档中的it actually points to the the address of the saved return address on the stack plus 8可以知道,在这个机器上每个位置的偏移量为8,因此这两个位置分别为fp - 8fp - 16,这实际上就是指针偏移一次和两次的结果,因此我们可以更直观地写为fp[-1](因为stack向下生长所以是减)。

题目告诉我们要实现在kernel/printf.c中,此外,hint还提示我们可以在kernel/riscv.h中添加这样一段代码以获取当前fp的值

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

此外还可以使用kernel/riscv.h中的PGROUNDDOWN(fp) 来获取页末的fp,整体遍历就好像遍历链表一样,就此我们就可以很容易写出如下代码:

void 
backtrace(void)
{
  for (uint64* fp = (uint64*)r_fp(), *bottom = (uint64*)PGROUNDDOWN((uint64)fp);
      fp > bottom ; fp = (uint64*)fp[-2])
  {
    printf("%p\n", fp[-1]); // 获取并打印返回地址
  }
}

由于我们这个函数是要被调用的,因此我们还需要在老地方暴露接口:
在这里插入图片描述
然后按照要求,在sys_sleep中调用backtrace
在这里插入图片描述

2.3 测试

推送后跑一下bttest
在这里插入图片描述

./grade-lab-traps backtrace
在这里插入图片描述

3. Alarm (hard)

3.1 简单分析

这个lab是要求我们实现一个feature,用途简单来说就是隔一段时间发出一次中断,调用一次函数,达到周期实现函数的效果。
在这里插入图片描述

3.2 test0: invoke handler

3.2.1 添加调用

我们首先按照要求,增加sigalarm调用与sigreturn 调用,添加方法比较常规:

  • 为Makefile添加文件
	$U/_alarmtest\	
  • 在user.h中暴露接口
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
  • 在usys.pl中添加脚本
entry("sigalarm");
entry("sigreturn");
  • 在syscall.h中注册编号
#define SYS_sigalarm    22
#define SYS_sigreturn   23
  • 在syscall.c中添加映射
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
...

[SYS_sigalarm]    sys_sigalarm,
[SYS_sigreturn]   sys_sigreturn,
  • 在sysproc.c中添加两个syscall的框架
uint64
sys_sigalarm(void)
{
  return 0;
}

uint64
sys_sigreturn(void)
{
  return 0;
}

3.2.2 获取参数

sys_sigalarm接受两个参数,分别是ticks和handle,从语义学的角度来看就知道前者是间隔的时间,后者是需要处理的函数,以类似于回调函数的形式传入。同时HintYour sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h). 告诉我们这俩参数还应该存在proc结构体里,同时我们还应当维护一个变量,用于已经经历的时长,因此我们需要先在proc里开三个字段,然后在实现里获取参数:

// proc.h
struct proc {
...
  int alarm_past;              // 系统调用alarm的过去时间
  int alarm_ticks;             // 系统调用alarm的间隔
  void (*alarm_handler)();     // 系统调用alarm的待执行的函数
...
// sysproc.c
uint64
sys_sigalarm(void)
{
  struct proc *p = myproc();
  // 从用户空间获取参数并存储到proc
  argint(0, &p->alarm_ticks);
  argaddr(1, (uint64*)&p->alarm_handler);
  p->alarm_past = 0; // 重置alarm_past


  return 0;
}

同时,我们应该在proc.c下的allocproc中将它们初始化为0。

static struct proc*
allocproc(void)
{
...
  p->alarm_past = 0;
  p->alarm_ticks = 0;
  p->alarm_handler = (void*)0;

3.2.3 处理中断

Hint提示我们时间中断时会在trap.cusertrap()中执行,并且我们只用修改if(which_dev == 2) ...控制的域,这里就存在一个问题:可以去直接在中断中调用handle吗?答案当然是不行的,我们知道,中断是内核态处理的东西,而handle是一个用户态的地址,用户态和内核态的内存空间是不共享的!因此我们需要找到一种方法去调用这个回调函数。

根据学习我们可以知道,trap时系统会把上下文全给存到proc下那个trapframe类型的指针指向的地方,而trapframe类型中又依赖epc去保存先前的用户程序的地址,因此我们这里假如粗暴地把epc重定向到我们的handle,就可以达成test0的调用中断处理函数的目的了:

  // give up the CPU if this is a timer interrupt.
  if (which_dev == 2) {
    if (p->alarm_ticks && ++p->alarm_past == p->alarm_ticks) {
      p->alarm_past = 0;
      p->trapframe->epc = (uint64)p->alarm_handler;
    }
    yield();
  }

3.2.4 测试

输入alarmtest,观察到test0 passed即可。
在这里插入图片描述

3.3 test1/test2()/test3(): resume interrupted code

3.3.1 恢复上下文与防止多次调用

在上一个test中,我们会发现,虽然test0通过了,但是test1挂掉了,究其原因其实很简单——trap时有个保存上下文(context)与恢复上下文的过程,但是我们刚刚直接暴力覆盖掉了保存的上下文,在恢复上下文的时候自然就会出现错误,因此,很容易想到我们可以使用一个中间变量去存取被覆盖掉的值,然后调用完handle后再放回去。但是这就有一个问题了——哪些值、或者说寄存器被覆盖了?我们显式覆盖的值自然只有epc一个,但是在handle的执行过程中,我们同样可能发生trap,导致trapframe的值被搞得一团糟,因此我们应该保存整个tramframe,此外,还有一个问题是我们的handle可能需要运行一段时间,若是在这段时间里handle再次被调用,很明显它就将占据全部的时间片段,为了避免这一点,我们应当设置一个值来标记当前hadle是否正在被执行。搞清楚了这些就比较简单了:

  • 首先在 proc中定义两个field:
  int alarm_on;                     // 系统调用alarm是否开启
  int alarm_past;                   // 系统调用alarm的过去时间
  int alarm_ticks;                  // 系统调用alarm的间隔
  void (*alarm_handler)();          // 系统调用alarm的待执行的函数
  struct trapframe* pre_trapframe; // 保存上一次的trapframe

在这里插入图片描述

  • 在alloc中模仿上文分配内存以及为alarm_on置零:
    在这里插入图片描述
  • 别忘了free:
    在这里插入图片描述
  • 检查一下trapframe,发现里面并没有什么需要深拷贝的东西,因此直接在trap中赋值:
  // give up the CPU if this is a timer interrupt.
  if (which_dev == 2) {
    if (!p->alarm_on && p->alarm_ticks && ++p->alarm_past == p->alarm_ticks) {
      p->alarm_on = 1;
      *p->pre_trapframe = *p->trapframe;
      p->alarm_past = 0;
      p->trapframe->epc = (uint64)p->alarm_handler;
    }
    yield();
  }

然后再sys_sigreturn中恢复上下文:

uint64
sys_sigreturn(void)
{
  struct proc *p = myproc();
  if (p->alarm_on)
  {
    *p->trapframe = *p->pre_trapframe;
    p->alarm_past = 0;
    p->alarm_on = 0;
  }
  return 0;
}

3.3.2 恢复寄存器a0的值

这个test3是6.1810,也就是22Fall才有的,之前都没有,说的是最后还存在一个问题,我们在task1学习过,函数的返回值会被保存在寄存器a0、a1中,而这些系统调用,包括sys_sigreturn都是有返回值的,这就会导致进行了系统调用后a0的值会被污染,因此我们直接返回原来的a0值即可:
在这里插入图片描述
话说总觉得这么写不太对,因为我查了一下Linux的sigreturn是返回-1的,这里明显不是返回-1,希望谁来给我解解惑。

3.3.3 测试

alarmtest后完美运行:
在这里插入图片描述

3.4 测试

跑一下./grade-lab-traps alarm,通过:
在这里插入图片描述

4. 最后测试

按照惯例加入time.txt,然后make grade

因为我在task1写了中文,直接显示编码错误,懒得折腾了,直接把测试脚本这里给注释了,反正它也只能读个单词数量(
在这里插入图片描述
在这里插入图片描述
顺利通过(话说这个usertests是啥玩意啊,花这么多时间)
在这里插入图片描述

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值