xv6 实验4 Traps
Lab traps: Trap
- https://pdos.csail.mit.edu/6.S081/2020/schedule.html
- https://pdos.csail.mit.edu/6.S081/2020/labs/traps.html
RISC-V assembly (easy)
阅读 call.asm,以及 RISC-V 指令集教程,回答问题。(学习 RISC-V 汇编)1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28Q: 哪些寄存器存储了函数调用的参数?举个例子,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; 不需要,57616 的十六进制是 110,无论端序(十六进制和内存中的表示不是同个概念)
Q: 在下面的代码中,'y=' 之后会答应什么? (note: 答案不是一个具体的值) 为什么?
printf("x=%d y=%d", 3);
A: 输出的是一个受调用前的代码影响的“随机”的值。因为 printf 尝试读的参数数量比提供的参数数量多。
第二个参数 `3` 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会
包含调用发生前的任何已经在里面的值。
Backtrace (moderate)
分析
backtrace功能: 打印出调用栈,用于调试

- 栈的生长方向是从高地址到低地址
- 栈帧中从高到低第一个 8 字节 fp-8 是 return address,也就是当前调用层应该返回到的地址。
- 栈帧中从高到低第二个 8 字节 fp-16 是 previous address,指向上一层栈帧的 fp 开始地址。
- 剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
- 在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。
实现
在 defs.h 中添加声明1
2
3
4
5// defs.h
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
void backtrace(void);
在 riscv.h 中添加获取当前 fp(frame pointer)寄存器的方法1
2
3
4
5
6
7static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x));
return x;
}
在 printf.c实现backtrace函数1
2
3
4
5
6
7
8
9
10void
backtrace(void)
{
uint64 fp = r_fp();
uint64 high = PGROUNDUP(fp), low = PGROUNDDOWN(fp);
while(fp>=low && fp<high) {
printf("%p\n", *((uint64*)(fp - 8))); // return address
fp = *((uint64*)(fp - 16)); // previous fp
}
}
在 sysproc.c 的 sys_sleep 的开头调用一次 backtrace()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace(); // print stack backtrace.
if(argint(0, &n) < 0)
return -1;
// ......
return 0;
}
结果
1 | $ bttest |
Alarm (hard)
任务:给 xv6 加一个功能——在进程使用CPU时间时定期发出警告。这对于限制 CPU 密集型(计算密集型)进程的占用时间,或对于在计算过程中有其他定期动作的进程可能很有用。更广泛的说,我们将实现一个用户级中断/异常的处理程序。
我们需要添加一个 sigalarm(interval, handler) 系统调用。如果一个应用调用了 sigalarm(n, fn),则该应用每耗时 n 个 ticks,内核应该使之调用 fn,fn 返回后该应用继续执行。如果一个应用调用 sigalarm(0, 0),内核应该停止产生 alarm calls。
test0: invoke handler
添加声明
- 在
Makefile的UPROGS的最后添加$U/_alarmtest\ user/user.h中的系统调用函数声明int sigalarm(int ticks, void(*handler)());,int sigreturn(void);user/usys.pl中添加entry("sigalarm");和entry("sigreturn");kernel/syscall.h添加系统函数编号#define SYS_sigalarm 22和#define SYS_sigreturn 23kernel/syscall.c添加内核态函数声明,如下所示1
2
3
4
5
6
7
8extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};
实现
test0是实现一部分的sys_sigalarm,因此为了使得可以编译运行,先将sys_sigreturn(void)的返回值设为0
在kernel/sysproc.c里面,添加如下代码1
2
3
4
5uint64
sys_sigreturn(void)
{
return 0;
}
接下来是关于如何实现发出alarm
- 首先,在struct proc(in kernel/proc.h)中添加一下进程的【属性】:总 ticks 数、处理程序指针、剩余 ticks 数
1 | // Per-process state |
在 allocproc (kernel/proc.c) 中初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16static struct proc*
allocproc(void)
{
...
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
// Initialize for alarm.
p->ticks = p->remain_ticks = 0;
p->handler = 0;
return p;
}在kernel/sysproc.c实现系统调用sys_sigalarm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18uint64
sys_sigalarm(void)
{
int ticks;
uint64 handler;
struct proc *p = myproc();
if(argint(0, &ticks) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
acquire(&p->lock);
p->ticks = ticks;
p->handler = handler;
p->remain_ticks = ticks;
release(&p->lock);
return 0;
}每一个 tick 过后硬件都会产生计时器中断(timer interrupt),所以我们在 kernel/trap.c 中 if(which_dev == 2) 语句下处理它,把 p->trapframe->epc 改成处理程序地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void
usertrap(void)
{
...
if(p->killed)
exit(-1);
// This is a timer interrupt.
if(which_dev == 2){
if(p->ticks == 0)
yield();
p->remain_ticks--;
if(p->remain_ticks == 0){
p->remain_ticks = p->ticks;
p->trapframe->epc = p->handler;
}
}
usertrapret();
}
test1/test2(): resume interrupted code
完善sigalarm
在struct proc(in kernel/proc.h)中添加一个【属性】用来保存栈帧
1
2
3
4
5
6
7
8// Per-process state
struct proc {
...
int ticks; // Ticks between two alarms.
uint64 handler; // Alarm handler.
int remain_ticks; // Remaining ticks after last alarm.
struct trapframe *save_trapframe; // Save trapframe page when handling alarm.
};在 allocproc (kernel/proc.c) 中初始化 栈帧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17static struct proc*
allocproc(void)
{
...
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
// Initialize for alarm.
p->ticks = p->remain_ticks = 0;
p->handler = 0;
p->save_trapframe = 0;
return p;
}在 kernel/trap.c 中 if(which_dev == 2) 语句处 保存栈帧
1
2
3
4
5
6
7
8
9
10
11
12// This is a timer interrupt.
if(which_dev == 2){
if(p->ticks == 0)
yield();
p->remain_ticks--;
if(p->remain_ticks == 0 && p->save_trapframe == 0){
p->remain_ticks = p->ticks;
p->save_trapframe = (struct trapframe *)kalloc();
memmove(p->save_trapframe, p->trapframe, PGSIZE);
p->trapframe->epc = p->handler;
}
}
实现sys_sigreturn,还原 栈帧
1 | uint64 |
评分
1 | grand@Lubuntu ~/xv6-labs-2020 (traps)> ./grade-lab-traps |
tips
如果只是修改了文件,没有任何git操作,想要撤销、还原文件
- 用暂存区的文件替换工作区的文件:
- 全部文件:
git checkout . - 指定文件:
git checkout -- [file]
- 全部文件:
如果修改了文件,并且git add,没有git commit
- git log —oneline // 可以省略,oneline 一条提交信息用一行展示
- git reset HEAD // 回退到当前版本
- git checkout — [file]
如果修改了文件,git add, git commit
- git log —oneline // 可以省略
- git reset HEAD^ // 回退到上一个版本,HEAD^^:上上版本,HEAD~数字:回退到数字个版本
- git checkout — [file]
修改记录
1 | diff --git a/Makefile b/Makefile |

