【MIT 6.S081】Lab4: Traps

MIT 6.S081的Lab4: Traps题解

RISC-V assembly (easy)

  • 目标:认识RISC-V的汇编代码
  • 方法:执行make fs.img 编译,在user文件夹中找到call.c 的汇编文件call.asm

问题一

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

即函数的参数存放在哪些寄存器?例如,哪个寄存器存放了call.c的main函数中的参数13

分析

1
2
3
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12

可以看到,13这个参数放在了a2寄存器,f(8)+1已经被计算出来是12放在a1寄存器。

回想起很多年前(并不)的计组知识(学的是mips),那这个即存在大概是a0~a7

回答

a0~a7a2

问题二

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.)

main函数里调用函数f读到代码在哪?g呢?(提示:编译器可能会将函数变成inline的)

分析

可以看到代码中并没有专门的跳转,所以是inline了。

答案

没有,编译器将其转成内联函数。

问题三

At what address is the function printf located?

printf函数的在哪?

分析

1
34:	600080e7          	jalr	1536(ra) # 630 <printf>

jalr是跳转,所以位置在1536(ra),再往前找ra

1
30:	00000097          	auipc	ra,0x0

auipc指令:将立即数左移12位加到PC上,这里立即数是0所以ra=pc=30

1536(ra)=0x600+0x30=0x630

答案

0x630

问题四

What value is in the register ra just after the jalr to printf in main?

main函数中跳转printf后的时刻ra值是多少?

查资料可知jalr指令将下一条指令的位置也就是pc+4赋给ra,所以应该是0x38

问题五

Run the following code.

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

What is the output? Here’s an ASCII table that maps bytes to characters.

The output depends on that fact that the RISC-V is little-endian. 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?

Here’s a description of little- and big-endian and a more whimsical description.

跑以下代码:

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

输出是什么?可以查ASCII对应表。输出取决于RISC-V是小端编址,如果它是大端编址,如何修改i得到相同输出?57616是否需要改?

分析

直接放到main.c的代码里去跑,输出结果是He110 World ,合理的。

因为%x输出57616 的十六进制数即E110

而字符串输出按地址读,由于小端编址,最右作为字地址(计组应软件接口教材里的说法,着实抽象),就是从最右开始存完整的一个字(高位先存),则有地址从小到大:72 6c 64 00 ,对应ASCII查表得r l d \0

大端编址存储会是00 64 6c 72,为了输出一致,i=0x726c6400 即可。

答案

He110 World ,大端编址i=0x726c640057616不用改。

问题六

In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?

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

第二个参数,没有给寄存器正确的值,则如果函数调用第二个参数应当是当时寄存器a2中的值。

Backtrace (moderate)

要求

考虑到debug时经常需要查看到错误发生时之前的函数栈,在kernel/printf.c 中实现一个backtrace() 函数。

编译器在每个栈帧中放一个帧指针指向调用者的帧指针。

sys_sleep中调用这个函数并运行bttest 验证,输出应是:

1
2
3
4
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898

也许会有些许不同,但输入:

1
2
3
4
5
addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D

应该会看到:

1
2
3
kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85

提示:

  • 记得把函数加到kernel/defs.h中以便sys_sleep调用

  • GCC编译器将当前的帧指针存在s0中,在kernel/riscv.h 中加入以下函数:

    1
    2
    3
    4
    5
    6
    7
    static inline uint64
    r_fp()
    {
    uint64 x;
    asm volatile("mv %0, s0" : "=r" (x) );
    return x;
    }

    backtrace()中可以利用这个读当前帧指针,这个函数用内联汇编读s0

  • 栈帧如图,则return address在fp-8,调用者的栈帧在fp-16

  • xv6给每个栈一个page,kernel/riscv.h中的PGROUNDDOWN(fp) and PGROUNDUP(fp)可以帮忙结束循环。

  • 成功后, 在kernel/printf.cpanic 中调用该函数。

分析

题面写的很清楚了,关于栈帧的具体分类和内容可以看xv6手册,这里不做总结了。

fp-8 指向return address,所以*(fp-8)就是地址,但直接输出十六进制会有问题,所以再取一次变成**(fp-8)输出%p即可。

