文章目录
- 一、RISC-V assembly
- 简介
- 问题
- 二、Backtrace
- 简介
- 注意
- 实验代码
- 实验结果
- 三、Alarm
- 简介
- 注意
- 实验代码
- 实验结果
一、RISC-V assembly
简介
- git checkout traps,切换到traps分支
user/call.c
文件在我们输入make fs.img
之后会被汇编为call.asm
文件,阅读该汇编文件中的函数:f、h、main
- call.c 中的代码比较简单:
#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);
}
问题
我们需要回答以下问题:
- 哪些寄存器用于传参? 例如,
main
函数调用printf
时持有13 这个参数
RISC-V 寄存器作用如下图:
通过main
函数的汇编代码,可以知道参数 13 使用a2
寄存器传递
main
函数对应的汇编代码在哪里调用了函数f ? 函数 g 的调用在哪里?(编译器可能会优化某些函数为内联函数)
由问题一查看的汇编可知,main
函数并未调用函数f
,而是直接由编译器优化得到了结果printf
函数的地址是什么?
printf
的地址为0x64a
(虚拟地址),main
函数中的调用也能说明这一点
main
函数中使用jalr
到printf
之后,ra
寄存器的值是什么?
jalr
指令执行时,会将pc + 4
的值存放到ra
寄存器中作为返回地址,因此ra
的值为0x38
- 运行下面的代码,输出是什么?(输出取决于大小端,RISC-V为小端模式,如果是大端,相同输出需要将 57616 改为多少?)
- unsigned int i = 0x00646c72;
- printf(“H%x Wo%s”, 57616, &i);
- 输出为:
He110 World
- 因为,57616对应16进制的 0xe110,0x00646c72 以小端存储,实际为 0x6c720064,
0x6c
对应r
,0x72
对应l
,0x64
对应d
(00
代表NULL
,不显示)。如果是大端,则高地址在高处,因此应该存储为0x6c720064
或者0x6c7264
- 下面的代码,
y=
后面的输出是什么?为什么?- printf(“x=%d y=%d”, 3);
- 输出为:乱码,因为参数传递时使用的寄存器是确定的,因此如果第二个参数不传递,依然还是会用
a2
寄存器传递,此时a2
中的值是caller
中的残留值
二、Backtrace
简介
当发生错误时,打印当前的调用栈。每个函数栈帧包含一个返回地址以及一个指向caller栈帧
的 frame pointer
(我理解就是caller的rsp)
在kernel/printf.c
中实现一个 backtrace()
,在 sys_sleep
中插入对该函数的调用,然后运行 bttest
,该用户程序会调用 sleep
系统调用,此时应该打印堆栈
bttest
在qemu
中执行完毕之后,新开一个窗口执行 addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel)
,将有如下输出:
阅读:kernel/sysproc.c:74、kernel/syscall.c:224、kernel/trap.c:85
注意
kernel/defs.h
中添加backtrace()
函数的声明- gcc产生汇编时,会将栈指针存放在
s0
寄存器,在kernel/riscv.h
中添加如下代码:
static inline uint64
r_fp()
{uint64 x;asm volatile("mv %0, s0" : "=r" (x) ); //含义为读取 s0 寄存器中的值,赋值给 x 并返回 xreturn x;
}
返回地址
位于栈指针 - 8
的位置,保存的栈指(caller的栈指针地址)
针位于当前函数的栈指针
的-16
处(栈指针即rbp
)- 你需要识别最后一个栈帧并结束,由于函数栈最大为4KB,因此可以使用
PGROUNDDOWN(fp) (see kernel/riscv.h)
来判断是否到达最后一个函数栈
实验代码
思路:
1、首先 kernel/riscv.h
添加要求的代码,kernel/defs.h
声明 backtrace()
2、 backtrace()
的逻辑为:使用 r_fq()
获取当前栈指针,打印栈指针 - 8
的地址(即调用栈),并 -16
,得到上一个栈帧的地址,依次类推,直到地址等于 PGROUNDDOWN(fp) (see kernel/riscv.h)
kernel/riscv.h
static inline uint64
r_fp()
{uint64 x;asm volatile("mv %0, s0" : "=r" (x) ); //含义为读取 s0 寄存器中的值,赋值给 x 并返回 xreturn x;
}
kernel/defs.h
void backtrace(void);
kernel/printf.c
void backtrace(void){// 获取当前栈帧uint64 addr = r_fp();// 向上遍历while(addr != PGROUNDDOWN(addr)){printf("%p\n", *(uint64 *)(addr - 8));addr = *(uint64 *)(addr - 16);}
}
kernel/sysproc.c
uint64
sys_sleep(void)
{int n;uint ticks0;argint(0, &n);//...其他代码backtrace();return 0;
}
实验结果
三、Alarm
简介
添加一项新的功能,定时报警,即简单的用户级中断/故障处理程序。为以后缺页中断打下基础。
添加一个新的系统调用 sigalarm(interval, handler)
,如果用户态应用调用 sigalarm(n, fn)
,那么每当该程序消耗 n
个cpu ticks
,内核会调用一次 fn
。当调用 sigalarm(0, 0)
时停止。
注意
- makefile 中添加
user/alarmtest.c
- 正确的结果输出如下:
实验代码
思路:proc中添加三个字段,分别记录用户设置的ticks
,cur_ticks
以及callback
,系统调用则是设置进程的 ticks
以及callback
,在处理时钟中断时,给当前的进程cur_ticks
加1,判断和ticks
是否相等,相等则调用callback
。
时钟中断的处理函数为 clockintr()
,当用户态进程运行时来了时钟中断,调用路径为:usertrap---判断类型---devintr
详细流程:参考 xv6-book 第四章
trap发生时, cpu 硬件会做一些准备 (book:44页),该步骤设置了SIE位, 保存 pc,设置trap原因,进入内核态(但是不切换内核页表以及内核栈),执行 trap 处理函数(stvec
寄存器保存的地址)
如果是在用户态发生 trap,此时处理函数为 uservec
,该函数保存了用户寄存器以及其他状态,切换内核页表(所以trap发生时,硬件只保证切换到内核态,即cpu设置为管理模式,但是并不会切换页表,切换页表是在trap处理函数中做的),最后跳转到 usertrap()
执行。
因此,我们需要在 usertrap
函数中添加对于时钟中断的判断逻辑
以及用户callback的调用
逻辑(需要保证不可重入,即我们如果在执行 callback
的时候再次被时钟中断trap
,此时不应该再次执行callback
,所以还需要在proc.h
中添加一个字段用于防止重入)。usertrap
中本身有对于时钟中断的判断逻辑
,即进程调度的部分,具体代码如图(所以照猫画虎即可):
最后还有一点需要注意,我们的callback
函数是应该在用户态运行的(因为内核态无法解析用户态的虚拟地址,所以无法调用callback
),此时我们处于内核态,因此我们需要保存上下文
,切换到用户态
,执行完callback之后
,切换回内核态
,恢复上下文
,继续usertrap
的执行。
如何切换到内核态呢?此时我们借助 usertrapret()
来返回用户态,所以我们需要更改trap的返回地址
,当前 trap 的返回地址
为 p->trapframe->epc += 4;
(kernel/trap.c:61)。所以,我们首先保存一下当前 trapframe,然后更改 trapframe->epc = callback
,这样在 usertrapret()
返回用户态后执行的就是 callback
函数,此时需要注意,callback
函数是不能有参数的(因为这不是正常的函数调用)
callback
执行成功了,但是我们该如何返回到用户态代码原先的地方执行呢? 因此,实验测试代码中提供了另外一个系统调用 sigreturn()
,来让我们重新回到内核态,恢复进入 trap 时的用户上下文,在这一次的 usertrapret()
返回到用户进程原先执行的地方。(xv6 设计的中断、系统调用走相同的调用路径,因此这里可以执行这样的骚操作。)
总体流程:
用户态程序
用户态程序
—>时钟中断
-->usertrap() (p->trapframe->epc 保存用户态程序的下一条执行语句)
—>保存trapframe 到另外一个地方(假设为A)
—> 更改p->trapframe = callback
—>usertrapret() 返回用户态执行callback
—>通过sigreturn() 重新进入trap流程
—>此时走系统调用路径
---->sys_sigreturn() 恢复 p->trapframe 为最开始保存的(从A处读取)
—>usertrapret() 返回用户态trap中断处继续执行
- 一次
alarm trap
需要往返内核态两次
代码:
- kernel/proc.h
// Per-process state
struct proc {struct spinlock lock;//新增字段int ticks; //回调执行所需要ticksint cur_ticks; //当前进程的ticksvoid* callback; //回调函数struct trapframe backup; //保存trapframeint execing; //是否已经在执行了
- makefile
UPROGS=\$U/_alarmtest\
- user/user.h
int sigalarm(int ticks, void (*callback)());
int sigreturn(void);
- user/usys.pl
entry("sigalarm");
entry("sigreturn");
- kernel/syscall.h
#define SYS_sigalarm 22
#define SYS_sigreturn 23
- kernel/syscall.c
extern uint64 sys_mkdir(void);
extern uint64 sys_close(void);
//新增
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);static uint64 (*syscalls[])(void) = {
//其他
[SYS_mkdir] sys_mkdir,
[SYS_close] sys_close,
// 新增
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};
- kernel/sysproc.c
uint64
sys_sigalarm(void){int ticks = 0;uint64 callback = 0;argint(0,&ticks);argaddr(1, &callback);struct proc* proc = myproc();proc->ticks = ticks;proc->callback = (void *)callback;proc->execing = 0;return 0;
}uint64
sys_sigreturn(void){struct proc* proc = myproc();*(proc->trapframe) = proc->backup;proc->execing = 0;// 由于该函数返回后,返回值保存在a0中,会覆盖trapframe中原来的a0// 该函数返回之后就会走 usertrapret() 返回到用户态return proc->trapframe->a0;
}
- kernel/trap.c
if(killed(p))exit(-1);// 新增代码 if(which_dev == 2){struct proc* proc = myproc();// 设置了ticks 且没有执行if(proc->ticks > 0 && proc->execing == 0){proc->cur_ticks++;if(proc->cur_ticks == proc->ticks){proc->execing = 1;proc->backup = *(proc->trapframe); //保存trapframeproc->trapframe->epc = (uint64)proc->callback;proc->cur_ticks = 0;}}}// 新增结束// give up the CPU if this is a timer interrupt.if(which_dev == 2)yield();
实验结果
- make qemu
- ./grade-lab-traps running alarmtest