您的当前位置:首页正文

5.Lab four —— Trap

2024-12-03 来源:个人技术集锦

首先切换traps分支

git checkout traps
make clean

RISC-V assembly

代码:

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

汇编后指令:

0000000000000000 <g>:
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int g(int x) {
   0:	1141                	addi	sp,sp,-16    # 栈顶指针下移16字节
   2:	e422                	sd	s0,8(sp)    # 存栈底指针s0/fp到sp+8的位置
   4:	0800                	addi	s0,sp,16    # 将sp+16即原sp的值作为新的栈帧
  return x+3;
}
   6:	250d                	addiw	a0,a0,3    # a0=a0+3, a0即为传入参数x又为返回值
   8:	6422                	ld	s0,8(sp)    # 从sp+8恢复原栈帧到s0
   a:	0141                	addi	sp,sp,16    # 回收栈顶指针
   c:	8082                	ret    # 返回

000000000000000e <f>:    # 与g()函数系统,相当于将g()内联了

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

000000000000001c <main>:

void main(void) {
  1c:	1141                	addi	sp,sp,-16    # 栈顶指针下移16字节
  1e:	e406                	sd	ra,8(sp)    # 存返回地址到sp+8的位置
  20:	e022                	sd	s0,0(sp)    # 存栈底指针s0/fp到sp的位置
  22:	0800                	addi	s0,sp,16    # 更新栈帧s0
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13    # 加载13到a2寄存器
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0    # 将pc+0加载到a0
  2c:	7b050513          	addi	a0,a0,1968 # 7d8 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0    # 将pc+0<<12加载到ra寄存器
  34:	600080e7          	jalr	1536(ra) # 630 <printf>    # 将pc设置为ra+1536,并将pc+4写入ra(即进行函数跳转)
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	27e080e7          	jalr	638(ra) # 2b8 <exit>

1.哪些寄存器保存函数的参数?例如,在main对printf的调用中,哪个寄存器保存13?

答:a0-a7,a2,a0保存格式化字符串,a1保存f(8)+1,a2保存13

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

答:进行了内敛优化,没有调用的代码,g(x)被内联到f(x)中,f(x)又被内联到main中

auipc指令格式:auipc rd,imm , 将当前PC的高20位与立即数imm相加,然后将结构存储在目标寄存器rd中

4.在main中printf的jalr之后的寄存器ra中有什么值?

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

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

答:输出为"He110 World",57616 = 0xE110,0x00646c72若是小端存储为72-6c-64-00,对应ASCLL码表为72:r, 6c:l, 64:d, 00:充当字符串结尾标识

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

答:i在内存中分布应该仍旧是72-6c-64-00,不过由于是大端存储,所以要改成0x726c6400,而57616不需要更改,因为是通过二进制数字读取的而非单个字符

6.在下面的代码中,“y=”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况? printf("x=%d y=%d", 3);

答;输出的是一个受调用前的代码影响的“随机”的值。因为 printf 尝试读的参数数量比提供的参数数量多。 第二个参数 3 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会 包含调用发生前的任何已经在里面的值。 即原本需要两个参数,却只传入了一个,因此y=后面打印的结果取决于之前a2中保存的数据

Bacektrace

在kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示,同时退出后执行addr2line -e kernel/kernel并且把刚才输出的三行粘贴进去可以得到看到执行的对应文件和行数

1.将下列函数添加到kernel/riscv.h中用于获取正在执行的函数的帧指针并保存到s0寄存器,该函数用内联汇编来获取s0

2.在kernel/defs.h中添加backtrace的原型

3.在kernel/printf.c中实现该函数

4.在kernel/sysproc的sys_sleep中插入对该函数的调用

我们可以看看user/bttest里面实现了什么

其实就是运行了一下sleep,然后就会到trap -》syscall -》 sysproc 里面执行到sys_sleep,最后由于事先在sys_sleep里面进行了埋点,那么就会执行backtrace(),打印出这几个调用关系

测试

一些疑问

按照栈的调用关系,sysproc 、syscall、systrap的关系应该如下图所示

Alarm

1.在user/user.h中添加两个系统调用的原型函数

在user/usys.pl脚本中添加两个系统调用相应的entry,然后在kernel/syscall.h和kernel/syscall.c中添加相应声明

3.在sysproc.c中编写sys_sigalarm()函数,将interval和handler值存到当前进程的proc结构体中

4.在kernel/proc.c中的allocproc()函数中将上述三个新增字段初始化赋值

5.每次经过设置好的时钟间隔,会引发时钟中断,调用kernel/trap.c里面的usertrap(),对于时钟中断which_dev变量的值为2,由此便可以单独对时钟中断进行操作

// lab4-3
  if(which_dev == 2){   // timer interrupt
    // increase the passed ticks
    if(p->interval != 0 && ++p->passedticks == p->interval){
      p->passedticks = 0;
      p->trapframe->epc = p->handler;   // execute handler() when return to user space 
    }
  }

6.修改makefile文件的UPROGS部分,添加alarmtest.c的编译

7.sigalarm(interval, handler)sigreturn() 两个函数是配合使用的, 在 handler 函数返回前会调用 sigreturn()

前面的做法是调用定时函数实际上修改trapframe->epc进而在返回到用户空间时调用定时函数,但这也产生了问题,原本的ec被覆盖,无法回到中断前用户代码执行的位置,因此需要考虑在 sigalarm() 函数中将寄存器值进行保存, 在 sigreturn() 函数中进行恢复. 这样在执行完 sigreturn() 后程序能够回到原来的执行位置.

在系统调用时用户代码中断时会将寄存器记录到 p->trapframe 中, 而前者由于在 usertrap() 覆盖了 p->trapframe->epc, 才能够执行定时函数, 执行完后又会导致一些寄存器的值被修改. 因此, 考虑在 struct proc 中保存一个 trapframe 的副本, 在覆盖 epc 之前先保存副本, 然后在 sys_sigreturn() 中将副本还原到 p->trapframe 中, 从而在 sigreturn 系统调用结束后恢复用户寄存器状态时能够将执行定时函数前的寄存器状态进行恢复.

8.在kernel/trap.c中的usertrap中覆盖p->trapfram->epc前先把trapframe副本存好

9.编写sysproc.c中的sys_sigreturn

10.一些理解:

开始得到kernel/proc.c中的allocproc()初始化p->trapframecopy为0,一开始没有副本。

同时需要注意的是myproc()->passedticks = 0的时机从原来的usertrap函数改成了sys_sigreturn。因为在usertrap重置后可能会导致重入(例如清零后后面重新计数再次进入handler而此时handler还没执行完)

而放在sys_sigreturn() 之后, 即在最后函数返回前才会清零,此时handler已经执行完成,则不会导致重入。

这个图是sygalarm的处理流程,handler在user/alarmtest.c中最后调用了sigreturn(),对应的就是下图中右边部分,若是到了触发条件回到用户态执行handler但是还得通过sigreturn()进入到内核态把trapframe复制回来

显示全文