*(fp-16)指向调用者的fp的位置,所以fp迭代为**(fp-16)

但是有一个细节,从题面给的r_fp()可以看出fpuint64格式的,所以第一次*引用时要转一下uint64*(好坑)。

实现

kernel/defs.h 中添加:

1
2
// printf.c
void backtrace(void);

kernel/riscv.h 中添加:

1
2
3
4
5
6
7
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}

kernel/sysproc.c 中补充sys_sleep

1
2
3
4
5
6
7
uint64
sys_sleep(void)
{
...
backtrace();
return 0;
}

kernel/printf.c中添加:

1
2
3
4
5
6
7
8
9
10
11
12
void
backtrace()
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 tp = PGROUNDUP(fp);
while(fp < tp){
// *(fp-8): return address
printf("%p\n",*((uint64*)(fp-8)));
fp = *((uint64*)(fp-16));
}
}

测试

1
2
make qemu
$ bttest

输出:

地址略有不同,提示给的方法exec fail了,所以直接运行了评价程序:

1
sudo python3 grade-lab-traps backtrace

通过:

按提示,测试完成后在kernel/printf.cpanic 中添加:

1
2
3
4
5
6
7
8
void
panic(char *s)
{
...
backtrace();
for(;;)
;
}

Alarm (hard)

要求

为xv6加功能,在一个进程使用cpu时周期性提醒它。你需要实现一个用户中断/错误处理的初级形式,这与处理页表错误类似。

加一个新的系统调用sigalarm(interval, handler),应用使用sigalarm(n, fn)时,每 n个CPU时间的ticks停止应用并调用fn函数,该函数返回时原应用继续。xv6的ticks取决于硬件计时器产生中断的频率。调用sigalarm(0, 0)时不再继续。

user/alarmtest.c加入Makefile,sigalarmsigreturn正确实现后才能编译通过。

alarmtest调用sigalarm(2, periodic)test0alarmtest的汇编代码在user/alarmtest.asm,也许可以帮助debug,正确时输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ alarmtest
test0 start
........alarm!
test0 passed
test1 start
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
...alarm!
..alarm!
test1 passed
test2 start
................alarm!
test2 passed
$ usertests
...
ALL TESTS PASSED
$

分析

完整代码见实现一节。

如果记忆不清晰了需要重新回顾一下【MIT 6.S081】Lab2: System Calls 中的许多内容。

test0: 调用处理函数

跟着提示走:

alarmtest.c 添加到Makefile中:

1
2
3
UPROGS=\
...
$U/_alarmtest\

将函数声明放到 user/user.h 中:

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

更新user/usys.plkernel/syscall.hkernel/syscall.c,可见【MIT 6.S081】Lab2: System Calls 中的第一个实验,基本一致。

kernal/sysproc.c中的sys_sigreturn 函数现在只需返回0:

1
2
3
4
uint64 sys_sigreturn(void)
{
return 0;
}

kernal/sysproc.c中的sys_sigreturn 函数需要将两个参数(ticks和函数指针,方法回顾Lab2)传给进程,同时进程也需要记录已经过了多少个ticks,类似Lab2操作kernel/proc.h中的proc结构体,并在kernel/proc.c中初始化:

1
2
3
4
5
6
7
8
// kernel/proc.h
struct proc {
...
// alarm
int interval;
void (*handler)();
int ticks;
};
1
2
3
4
5
6
7
8
9
10
11
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
// alarm
p->interval = 0;
p->handler = (void*)0;
p->ticks = 0;
return p;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// kernel/sysproc.c
uint64 sys_sigalarm(void)
{
int interval;
if(argint(0, &interval) < 0)
return -1;
uint64 handler;
if(argaddr(1, &handler) < 0)
return -1;
myproc()->interval = interval;
myproc()->handler = (void*)handler;
return 0;
}

硬件产生的中断在kernel/trap.cusertrap() 处理,则每次中断更新进程的ticks计数,如果到达周期则调用对应函数,提示在代码if(which_dev == 2) ...中处理,以及提醒注意需要调用函数的地址可能为0,则只能通过interval0判断是否需要处理。

usertrap()中注意到一行关键代码:

1
2
3
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;

