笔记 Lab4: Traps | 中断陷阱

RISC-V assembly (easy)

It will be important to understand a bit of RISC-V assembly, which you
were exposed to in 6.004. There is a file user/call.c in your xv6
repo. make fs.img compiles it and also produces a readable assembly
version of the program in user/call.asm.

Read the code in call.asm for the functions g, f, and main. The
instruction manual for RISC-V is on the reference page. Here are some
questions that you should answer (store the answers in a file
answers-traps.txt):

阅读 call.asm,以及 RISC-V 指令集教程,回答问题。(学习 RISC-V 汇编)

Q: 哪些寄存器存储了函数调用的参数?举个例子,main 调用 printf 的时候,13 被存在了哪个寄存器中?
A: a0-a7; a2;

Q: main 中调用函数 f 对应的汇编代码在哪?对 g 的调用呢? (提示:编译器有可能会内链(inline)一些函数)
A: 没有这样的代码。 g(x) 被内链到 f(x) 中,然后 f(x) 又被进一步内链到 main() 中

Q: printf 函数所在的地址是?
A: 0x0000000000000628, main 中使用 pc 相对寻址来计算得到这个地址。

Q: 在 main 中 jalr 跳转到 printf 之后,ra 的值是什么?
A: 0x0000000000000038, jalr 指令的下一条汇编指令的地址。

Q: 运行下面的代码

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

输出是什么?
如果 RISC-V 是大端序的,要实现同样的效果,需要将 i 设置为什么?需要将 57616 修改为别的值吗?
A: "He110 World"; 0x726c6400不需要修改57676,i需要修改为:0x726c6400;


Q: 在下面的代码中,'y=' 之后会打印什么? (note: 答案不是一个具体的值) 为什么?

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


A: 输出的是一个受调用前的代码影响的“随机”的值。因为 printf 尝试读的参数数量比提供的参数数量多。
第二个参数 `3` 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会
包含调用发生前的任何已经在里面的值。

Backtrace (moderate)

Implement a backtrace() function in kernel/printf.c. Insert a call to
this function in sys_sleep, and then run bttest, which calls
sys_sleep. Your output should be as follows:

