MIT-6.S081实验四学习记录


这个实验不是很复杂,但是需要搞清楚trap的工作原理,也就是通过ECALL从用户态进入内核态后,操作系统做了哪些事。一是为了恢复用户态ECALL之前的状态,并保证其能执行调用前的下一条语句,使用SEPC寄存器存了PC值,然后使用了trapframe保存用户态切换到内核态前的寄存器值。

trap机制

首先把分支切换过来。

git fetch
git checkout traps
make clean

1、一些Q&A

首先按照要求,把fs.img文件编译一下,然后得到了 /kernel/call.asm 汇编文件,如下所示:

## /kernel/call.asm
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
   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

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

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>
  ...
  ...
  0000000000000630 <printf>:
  void
printf(const char *fmt, ...)
{
 630:	711d                	addi	sp,sp,-96
 632:	ec06                	sd	ra,24(sp)
 634:	e822                	sd	s0,16(sp)
 636:	1000                	addi	s0,sp,32
 638:	e40c                	sd	a1,8(s0)
 63a:	e810                	sd	a2,16(s0)
 63c:	ec14                	sd	a3,24(s0)
 63e:	f018                	sd	a4,32(s0)
 640:	f41c                	sd	a5,40(s0)
 642:	03043823          	sd	a6,48(s0)
 646:	03143c23          	sd	a7,56(s0)
Q1:Which registers contain arguments to functions?
 For example, which register holds 13 in main's call to printf?
A1:a0-a7; a2;

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.)
A2:可以看到,在main里,下面这段代码:
  26:	45b1    li	a1,12
  应该就是调用了f和g后的返回值,说明编译的时候,编译器将其作为inline函数来优化了
  因此g是内联到f的,然后f内联到main。
  
Q3:At what address is the function printf located?
A3:jalr 1536(ra) # 630 <printf> 这一句可以看到是跳转到630 处调用printf
   然后发现在0000000000000630 <printf>处定义了printf,所以地址显然。
   
Q4:What value is in the register ra just after the jalr to printf in main?
A4:ra寄存器里的值一定是完成printf函数调用后的下一条指令的值,这样才能跳转回来继续执行代码
   因此,ra的值是38.

Q5:Run the following code.

	unsigned int i = 0x00646c72;
	printf("H%x Wo%s",57616, &i);
      
  What is the output?
A5:这段代码,可以看到有两个类型的变量需要解析,一个是%x,这是十六进制,所以将57616转换为
  16进制,为E110,然后是无符号整型 i,按照ASCII码,00:NULL,64:d;6c:l;72:r,
  但是这里也提示了,RISC-V 是大端序的,所以输出应该是"HE110, W0rld".
  
Q6:In the following code, what is going to be printed after ? 
  (note: the answer is not a specific value.) Why does this happen? 'y='
	printf("x=%d y=%d", 3);
A6:这个问题比较好解释,因为只给a1寄存器赋值3,所以a2寄存器的值就是上一次被用完保留下来的值
  因此x=3,y=一个不确定的数。

2、backtrace()

这个实验主要是写一个打印栈的函数,然后按照要求定义在 /kernel/printf.c里,然后提示说要在 kernel/riscv.h 里面添加一个结构体,可以用于获取stack frames的地址。
打印格式如下:

backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

然后将结构体添加到 /kernel/riscv.h

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

然后定义backtrace函数。从上面结构体获取到的frame pointer指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 栈从高地址往低地址拓展,所以 fp 虽然是帧开始地址,但是地址比 sp 高。
栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
就是说当前frame的返回地址是fp -8,然后下一个frame的地址是fp -16。

void
backtrace()
{	
  printf("backtrace:\n");
  uint64 fp = r_fp();
  while(fp != PGROUNDUP(fp)){
  	uint64 ra = *(uint64*)(fp - 8);
  	printf("%p\n", ra);
  	fp = *(uint64*)(fp - 16);
  }
}

然后在sys_sleep中调用即可

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

  backtrace(); //加在这里
  
  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);
  return 0;
}

然后使用脚本测试:

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ ./grade-lab-traps backtrace test
make: 'kernel/kernel' is up to date.
== Test backtrace test == backtrace test: OK (1.1s) 

3、Alarm

这个实验是要添加两个系统调用(sigalarmsigreturn)。前者主要作用就是当CPU过了n个时钟周期,内核应该发起一个时钟中断,比如调用sigalarm(2,periodic) 就意味着每过两个时钟周期,就调用periodic,然后返回到中断前的地方。所以可以猜测,sigalarm(2,periodic) 负责发起定时器中断,然后做一些处理,而sigreturn 负责还原中断前的状态并返回。