这熟悉的下一条指令"PC=PC+4",显然将p->handler传给p->trapframe->epc即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// kernel/proc.c
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval != 0) {
p->ticks++;
if(p->ticks == p->interval) {
p->trapframe->epc = (uint64)p->handler;
p->ticks = 0;
}
}
}
yield();
usertrapret();
}

于是完成了第一部分,这时候运行$ alarmtest会crashed,但是既然文档说了不用管那就不用管。

test1/test2: 返回原来的执行位置

alarm要求结束时调用 sigreturnuser/alarmtest.c中的periodic可做参考。

为了保存调用函数前的状态,需要保存寄存器现场,这里看了网上提示才恍然大悟,存trapframe就行了,用的是基本的memmove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// kernel/proc.c
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval != 0) {
p->ticks++;
if(p->ticks == p->interval) {
// ra
p->last_epc = p->trapframe->epc;
// registers
p->last_trapframe = (struct trapframe*)kalloc();
memmove(p->last_trapframe,p->trapframe,PGSIZE);
p->trapframe->epc = (uint64)p->handler;
p->ticks = 0;
}
}
}
yield();
usertrapret();
}
1
2
3
4
5
6
7
8
9
10
11
// kernel/sysproc.c
uint64 sys_sigreturn(void)
{
struct proc* p = myproc();
// ra
p->trapframe->epc = p->last_epc;
// registers
memmove(p->trapframe,p->last_trapframe,PGSIZE);
kfree(p->last_trapframe);
return 0;
}

这时候test2出问题了,test2是要解决如果被调用的函数还没有返回但ticks到了,这时不要再次调用,进程加一个标志就可以了:

1
2
3
4
5
6
7
// kernel/proc.h
struct proc {
...
// alarm
...
int f_alive;
};
1
2
3
4
5
6
7
8
9
10
// kernel/proc.c
static struct proc*
allocproc(void)
{
...
// alarm
...
p->f_alive = 0;
return p;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// kernel/proc.c
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval != 0) {
p->ticks++;
if(p->ticks >= p->interval && !p->f_alive) {
p->f_alive = 1;
...
}
}
}
...
}
1
2
3
4
5
6
7
// kernel/sysproc.c
uint64 sys_sigreturn(void)
{
...
p->f_alive = 0;
return 0;
}

然后就能通过了!usertests一样也通过。

实现

Makefile:

1
2
3
UPROGS=\
...
$U/_alarmtest\

user/user.h

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

user/usys.pl

1
2
3
...
entry("sigalarm");
entry("sigreturn");

kernel/syscall.h

1
2
3
...
#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c

1
2
3
4
5
6
7
8
9
...
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
...
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn
};

kernal/sysproc.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
uint64 sys_sigalarm(void)
{
int interval;
if(argint(0, &interval) < 0)
return -1;
uint64 handler;
if(argaddr(1, &handler) < 0)
return -1;
myproc()->interval = interval;
myproc()->handler = (void*)handler;
return 0;
}
uint64 sys_sigreturn(void)
{
struct proc* p = myproc();
// ra
p->trapframe->epc = p->last_epc;
// registers
memmove(p->trapframe,p->last_trapframe,PGSIZE);
kfree(p->last_trapframe);
p->f_alive = 0;
return 0;
}

kernel/proc.h

1
2
3
4
5
6
7
8
9
10
struct proc {
...
// alarm
int interval;
void (*handler)();
int ticks;
struct trapframe *last_trapframe;
uint64 last_epc;
int f_alive;
};

kernel/proc.c

1
2
3
4
5
6
7
8
9
10
11
static struct proc*
allocproc(void)
{
...
// alarm
p->interval = 0;
p->handler = (void*)0;
p->ticks = 0;
p->f_alive = 0;
return p;
}

kernel/trap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void
usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
if(p->interval != 0) {
p->ticks++;
if(p->ticks >= p->interval && !p->f_alive) {
p->f_alive = 1;
// ra
p->last_epc = p->trapframe->epc;
// registers
p->last_trapframe = (struct trapframe*)kalloc();
memmove(p->last_trapframe,p->trapframe,PGSIZE);
p->trapframe->epc = (uint64)p->handler;
p->ticks = 0;
}
}
}
yield();
usertrapret();
}

测试

1
sudo python3 grade-lab-traps alarm