backtrace: 0x0000000080002cda 0x0000000080002bb6 0x0000000080002898
After bttest exit qemu. In your terminal: the addresses may be
slightly different but if you run addr2line -e kernel/kernel (or
riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the
above addresses as follows:
$ 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

添加 backtrace 功能,打印出调用栈,用于调试。

在 defs.h 中添加声明

// defs.h
void            printf(char*, ...);
void            panic(char*) __attribute__((noreturn));
void            printfinit(void);
void            backtrace(void); // new

在 riscv.h 中添加获取当前 fp(frame pointer)寄存器的方法:

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

fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 (栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高)
栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。
查看 call.asm,可以看到一个函数的函数体最开始首先会扩充一个栈帧给该层调用使用,在函数执行完毕后再回收,例子:


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:	7c050513          	addi	a0,a0,1984 # 7e8 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	610080e7          	jalr	1552(ra) # 640 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	27e080e7          	jalr	638(ra) # 2b8 <exit>

注意栈的生长方向是从高地址到低地址,所以扩张是 -16,而回收是 +16。

实现 backtrace 函数:
使用循环判断有无到达栈底,没有到达的话继续向上-16,到达上一个栈的开始位置进行回溯

// printf.c

void backtrace() {
  uint64 fp = r_fp();
  while(fp != PGROUNDUP(fp)) { // 如果已经到达栈底
    uint64 ra = *(uint64*)(fp - 8); // return address
    printf("%p\n", ra);
    fp = *(uint64*)(fp - 16); // previous fp
  }
}

在 sys_sleep 的开头调用一次 backtrace()

// sysproc.c
uint64
sys_sleep(void)
{
  int n;
  uint ticks0;

  backtrace(); // print stack backtrace.

  if(argint(0, &n) < 0)
    return -1;
  
  // ......

  return 0;
}

编译运行:

./grade-lab-traps backtrace
== Test backtrace test == backtrace test: OK (1.5s) 

Alarm (hard)

In this exercise you’ll add a feature to xv6 that periodically alerts
a process as it uses CPU time. This might be useful for compute-bound
processes that want to limit how much CPU time they chew up, or for
processes that want to compute but also want to take some periodic
action. More generally, you’ll be implementing a primitive form of
user-level interrupt/fault handlers; you could use something similar
to handle page faults in the application, for example. Your solution
is correct if it passes alarmtest and usertests.

根据之前系统调用的实验步骤,添加sigalarm 和sigreturn

int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

首先,在 proc 结构体的定义中,增加 alarm 相关字段:

alarm_interval:时钟周期,0 为禁用
alarm_handler:时钟回调处理函数
alarm_ticks:下一次时钟响起前还剩下的 ticks 数
alarm_trapframe:时钟中断时刻的 trapframe,用于中断处理完成后恢复原程序的正常执行
alarm_goingoff:是否已经有一个时钟回调正在执行且还未返回(用于防止在 alarm_handler 中途闹钟到期再次调用 alarm_handler,导致 alarm_trapframe 被覆盖)

struct proc {
  // ......
  int alarm_interval;          // Alarm interval (0 for disabled)
  void(*alarm_handler)();      // Alarm handler
  int alarm_ticks;             // How many ticks left before next alarm goes off
  struct trapframe *alarm_trapframe;  // A copy of trapframe right before running alarm_handler
  int alarm_goingoff;          // Is an alarm currently going off and hasn't not yet returned? (prevent re-entrance of alarm_handler)
};

sigalarm 与 sigreturn 具体实现:

// sysproc.c
uint64 sys_sigalarm(void) {
  int n;
  uint64 fn;
  if(argint(0, &n) < 0)
    return -1;
  if(argaddr(1, &fn) < 0)
    return -1;
  
  return sigalarm(n, (void(*)())(fn));
}

uint64 sys_sigreturn(void) {
	return sigreturn();
}
// trap.c
int sigalarm(int ticks, void(*handler)()) {
  // 设置 myproc 中的相关属性
  struct proc *p = myproc();
  p->alarm_interval = ticks;
  p->alarm_handler = handler;
  p->alarm_ticks = ticks;
  return 0;
}

int sigreturn() {
  // 将 trapframe 恢复到时钟中断之前的状态,恢复原本正在执行的程序流
  struct proc *p = myproc();
  *p->trapframe = *p->alarm_trapframe;
  p->alarm_goingoff = 0;
  return 0;
}

在 proc.c 中添加初始化与释放代码:

// proc.c
static struct proc*
allocproc(void)
{
  // ......

found:
  p->pid = allocpid();

  // Allocate a trapframe page.
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    release(&p->lock);
    return 0;
  }

  // Allocate a trapframe page for alarm_trapframe.
  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;
}

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;
  p->state = UNUSED;
}

在 usertrap() 函数中,实现时钟机制具体代码:

void
usertrap(void)
{
  int which_dev = 0;

  // ......

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

  // give up the CPU if this is a timer interrupt.
  // if(which_dev == 2) {
  //   yield();
  // }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2) {
    if(p->alarm_interval != 0) { // 如果设定了时钟事件
      if(--p->alarm_ticks <= 0) { // 时钟倒计时 -1 tick,如果已经到达或超过设定的 tick 数
        if(!p->alarm_goingoff) { // 确保没有时钟正在运行
          p->alarm_ticks = p->alarm_interval;
          // jump to execute alarm_handler
          *p->alarm_trapframe = *p->trapframe; // backup trapframe
          p->trapframe->epc = (uint64)p->alarm_handler;
          p->alarm_goingoff = 1;
        }
        // 如果一个时钟到期的时候已经有一个时钟处理函数正在运行,则会推迟到原处理函数运行完成后的下一个 tick 才触发这次时钟
      }
    }
    yield();
  }

  usertrapret();
}

这样,在每次时钟中断的时候,如果进程有已经设置的时钟(alarm_interval != 0),则进行 alarm_ticks 倒数。当 alarm_ticks 倒数到小于等于 0 的时候,如果没有正在处理的时钟,则尝试触发时钟,将原本的程序流保存起来(*alarm_trapframe = *trapframe),然后通过修改 pc 寄存器的值,将程序流转跳到 alarm_handler 中,alarm_handler 执行完毕后再恢复原本的执行流(*trapframe = *alarm_trapframe)。

编译运行:

./grade-lab-traps alarmtest
make: 'kernel/kernel' is up to date.
== Test running alarmtest == (3.9s) 
== Test   alarmtest: test0 == 
  alarmtest: test0: OK 
== Test   alarmtest: test1 == 
  alarmtest: test1: OK 
== Test   alarmtest: test2 == 
  alarmtest: test2: OK
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值