【MIT 6.S081】实验四:traps (实验暂停)

前言

第三章内容是关于使用系统陷入来实现系统调用的。

实验使用win10 + wsl2 Ubuntu 20.04完成了前两个实验。

内容总览

本实验有三个内容。

包括:

  • RISC-V assembly: 理解汇编代码。
  • Backtrace: 打印堆栈。
  • Alarm: 定期调用用户级回调函数。

reference

十分感谢以上大佬开源的贡献。知识参考:课程内容翻译 github

内容1:RISC-V assembly

在您的 xv6 存储库中有一个文件 user/call.c。 make fs.img 对其进行编译,并在 user/call.asm 中生成程序的可读汇编版本。

阅读 call.asm 中函数 g、f 和 main 的代码。

这部分没有需要写的代码,要做的就是理解c代码里面的函数以及对照的汇编代码。

call.c

/*
	函数g
*/
//c代码
int g(int x)
{
  return x + 3;
}

//汇编
int g(int x) {
   0:	1141                	addi	sp,sp,-16
   2:	e422                	sd	s0,8(sp)
   4:	0800                	addi	s0,sp,16
  return x+3;
}
   6:	250d                	addiw	a0,a0,3
   8:	6422                	ld	s0,8(sp)
   a:	0141                	addi	sp,sp,16
   c:	8082                	ret

/*
	函数f
*/
//c代码
int f(int x)
{
  return g(x);
}
//汇编
000000000000000e <f>:

int f(int x) {
   e:	1141                	addi	sp,sp,-16
  10:	e422                	sd	s0,8(sp)
  12:	0800                	addi	s0,sp,16
  return g(x);
}
  14:	250d                	addiw	a0,a0,3
  16:	6422                	ld	s0,8(sp)
  18:	0141                	addi	sp,sp,16
  1a:	8082                	ret

/*
	main函数
*/
//c代码
void main(void)
{
  printf("%d %d\n", f(8) + 1, 13);
  exit(0);
}

//汇编代码
000000000000001c <main>:
void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	7b050513          	addi	a0,a0,1968 # 7d8 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	600080e7          	jalr	1536(ra) # 630 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	27e080e7          	jalr	638(ra) # 2b8 <exit>
  1. 哪些寄存器包含函数的参数? 例如,在 main 对 printf 的调用中,哪个寄存器保存了 13?

    • 为了回答这个问题,我在call.c里面添加了三行代码。观察可以知道,参数通常会存在寄存器a当中…

    call.c

      // my test 1
      printf("x=%d y=%d", 3);
      // my test 2
      printf("x=%d y=%d", 3, 6);
      // my test 3
      printf("x=%d y=%d", 3, 6, 9);
      
    /*
    	以下是对应的汇编代码
    */
        // my test 1
      printf("x=%d y=%d", 3);
      38:	458d                	li	a1,3
      3a:	00000517          	auipc	a0,0x0
      3e:	7de50513          	addi	a0,a0,2014 # 818 <malloc+0xee>
      42:	00000097          	auipc	ra,0x0
      46:	62a080e7          	jalr	1578(ra) # 66c <printf>
      // my test 2
      printf("x=%d y=%d", 3, 6);
      4a:	4619                	li	a2,6
      4c:	458d                	li	a1,3
      4e:	00000517          	auipc	a0,0x0
      52:	7ca50513          	addi	a0,a0,1994 # 818 <malloc+0xee>
      56:	00000097          	auipc	ra,0x0
      5a:	616080e7          	jalr	1558(ra) # 66c <printf>
      // my test 3
      printf("x=%d y=%d", 3, 6, 9);
      5e:	46a5                	li	a3,9
      60:	4619                	li	a2,6
      62:	458d                	li	a1,3
      64:	00000517          	auipc	a0,0x0
      68:	7b450513          	addi	a0,a0,1972 # 818 <malloc+0xee>
      6c:	00000097          	auipc	ra,0x0
      70:	600080e7          	jalr	1536(ra) # 66c <printf>
    
    • 24: 4635 li a2,13 知道13被存在了寄存器a2上。
  2. main 的汇编代码中对函数 f 的调用在哪里? 对 g 的调用在哪里? (提示:编译器可能内联函数。)

    • 无,直接被内联了
  3. 函数 printf 位于哪个地址?

可以从call.asm中直接找到0000000000000630 <printf>:.

也可以通过计算

  2c:	7b050513          	addi	a0,a0,1968 # 7d8 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	600080e7          	jalr	1536(ra) # 630 <printf>
  • auipc:是指将当前立即数向右移动12位,然后加上 pc 寄存器的值,赋给 ra 寄存器。pc = 0x30,立即数是0x0,因此ra = 0x30
  • jalr:函数跳转。JALR指令格式为 jalr rs rd,无条件跳转到由寄存器rs指定的指令,**并将下一条指令的地址保存到寄存器ra。**所以目标地址printf为1536(ra),即0x600 + 0x30 = 0x630
  1. 在 jalr 到 main 中的 printf 之后,寄存器 ra 中有什么值?

    • 在第三点已经解释了jalr的作用了,下一条指令的地址是0x38
  2. Run the following code.

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

    What is the output? Here’s an ASCII table that maps bytes to characters.

这题有点意思。

  • 输出是He110 World.
    • %x表示输出十六进制,而57616的十六进制就是e110
    • %s表示输出字符串,当是小端序表示的时候,内存中存放的数是:72 6c 64 00,刚好对应rld。当是大端序的时候,则反过来了,因此需要将 i 以16进制数的方式逆转一下。
  1. 在下面的代码中,'y='之后会打印什么? (注意:答案不是特定值。)为什么会发生这种情况?

