【MTI 6.S081 Lab】traps

这篇具有很好参考价值的文章主要介绍了【MTI 6.S081 Lab】traps。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


本实验阅读《深入理解计算机系统》第八章异常控制流并做shell实验将会是很有帮助的

本实验探讨了如何使用陷阱实现系统调用。您将首先使用堆栈进行热身练习,然后实现用户级陷阱处理的示例。

RISC-V assembly (easy)

了解一下您在6.1910(6.004)中接触到的RISC-V程序集非常重要。在您的xv6 repo中有一个文件user/call.c。makefs.img编译它,并在user/call.asm中生成程序的可读汇编版本。

阅读call.asm中函数g、f和main的代码。RISC-V的使用说明书见参考页。以下是您应该回答的一些问题(将答案存储在answers-traps.txt文件中):

  • 函数的参数包含在那个寄存器里面?例如,在main调用printf时,13在哪个寄存器中?

    : RISC-V调用约定尽可能在寄存器中传递参数。为此,最多使用八个整数寄存器a0-a7和八个浮点寄存器fa0-fa7。寄存器a2保存13。

  • 在main中哪里是调用函数f的汇编代码?哪里是调用g的

    :在26: 45b1 li a1,12中,为调用f 和 g的代码,由于函数很简单,传入的参数又是一个编译时常量,所以直接将函数的结果在编译器算出来了,即12.

  • printf的地址位于何处?

    auipc rd, immediate x[rd] = pc + sext(immediate[31:12] << 12)把符号位扩展的 20 位(左移 12 位)立即数加到 pc 上,结果写入 x[rd]。

    30:	00000097          	auipc	ra,0x0
    

    当前pc为下一条指令的pc,pc=34,所以ra=0x34+0=34

    34:	600080e7          	jalr	1536(ra) # 630 <printf>
    jalr rd, offset(rs1)  t =pc+4; pc=(x[rs1]+sext(offset))&~1; x[rd]=t
    此处rd为编号为0x01的寄存器,即为ra。
    ra = pc + 4 = 0x38, pc=0x34+1536=0x630
    

    printf的地址位于0x630处。

  • 在jalr跳转到printf后,ra的值是什么?

    : ra放入返回值,jalr存了返回地址在ra中,此时为jalr下一条指令的地址,所以ra中存储的值为0x38

  • 运行下面的代码,输出是什么?

    unsigned int i = 0x00646c72;
    printf("H%x Wo%s", 57616, &i);
    

    输出HE110 World,说明我的机器是小端机器。

  • 在下面的代码中,“y=”之后将打印什么?(注意:答案不是一个特定的值。)为什么会发生这种情况?

    printf("x=%d y=%d", 3);
    x=3 y=1
    

    通过gdb调试,发现此时a2=1,所以输出y的值为1,因为函数参数默认在a0~a7寄存器中,此时函数想要有三个参数,一个是格式化输出"x=%d y=%d",一个是x的值,在此处为3,一个是y的值,应该被放入a2中,所以此处为2。

Backtrace (moderate)

对于调试来说,有一个回溯通常很有用:在发生错误的点之上的堆栈上的函数调用列表。为了帮助进行回溯,编译器生成机器代码,在堆栈上维护与当前调用链中的每个函数相对应的堆栈帧。每个堆栈帧由返回地址和指向调用方堆栈帧的“帧指针”组成。寄存器s0包含一个指向当前堆栈帧的指针(它实际上指向堆栈上保存的返回地址的地址加8)。回溯应该使用帧指针向上遍历堆栈,并在每个堆栈帧中打印保存的返回地址。

在这个代码中,我看到进入函数压栈至少为16,从这里的说明来看,这么做的原因是,除了压一个返回地址,其实也压了上一个函数的栈帧位置,这样就可以实现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)(在我的机器上为riscv64-unknown-linux-gnu-addr2line -e kernel/kernel),并且从你的backtrace复制粘贴这个地址,像下面一样:

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

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

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

