# MIT6.s081-2020 Lab4 Traps
本实验探讨如何使用Traps实现系统调用。
# 准备
git fetch
git checkout traps
make clean
2
3
# RISC-V assembly
该部分内容较为简单,旨在了解risc-v
的一些汇编指令即可。
1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
x10-x17
a2
2. 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.)
0x0000000000000026: 45b1 li a1,12
0x0000000000000014: 250d addiw a0,a0,3
3. At what address is the function printf located?
0x0000000000000640 <printf>:
4. What value is in the register ra just after the jalr to printf in main?
0x0000000000000038
5.1 What is the output?
HE110 World
5.2 If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
0x726c6400
no need
6. In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
Contents of register a2
If argument i < 8 it is passed in integer register ai.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Backtrace
引言
对于调试来说,回溯通常很有用:当堆栈上发生错误的点可以回溯得到上方的函数调用列表。该部分任务的目的是实施一个backtrace()
作用于kernel/printf.c
。
由如图可知,编译器在每个堆栈帧中放入一个帧指针,该指针保存调用者帧指针的地址。你的backtrace
应该使用这些帧指针向上走堆栈并在每个堆栈帧中打印保存的返回地址。其中fp
指向当前函数栈的开头,sp
执行当前函数栈的结尾。
实现
根据所给的提示,按照HINT一步步做下去即可。
HINT1:将回溯的原型添加到kernel/defs.h
这样您就可以调用backtrace
在sys_sleep
.
// printf.c
//...
void backtrace(void);
2
3
HINT2:GCC编译器将当前正在执行的函数的帧指针存放在寄存器中s0。 将以下函数添加到kernel/riscv.h
:
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
2
3
4
5
6
7
并调用这个函数backtrace读取当前帧指针。该函数使用内嵌汇编来读取s0。
HINT3:编写backtrace
函数,请注意,返回地址位于距堆栈帧的帧指针的固定偏移量 (-8) 处,而保存的帧指针位于距帧指针的固定偏移量 (-16) 处。此外,xv6 为 xv6 内核中的每个堆栈在 PAGE 对齐地址处分配一个页面。您可以使用计算堆栈页的顶部和底部地址PGROUNDDOWN(fp)
和PGROUNDUP(fp)
(看kernel/riscv.h
,这些数字有助于backtrace
终止其循环。
void backtrace(void) {
printf("backtrace:\n");
// cur fp
uint64 fp = r_fp();
while(fp != PGROUNDUP(fp)) {
uint64 ra = *(uint64*)(fp - 8);
printf("%p\n",ra);
fp = *(uint64*)(fp - 16);
}
}
2
3
4
5
6
7
8
9
10
11
HINT4:为了测试方法的正确性,在kernel/sysproc.c
中sys_sleep
函数和kernel/printf.c
中panic
函数里随便找个地方插入,看看运行结果就好。
在您的终端中:地址可能略有不同,但如果您运行addr2line -e kernel/kernel
(或者riscv64-unknown-elf-addr2line -e kernel/kernel
) 就可以查看返回地址在代码中的位置。
# Alarm
引言
在本练习中,您将向 xv6 添加一个功能,该功能会在进程使用 CPU 时间时定期提醒它。这对于想要限制它们占用多少 CPU 时间的计算绑定进程,或者对于想要计算但又想要采取一些定期操作的进程可能很有用。更一般地说,您将实现一种原始形式的用户级中断/故障处理程序;例如,您可以使用类似的东西来处理应用程序中的页面错误。如果它通过了警报测试和用户测试,则您的解决方案是正确的。
实现
该部分任务的需要添加一个新的sigalarm(interval, handler)
系统调用。如果应用程序调用sigalarm(n, fn)
,然后在每个 n
程序消耗的CPU时间的“tick”,内核应该引起应用程序功能 fn
被称为。什么时候fn
返回时,应用程序应该从中断的地方继续。在 xv6 中,tick 是一个相当任意的时间单位,由硬件定时器产生中断的频率决定。如果应用程序调用sigalarm(0, 0)
,内核应停止生成定期警报调用。此外,您必须确保在警报处理程序完成后,控制权返回到用户程序最初被定时器中断中断的指令。您必须确保寄存器内容恢复到它们在中断时保持的值,以便用户程序可以在警报后不受干扰地继续运行。最后,您应该在每次关闭后重置警报计数器,以便定期调用处理程序。用户警报处理程序需要调用sigreturn
完成后的系统调用。因此,按Hint步骤做下来就好。
HINT1:需要修改 Makefile 以使alarmtest.c
编译为 xv6 用户程序。
$U/_wc\
$U/_zombie\
$U/_alarmtest\
2
3
HINT2:正确的声明user/user.h
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
2
HINT3:更新 user/usys.pl
(生成 user/usys.S
)、kernel/syscall.h
和 kernel/syscall.c
以允许alarmtest
调用 sigalarm
和 sigreturn
系统调用。
user/usys.pl
entry("sigalarm");
entry("sigreturn");
2
kernel/syscall.h
#define SYS_close 21
#define SYS_sigalarm 22
#define SYS_sigreturn 23
2
3
kernel/syscall.c
extern uint64 sys_uptime(void);
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
static uint64 (*syscalls[])(void) = {
//....
[SYS_close] sys_close,
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
}
2
3
4
5
6
7
8
9
10
HINT4:sys_sigalarm()
应该将警报间隔和指向处理函数的指针存储在proc
结构(在kernel/proc.h
)
struct proc {
//...
uint64 interval; // alarm interval time
void (*handler)(); // alarm handle function
uint64 ticks; // how many ticks have passed since the last call
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)
}
2
3
4
5
6
7
8
HINT5:初始化proc
中的字段在proc.c
中的allocproc()
,与此同时,在proc.c
中的freeroc()
释放新增字段。
static struct proc*
allocproc(void)
{
//....
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
//...
p->ticks = 0;
p->handler = 0;
p->interval = 0;
p->alarm_goingoff = 0;
return p;
}
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
if(p->alarm_trapframe)
kfree((void*)p->alarm_trapframe);
p->trapframe = 0;
p->alarm_trapframe = 0;
//...
p->ticks = 0;
p->handler = 0;
p->interval = 0;
p->alarm_goingoff = 0;
p->state = UNUSED;
}
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
HINT6:实现sys_sigalarm()
和sig_sigreturn()
函数,用于实现系统调用。
kernel/sysproc.c
uint64 sys_sigalarm(void) {
int n;
uint64 handler;
if(argint(0, &n) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
return sigalarm(n, (void(*)())(handler));
}
uint64 sys_sigreturn(void) {
return sigreturn();
}
2
3
4
5
6
7
8
9
10
11
12
13
在trap.c
中实现int sigalarm(int ticks, void(*handler)()
以及int sigreturn()
int sigalarm(int ticks, void(*handler)()) {
// 初始化alarm时设置该进程的计数大小以及对于alarm函数
struct proc *p = myproc();
p->interval = ticks;
p->handler = handler;
p->ticks = 0;
return 0;
}
int sigreturn() {
// alarm返回时将备份的trapframe寄存器恢复,确保回退时cpu状态和进入中断时一致,对被中断函数透明
struct proc *p = myproc();
*(p->trapframe) = *(p->alarm_trapframe);
// 清除进入alarm标志位,确保能再次进入
p->alarm_goingoff = 0;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
为了实现函数调用关系,需要在kernel/defs.h
文件中添加该函数的声明
//trap.c
//...
int sigalarm(int , void (*handler)());
int sigreturn(void);
2
3
4
HINT7:每个ticket,硬件时钟都会强制产生一个中断,该中断在usertrap()
在kernel/trap.c
。因此需要在添加定时器中断响应。
void
usertrap(void)
{
//...
else if((which_dev = devintr()) != 0){
// ok
if(which_dev == 2) {
if (p->interval != 0) { // 设定了时钟条件
if (p->ticks == p->interval && p->alarm_goingoff == 0) {
// 达到计时次数后
// 此时处于内核中断
p->ticks = 0;
// A程序进入内核时,会将pc的值存到spec中,离开内核时再从spec中读取该值,返回A中代码执行的地方
// 此时修改spec寄存器的值为目标函数,就能够在离开中断时返回到我们想要的地方
// 但在次之前要保存当前进程的寄存器值,保证在推出目标handler时能够回到A程序正确的位置中
*(p->alarm_trapframe) = *(p->trapframe);
p->trapframe->epc = (uint64)p->handler;
p->alarm_goingoff = 1; // 不允许递归触发handler
}
p->ticks++; // cpu每产生一次timer中断计数++
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
结果
# 总结
总结一下系统调用过程:
- ecall:ecall是RISC-V硬件指令,只做了最低限度的几件事,允许软件有最大的灵活性,可以随心所欲地设计操作系统,ecall做了三件事:
- 将模式从用户模式更改为管理者模式
- 将程序计数器pc保存在sepc寄存器
- 跳转到stvec指向的指令之后开始执行trampoline
- trampoline 保存trapoframe中用户寄存器,切换页表
- 调用usertrap,这里调用syscall
- syscall调用对应的系统调用函数
- 系统调用返回后调用usertrapret
- usertrapret调用trampoline执行userset,最后回到用户空间