看第一个问题的test1,它打印一个参数,参数被加载到了a1寄存器当中;看第一个问题的test2,它打印两个参数,参数被加载到了a1、a2寄存器当中;所以这题答案就是,a2有什么就打印什么。

内容2:Backtrace

在debug的时候,如果发生错误了,打印函数调用的堆栈信息是相当有用的。

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

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

执行bttest 后退出 qemu。 在您的终端中:地址可能略有不同,但如果您运行 addr2line -e kernel/kernel并剪切并粘贴上述地址,如下所示:

   $ addr2line -e kernel/kernel
    0x0000000080002de2
    0x0000000080002f4a
    0x0000000080002bfc
    Ctrl-D
  
You should see something like this:
    kernel/sysproc.c:74
    kernel/syscall.c:224
    kernel/trap.c:85

最重要的理解:The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your backtrace should use these frame pointers to walk up the stack and print the saved return address in each stack frame.
意思是:编译器在每个堆栈帧中放入一个帧指针,该指针保存调用者帧指针的地址。 brcktrace函数应该使用这些帧指针去向上遍历堆栈,并在每个堆栈帧中打印被保存的返回地址

提示和理解

  1. traceback()的函数原型添加到 kernel/defs.h,以便您可以在sys_sleep 中调用traceback()
  2. The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:
static inline uint64
r_fp()
{
  uint64 x;
  asm volatile("mv %0, s0" : "=r" (x) );
  return x;
}

并在traceback()中调用此函数以读取当前帧指针。 此函数使用内联汇编来读取 s0。

  1. 请注意,返回地址(return address)位于距堆栈帧的帧指针(fp)的固定偏移量 (-8) 处,而保存的帧指针(saved frame pointer)位于距帧指针的固定偏移量 (-16) 处。如下图所示,fp-16指向上一个栈帧,可以用来遍历。

请添加图片描述

  1. xv6 为 xv6 内核中的每个堆栈在 PAGE 对齐地址处分配一个页面。 您可以使用PGROUNDDOWN(fp) PGROUNDUP(fp) 计算堆栈页面的顶部和底部地址(请参阅 kernel/riscv.h。这些数字有助于回溯终止其循环。
  2. 一旦你的回溯工作正常,从 kernel/printf.c 中的 panic 调用它,这样你就可以看到 kernelpanic 时的回溯。

做法和代码

参考ref2,可以轻松通过,也可以相像提示说的,使用 addr2line -e kernel/kernel打印所在的函数行数

请添加图片描述

核心代码

void backtrace(void) {
  uint64 fp = r_fp(), top = PGROUNDUP(fp);
  printf("backtrace:\n");
  //fp-16就是上一个栈帧,
  //fp-8就是当前的返回地址。
  for(; fp < top; fp = *((uint64*)(fp-16))) {
    printf("%p\n", *((uint64*)(fp-8)));
  }
}

内容3:Alarm

这个实验好长!

在本练习中,您将向 xv6 添加一个功能,该功能会在进程使用 CPU 时间时定期提醒它
这对于想要限制进程占用CPU的时间,或者对于想要采取一些定期操作的进程可能很有用。 更一般地说,您将实现一种原始形式的用户级中断/故障处理程序; 例如,您可以使用类似的东西来处理应用程序中的页面错误。

如果它通过了alarmtest usertests,则答案正确。

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

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

alarmtesttest0 中调用 sigalarm(2,periodic) 以要求内核每 2 个滴答声强制调用一次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

这里是整个实验的第一步,修改内核以跳转到用户空间的警报处理程序handler开始。尽管跳转完系统程序会崩溃,但是无伤大雅,只有把test1完成了才会全部完成。

下面是提示

  1. 您需要修改 Makefile 以使 alarmtest.c 编译为 xv6 用户程序。
  2. 放入 user/user.h 的正确声明是:
    int sigalarm(int ticks, void (*handler)());
    int sigreturn(void);
  1. 更新 user/usys.pl(生成 user/usys.S)、kernel/syscall.hkernel/syscall.c 以允许 alarmtest 调用 sigalarmsigreturn 系统调用。
  2. 现在,您的 sys_sigreturn 应该只返回零。
  3. 您的 sys_sigalarm() 应该将警报间隔指向处理函数的指针存储在 proc 结构(在 kernel/proc.h 中)的新字段中。
  4. 您需要跟踪自上次调用(或留到下一次调用)到进程的警报处理程序以来已经过去了多少tricks; 为此,您还需要 struct proc 中的一个新字段。 您可以在 proc.c 中的 allocproc() 中初始化 proc 字段。
  5. 每个tick,硬件时钟都会强制中断,该中断在 kernel/trap.c 中的 usertrap() 中处理。
  6. 如果有计时器中断,您只想操纵进程的警报滴答声; 你会用到下面的代码
  if(which_dev == 2) ...
  1. 仅当进程有未完成的计时器时才调用警报函数。 请注意,用户的警报函数的地址可能为 0(例如,在 user/alarmtest.asm 中,周期性在地址 0 处)。

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

  3. 如果您告诉 qemu 只使用一个 CPU,那么使用 gdb 查看陷阱会更容易,您可以通过运行make CPUS=1 qemu-gdb

  4. 如果alarmtest 打印出**“alarm!”**,你就成功了。

后话

关于为什么暂停了实验,主要有两个原因:

  1. 实验内容不足够有趣,在我当前的理解基础上,有些枯燥,或者说我还get不到很有用的点,这一点CSAPP是给足了我惊喜,而6.S081却没有这种感觉。
  2. 五月份开始后要忙更多学校的项目、课程项目。

有点可惜,后面会开始新的论文研究以及关于【Unix环境高级环境编程】的学习,加油!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值