Hints

  • kernel/defs.h中添加函数backtrace()原型,以便于你在sys_sleep中能调用backtrace

  • GCC编译器存储一个当前执行函数的帧指针在寄存器s0中。在kernel/riscv.h中添加下面的函数,并在backtrace中调用此函数来读取当前帧指针。r_fp()使用内联汇编读取s0。

    static inline uint64
    r_fp()
    {
      uint64 x;
      asm volatile("mv %0, s0" : "=r" (x) );
      return x;
    }
    
  • 这个页面展示了栈帧的布局图。注意到返回地址位于离堆栈帧的帧指针的固定偏移量(-8)处,而保存的帧指针位于离帧指针的恒定偏移量(-16)处。

    【MTI 6.S081 Lab】traps,MIT6S081,linux,c语言,系统架构,risc-v

  • backtrace()需要一种方法来识别它已经看到了最后一个堆栈帧,并且应该停止。一个有用的事实是,为每个内核堆栈分配的内存由一个页面对齐的页面组成,因此给定堆栈的所有堆栈帧都在同一页面上。您可以使用PGROUNDDOWN(fp)(请参阅kernel/rescv.h)来标识帧指针所引用的页面。

一旦您的backtrace工作正常,请在kernel/printf.c中从panic调用它,以便在它panic时看到内核的回溯。

解决方案

backtrace

void
backtrace(void) {
  printf("backtrace:\n");
  uint64 fp = r_fp();   // 获取当前栈指针
  uint64 max_fp = PGROUNDDOWN(fp) + PGSIZE;   // 由于栈在一页上,所以此处就能知道最大的栈帧在哪里了,可以等于最大栈帧,等于即最后一个
  do {
    printf("%p\n", *(uint64 *)(fp - 8));    // 打印返回地址
    fp = *(uint64 *)(fp - 16);    // 下一栈帧在固定偏移(-16)
  } while (fp < max_fp);
  return;
}

sys_sleep

uint64
sys_sleep(void)
{
  acquire(&tickslock);
  ...
  release(&tickslock);
  backtrace();
  return 0;
}

注意:

  • 这里不是打印栈帧的位置,而是打印返回地址的位置,通过返回地址,我们运行riscv64-unknown-linux-gnu-addr2line -e kernel/kernel,找到开始位置是这些的地址的函数,如果不是函数的开始,说明backtrace打印错误,会输出??:0

  • 在我的输出中

    $ ./bttest
    backtrace:
    0x00000000800021b0
    0x0000000080002022
    0x0000000080001d18
    
    riscv64-unknown-linux-gnu-addr2line -e kernel/kernel
    0x00000000800021b0
    0x0000000080002022
    0x0000000080001d18
    /home/zhj/MIT6S081OS/xv6-labs-2022/kernel/sysproc.c:71
    /home/zhj/MIT6S081OS/xv6-labs-2022/kernel/syscall.c:141
    /home/zhj/MIT6S081OS/xv6-labs-2022/kernel/trap.c:85
    

Alarm (hard)

实验任务

在本练习中,您将向xv6添加一个功能,该功能在进程使用CPU时间时定期提醒进程。这对于想要限制占用CPU时间的计算绑定进程,或者对于想要计算但也想要采取一些周期性操作的进程来说可能很有用。更一般地说,您将实现用户级中断/故障处理程序的原始形式;例如,您可以使用类似的方法来处理应用程序中的页面错误。如果您的解决方案通过了alarmtest和’usertests-q’,则它是正确的。

你应该添加一个新的sigalarm(interval, handler)系统调用。如果应用程序调用sigalarm(n, fn),那么在程序消耗的CPU时间的每n个“滴答”之后,内核应该调用应用程序函数fn。当fn返回时,应用程序应该从停止的地方恢复。在xv6中,tick是一个相当任意的时间单位,由硬件计时器生成中断的频率决定。如果应用程序调用sigalarm(0,0),内核应该停止生成周期性的警报调用

这个实验有点像CSAPP中所做的第八章,异常控制流的实验,shell实验,信号处理

您将在xv6存储库中找到一个文件user/armtest.c。将其添加到Makefile中。在添加了sigalarm和sigreturn系统调用之前,它不会正确编译(见下文)。