test0、test1、test2

这一部分主要是添加好这两个系统调用,细节可以看实验二。

##/user/user.h
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

然后把其他的文件像实验二一样配置好,接下来在 proc.h 里进程结构体中加一些变量:间隔n、alarm函数fn、还有当前嘀嗒数ticks、用于恢复的trapframe、还有一个flag(用于记录有没有最近调用过,如果有就不要重复调用)

struct proc {
  ......
  int alarm_interval;            // 间隔n
  uint64 alarm_handler;     // alarm函数fn
  int alarm_ticks;              // 当前嘀嗒数ticks
  struct trapframe *alarm_trapframe; //用于恢复的trapframe
  int alarm_goingoff;  //标志
};

然后 /kernel/sysproc.c 里添加这两个函数,这里sys_sigalarm类似于做一个赋值操作,真正发挥作用是在定时器发生中断,也就是 ***if(which_dev == 2)***时。

## /kernel/sysproc.c
//将间隔设置为n,回调函数设置为fn,嘀嗒数置0
uint64 sys_sigalarm(void){
  int n;
  uint64 fn;
  if(argint(0, &n) < 0 || argaddr(0, &fn) < 0)
    return -1;
  struct proc *p = myproc();
  p->alarm_interval = n;
  p->alarm_handler = fn;
  p->alarm_ticks = 0;
  return 0;
}

//将当前trapframe修改为中断前的trapframe,然后嘀嗒数归0,调用flag归0
uint64 sys_sigreturn(void) {
  struct proc *p = myproc();
  *p->trapframe = *p->alarm_trapframe;
  //memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;
  return 0;
}

因为这个是进程变量,所以需要在进程创建时初始化,所以在 /kernel/proc.c 中进行初始化与销毁。

## /kernel/proc.c
static struct proc*
allocproc(void)
{
  ...
found:
  ...
  if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }
  
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;
  ...
  return p;
}

既然分配内存时初始化,那么释放时也要记得销毁重置。

## /kernel/proc.c
static void
freeproc(struct proc *p)
{
  ......
  if(p->alarm_trapframe)
    kfree((void*)p->alarm_trapframe);
  ......
  p->alarm_trapframe = 0;
  p->alarm_interval = 0;
  p->alarm_handler = 0;
  p->alarm_ticks = 0;
  p->alarm_goingoff = 0;
}

接下来就要在定时器发生中断时,执行alarm 函数以及记录中断前的状态(trapframe)。

## /kernel/trap.c
// give up the CPU if this is a timer interrupt.
  if(which_dev == 2){
    //说明调用了sisalarm函数,
    if(!(p->alarm_interval == 0 && p->alarm_handler == 0)) {
    //消耗完了n个CPU的嘀嗒数
      if(p->alarm_ticks <= 0){
        p->alarm_ticks--; //减少一个嘀嗒数,当n次中断后,查看之前有没有被调用过(标志位为0)
        if(!(p->alarm_goingoff)) { 
          // 记录好中断前的trapframe
          *p->alarm_trapframe = *p->trapframe; 
          // 将当前程序计数器设置为alarm函数fn
          p->trapframe->epc = p->alarm_handler;
          p->alarm_goingoff = 1;
      }
     }
    }
    yield();
  }

到这里代码就写完了,虽然不多,但是需要深刻理解工作原理,尤其是trap的工作机制,比如说进入后恢复中断前状态、保证不被重复调用的标志位等等。

最后用脚本测试一下功能是否ok即可。

jimmy@ubuntu:~/xv6-test/xv6-labs-2020$ make grade
== Test answers-traps.txt == answers-traps.txt: OK 
== Test backtrace test == 
$ make qemu-gdb
backtrace test: OK (6..s) 
== Test running alarmtest == 
$ make qemu-gdb
(1.9s) 
== Test   alarmtest: test0 == 
  alarmtest: test0: OK 
== Test   alarmtest: test1 == 
  alarmtest: test1: OK 
== Test   alarmtest: test2 == 
  alarmtest: test2: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (78.2s) 
== Test time == 
time: OK 
Score: 85/85

总结

这个实验重在理解trap机制,如果有基础的一定知道,最重要的就是进入内核后,要能够恢复中断前的状态继续执行,然后这个实验也帮助回顾了一下系统调用怎么写,可以说收获颇丰。一定要看lecture 6的视频,Robert讲的非常好,手把手教你如何gdb调试。
[1]: https://pdos.csail.mit.edu/6.S081/2020/labs/traps.html
[2]: https://zhuanlan.zhihu.com/p/626826395
[3]: https://blog.miigon.net/posts/s081-lab4-traps/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值