alarmtest在test0中调用sigalarm(2,periodic),要求内核每隔2次强制调用periodic(),然后旋转一段时间。您可以在user/armtest.asm中看到alarmtest的汇编代码,这可能对调试很方便。当alarmtest生成这样的输出并且usertests -q也正确运行时,您的解决方案是正确的:

$ 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
test3 start
test3 passed
$ usertest -q
...
ALL TESTS PASSED
$

当你完成后,你的解决方案将只有几行代码,但可能很难把它做好。我们将使用原始存储库中的alarmtest.c版本来测试您的代码。您可以修改alarmtest.c来帮助您进行调试,但要确保原始的alarmtest表明所有测试都通过了。

test0: invoke handler

首先修改内核以跳转到用户空间中的alarm处理程序,这将导致test0打印“alarm!”。不要担心“alarm!”输出后会发生什么;如果你的程序在打印“alarm!”后崩溃,在当前的完成进度来说,是可以的。

Hint

  • 修改Makefile,添加alarmtest.c

  • user/user.h正确声明

    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
    
  • 更新user/usys.plkernel/syscall.hkernel/syscall.c,允许alarm调用sigalarm和sigreturn

  • 现在,sys_sigreturn应该返回0

  • 你的sys_sigalarm应该存储alarm间隔和一个指针指向处理函数,在proc结构中添加新的字段

  • 您需要跟踪从上一次调用(或直到下一次调用)到进程的alarm处理程序已经传递了多少ticks;为此,您还需要在struct proc中添加一个新字段。您可以在proc.c中的allocproc()中初始化proc字段。

  • 每一次tick,硬件时钟都会强制中断,这在kernel/trap.cusertrap中处理

  • 如果有个时钟中断,在下面的语句内操作进程的alarm ticks

    if(which_dev == 2) ...
    
  • 只有当进程有一个未完成的计时器时,才调用报警功能。请注意,用户的报警功能的地址可能为0(例如,在user/armtest.asm中,periodical位于地址0)。

    用户函数的地址可能为0,所以不能通过handler函数指针是否指向0来判断是否启用了这个sig功能

  • 您需要修改usertrap(),以便在进程的警报间隔到期时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,是什么决定了用户空间代码恢复执行的指令地址?

    epc中存储的值,决定了从trap返回后,程序执行的第一条指令的地址,所以将其改为handler的地址即可。然而,这样会导致回不到原来的pc了,所以这里会导致崩溃。这个要等实现sigreturn后才能修复,所以这个任务上说,打印了alarm后崩溃是OK的。

  • 如果你告诉qemu只使用一个CPU,那么用gdb查看陷阱会更容易,这可以通过运行

    make CPUS=1 qemu-gdb
    
  • 如果alarmtest打印“alarm!”,则您已成功。

实验结果 满足实验任务要求

【MTI 6.S081 Lab】traps,MIT6S081,linux,c语言,系统架构,risc-v

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

可能是alarmtest在打印“alarm!”后在test0或test1中崩溃,或者alarmtest(最终)打印“test1 failed”,或者alarm test在未打印“test1passed”的情况下退出。要解决此问题,必须确保在完成alarm处理程序时,控制返回到用户程序最初被计时器中断的指令。您必须确保寄存器内容恢复到中断时的值,这样用户程序才能在alarm后不受干扰地继续运行。最后,您应该在每次alarm计数器达到值后“re-arm”它,以便周期性地调用处理程序。

作为起点,我们为您做出了一个设计决定:用户alarm处理程序需要在完成后调用sigreturn系统调用。以alarmtest.c中的periodic为例。这意味着您可以将代码添加到usertrap和sys_sigreturn中,这两个代码协同工作,使用户进程在处理完警报后能够正常恢复。

Hints

  • 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)。

    【MTI 6.S081 Lab】traps,MIT6S081,linux,c语言,系统架构,risc-v

    所以,至少要保存调用者寄存器。ra,t0-t6,a0-a7,还要保存pc

    在要进入alarm处理程序时,从蹦床页面保存一些寄存器。sigreturn时恢复那些寄存器。

    【MTI 6.S081 Lab】traps,MIT6S081,linux,c语言,系统架构,risc-v

  • 当计时器关闭时,让usertrap在struct proc中保存足够的状态,以便sigreturn能够正确地返回到中断的用户代码。

  • 防止对处理程序的重入调用——如果处理程序还没有返回,内核就不应该再次调用它。test2对此进行了测试。

    在Linux中使用的是一个掩码,待处理的向量。在这里,如果要处理很多信号,其实也可以放一个mask,这样一位就可以代表信号处理了。

  • 确保恢复a0。sigreturn是一个系统调用,其返回值存储在a0中。

通过test0、test1、test2和test3后,运行usertests q以确保没有破坏内核的任何其他部分。

解决方案

trap.c中的时钟中断处理程序

//
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
// 处理从用户空间而来的中断、异常或系统调用,被trampoline.S调用
// usertrap的任务是确定陷阱的原因,处理并返回
//
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);     // 此时已经陷入内核了,所以要将trap vec 指向处理内核陷阱的代码
  struct proc *p = myproc();
  // save user program counter. 保存用户程序计数器,再次保存是因为usertrap可能有一个进程切换,导致再次覆盖spec的值
  // 中断总是会被RISC-V的trap硬件关闭,所以到这里为止,sepc肯定是没有变过的
  p->trapframe->epc = r_sepc();
  // 根据触发trap的原因,RISC-V的SCAUSE寄存器会有不同的数字。 
  if(r_scause() == 8){
    // system call 陷阱来自系统调用
    if(killed(p))
      exit(-1);
    // sepc points to the ecall instruction,
    // but we want to return to the next instruction.
    p->trapframe->epc += 4;       // 因为系统调用后,用户执行导致系统调用的下一条指令,所以这里+4
    // an interrupt will change sepc, scause, and sstatus,
    // so enable only now that we're done with those registers.
    // 中断总是会被RISC-V的trap硬件关闭,所以在这个时间点,我们需要显式的打开中断。
    // 这样中断可以更快的服务,有些系统调用需要许多时间处理。
    intr_on();
    syscall();
  } else if((which_dev = devintr()) != 0){
    // ok 陷阱来自设备中断,devintr已经处理
  } else {
    // 是一个异常,内核会杀死错误进程
    printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
    printf("            sepc=%p stval=%p\n", r_sepc(), r_stval());
    setkilled(p);
  }

  // 在退出过程中,usertrap检查进程是已经被杀死还是应该让出CPU(如果这个陷阱是一个时钟中断)

  if(killed(p))
    exit(-1);

  // give up the CPU if this is a timer interrupt. 时钟中断
  if(which_dev == 2) {
    if (p->sigalarm.interval) {
      p->sigalarm.ticks++;
      if (p->sigalarm.ticks >= p->sigalarm.interval && !(p->handling_mask & HANDLING_ALARM)) {
        // 保存调用者保存寄存器
        memmove(&(p->sigalarm.trapframe), p->trapframe, sizeof(struct trapframe));
        p->trapframe->epc = p->sigalarm.handler;
        p->handling_mask |= HANDLING_ALARM;   // 标志中断正在处理
        p->sigalarm.ticks = 0;					// 重新计时
      }
    }
    yield();
  }

  usertrapret();
}

sys_sigalarm

信号处理程序的初始化,说明现在需要处理信号了,当handler等于0,意味着取消信号处理程序,或者说恢复默认。

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

sys_sigreturn

恢复调用者保存寄存器,然后改变程序计数器的指向,使得返回后从原来中断的地方恢复。文章来源地址https://www.toymoban.com/news/detail-618271.html

uint64
sys_sigreturn(void) {
  // 恢复调用者保存寄存器
  struct proc *p = myproc();
  memmove(p->trapframe, &(p->sigalarm.trapframe), sizeof(struct trapframe));
  p->handling_mask &= (~HANDLING_ALARM);
  return 0;
}

到了这里,关于【MTI 6.S081 Lab】traps的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MIT6.S081学习笔记--lec 1

    abstract H/W 抽象化硬件 multiplex 多路复用 isolation 隔离性 sharing 共享(进程通信,数据共享) security / access control 安全性/权限控制 performance 性能/内核开销 range of applications 多应用场景 操作系统应该提供的功能:1. 多进程支持 2. 进程间隔离 3. 受控制的进程间通信 xv6 :一种在本

    2024年02月16日
    浏览(37)
  • MIT6.S081 - Lecture1: Introduction and Examples

    理解操作系统的设计和实现 通过 XV6 操作系统动手实验,可以扩展或改进操作系统 Abstraction: 对硬件进行抽象 Multiplex: 在多个应用程序之间共用硬件资源 Isolation: 隔离性,程序出现故障时,不同程序之间不能相互干扰 Sharing: 实现共享,如数据交互或协同完成任务 Securi

    2024年04月15日
    浏览(50)
  • mit6.828 - lab5笔记(上)

    unix的文件系统相关知识 unix将可用的磁盘空间划分为两种主要类型的区域: inode区域 和 数据区域 。 unix为每个文件分配一个inode,其中保存文件的 关键元数据 ,如文件的stat属性和指向文件数据块的指针。 数据区域中的空间会被分成大小相同的数据块(就像内存管理中的分

    2024年02月02日
    浏览(34)
  • MIT6.S081 - Lecture3: OS Organization and System Calls

    使用操作系统的主要原因是为了实现 CPU 多进程分时复用以及内存隔离 如果没有操作系统,应用程序会直接与硬件进行交互,这时应用程序会直接使用 CPU,比如假设只有一个 CPU 核,一个应用程序在这个 CPU 核上运行,但是同时其他程序也需要运行,因为没有操作系统来帮助

    2024年04月22日
    浏览(41)
  • MIT 6.S081 Lab Three

    本文为 MIT 6.S081 2020 操作系统 实验三解析。 MIT 6.S081课程前置基础参考: 基于RISC-V搭建操作系统系列 在本实验中,您将探索页表并对其进行修改,以简化将数据从用户空间复制到内核空间的函数。 开始编码之前,请阅读xv6手册的第3章和相关文件: * kernel/memlayout.h* ,它捕获了

    2024年02月09日
    浏览(48)
  • MIT6.828/6.S081 Mac OS下搭建xv6和risc-v

    题外话: 其实我是一名非计算机专业的在校生,因为对软件开发和服务器开发很感兴趣,并且这方面的就业相对我来说资源比较充沛,所以就学习了mit6.828的实验 课程的学习直接跟着官网的schedule走就行,先看Lecture下提供的讲义和手册,然后完成相应的Lab,Lab共计10个,主要

    2024年03月09日
    浏览(38)
  • 【MIT 6.S081】Lab7: Multithreading

    本Lab比较简单,就是为xv6添加一个用户级的多线程功能,然后熟悉一下Linux下多线程编程。 笔者用时约2h 这一部分的代码不涉及内核代码,所以也比较简单,根据提示修改 user/uthread.c 中的代码即可。仿照内核中进程转换函数 swtch 的实现即可。首先,添加一个 context 上下文结

    2023年04月09日
    浏览(37)
  • MIT6.5830 Lab1-GoDB实验记录(四)

    标签:Golang 读写缓冲区我是一点思路都没有,所以得单独开篇文章记录。 实验补充 了解buffer、序列化与反序列化 这里的序列化,简单来说类似于把一个很长的字符串拆成一个个字符;反序列化就是把这一个个字符拼回成完整的字符串。此处我们需要根据所给的Tuple,转换为

    2024年02月06日
    浏览(51)
  • MIT6.5830 Lab1-GoDB实验记录(五)

    完成了Exercise 1,还有四个Exercise在等着我,慢慢来吧。 实验准备 了解缓冲池 缓冲池,俗称BP。相关的概念还有数据页和缓存页。页(Pages)的概念和操作系统中“分页”的概念是一样的,指的都是把逻辑地址空间分为若干同等大小的页,并从0开始编号。 而缓冲池(Buffer Po

    2024年02月05日
    浏览(47)
  • MIT 6.S081 教材第八章内容 -- 文件系统 -- 02

    MIT 6.S081 2020 操作系统 本文为MIT 6.S081课程第八章教材内容翻译加整理。 本课程前置知识主要涉及: C语言(建议阅读C程序语言设计—第二版) RISC-V汇编 推荐阅读: 程序员的自我修养-装载,链接与库 术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和

    2024年02月13日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包