RISC-V 实验:

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

在推荐阅读材料《Calling Convention》中,有提到:

“The RISC-V calling convention passes arguments in registers when possible. Up to eight integer registers, a0–a7, and up to eight floating-point registers, fa0–fa7, are used for this purpose.”

所以,向函数传递参数的寄存器是 a0-a7fa0-fa7. 在调用 printf 的时候,13 被放在了寄存器 a2 中 (24: 4635 li a2,13)

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

main 函数应该是在 printf 中调用了函数 f,但是通过查看汇编代码,我们发现本应调用函数 f 的地方,main 函数直接读取了参数 12 (24: 45b1 li a1,12),可以想到是函数 f 和函数 g 太简单了,直接被编译器优化掉了。

3. At what address is the function printf located?

(34:61a080e7 jalr 1562(ra) # 64a <printf>)这句,应该是在 0x64a 的位置,其中 ra 是上一行使用 (30: 00000097 auipc ra,0x0) 获取的这行代码的地址 0x30,然后 jalr 的目标地址就是 1562(十进制) + 0x30 = 0x64a,我们把代码往下翻到 0x64a 的地方,也确实是 printf 函数的起始地址。

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

参考阅读材料:

“The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then setting the least-significant bit of the result to zero. The address of the instruction following the jump (pc+4) is written to register rd.Register x0 can be used as the destination if the result is not required.”

所以,应该 ra 里存的是 pc+4,也就是 0x38

5. Run the following code. unsigned int i = 0x00646c72; printf(“H%x Wo%s”, 57616, &i)

输出:HE110 World

前面这个%x 是以 16 进制输出 57616(十进制),就是 E110;后面因为 risc-v 采用 little-endian 存
储数据,所以输出的时候是 72, 6c, 64, 00,对应的字符就是 r l d \0,如果是 big-endian, i 就应该
0x726c640057616 不用变。

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? printf(“x=%d y=%d”, 3);

输出:x=3 y=8229

前面一个正常输出 3,y 这个没传参数,会输出一个奇怪的值。看汇编码的话,printf(“x=%d y=%d”, 3) 会比 printf(“x=%d y=%d”, 3, 4) 少一句 li a2,4,其余汇编码都是一样的。我猜就是没传参数的话,y 也会输出 a2 寄存器里的内容,只不过寄存器里就是一个不确定的值了。

BACKTRACE实验

backtrace函数的目的是递归读取每个函数调用栈,依次打印返回地址

我们先来看一下本次的测试程序 /user/bttest ,比较朴素,就调用了一个sleep()的system call

所以我们要在 kernel/defs.h 头文件中注册backtrace函数的原型,以便sys_sleep函数可以调用

1
2
3
4
5
// printf.c
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
void backtrace(void);

进一步按照提示,在 kernel/riscv.h 添加如下函数获取s0寄存器中保存的fp的值

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

kernel/sysproc.c 的sys_sleep()中调用backtrace()

1
2
3
4
5
6
7
8
9
10
11
uint64
sys_sleep(void)
{
int n;
uint ticks0;

backtrace();

argint(0, &n);
//...
}

backtrace函数实现

我们可以先看 https://pdos.csail.mit.edu/6.1810/2022/lec/l-riscv.txt 中对函数调用栈的结构的介绍

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
28
29
30
31
32
33
34
Stack
.
.
+-> .
| +-----------------+ |
| | return address | |
| | previous fp ------+
| | saved registers |
| | local variables |
| | ... | <-+
| +-----------------+ |
| | return address | |
+------ previous fp | |
| saved registers | |
| local variables | |
+-> | ... | |
| +-----------------+ |
| | return address | |
| | previous fp ------+
| | saved registers |
| | local variables |
| | ... | <-+
| +-----------------+ |
| | return address | |
+------ previous fp | |
| saved registers | |
| local variables | |
$fp --> | ... | |
+-----------------+ |
| return address | |
| previous fp ------+
| saved registers |
$sp --> | local variables |
+-----------------+

每个函数使用一个栈帧,栈从高地址向低地址生长。由图,我们可以明白,每个栈帧的起始地址由fp给出(通过 r_fp() 获取),fp-8 就是返回地址,fp-16 就是前一个函数的栈帧的fp。

所以我们只要递归地打印信息就可以了,至于终止条件,根据提示我们可以参看 kernel/riscv.h 中的PGROUNDUP(fp)或PGROUNDDOWN(fp)函数

多个函数的栈帧都在同一个页面中,无论fp指针指向哪个函数的栈帧,PGROUNDUP(fp) 和 PGROUNDDOWN(fp)都是固定不变的,且有PGROUNDUP(fp) - PGROUNDDOWN(fp) = PGSIZE。而当 fp 指向当前页的起始地址时,会有PGROUNDUP(fp) = PGROUNDDOWN(fp),此时循环终止。

kernel/printf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
void 
backtrace(void){
printf("barcktrace:\n");

uint64 ra, fp = r_fp();

while(PGROUNDUP(fp) - PGROUNDDOWN(fp) == PGSIZE)
{
ra = *((uint64*)(fp - 8));
printf("%p\n", ra);
fp = *((uint64*)(fp - 16));
}
}

在调用完bttest后退出qemu,执行addr2line -e kernel/kernel,查看返回地址在代码中的位置

执行命令:

1
2
3
4
addr2line -e kernel/kernel
0x000000008000212c
0x000000008000201e
0x0000000080001d14

得到结果:

1
2
3
/root/Desktop/xv6-labs-2022/kernel/sysproc.c:59
/root/Desktop/xv6-labs-2022/kernel/syscall.c:141
/root/Desktop/xv6-labs-2022/kernel/trap.c:76

Alarm实验

首先我们需要一些准备工作,添加两个系统调用 sys_sigalarmsys_sigreturn ,可以参看Lab2的操作

修改一下Makefile文件来编译alarmtest.c

1
2
3
4
UPROGS=\
# ...
$U/_zombie\
$U/_alarmtest\

user/user.h 中添加系统调用声明

1
2
3
// system calls
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

kernel/syscall.h 中,添加两个宏定义

1
2
#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c 指定系统调用的主体函数

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

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

user/usys.pl 中添加系统调用的存根

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

以上就做完函数调用的准备了,接下来,先实现 sys_sigalarm() 函数

修改 kernel/proc.h 中的proc结构体,添加sigalarm的两个参数(时间间隔n、调用函数fn),还需要记录从上次调用sigalarm后经过的时间间隔ticks

1
2
3
int n; // the alarm interval
int ticks;
uint64 fn; // the pointer to the handler function

kernel/proc.c 中的allocproc()函数中初始化新加的参数

1
2
3
4
5
6
found:
p->pid = allocpid();
p->state = USED;
p->n = 0;
p->ticks = 0;
p->fn = 0;

kernel/sysproc.c 中实现 sys_sigalarm() 函数(暂时 sys_sigreturn 只返回0)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
uint64
sys_sigalarm(void){
int n;
uint64 fn;
struct proc *p = myproc();

argint(0,&n);
argaddr(1,&fn);

p->n = n;
p->fn = fn;
p->ticks = 0;

return 0;
}

uint64
sys_sigreturn(void){
return 0;
}

修改 kernel/trap.c 对时间中断的处理,如果是时钟中断(which_dev==2),就增加记录的中断数。如果中断数到达指定间隔,就将处理函数fn的指针传给epc,以执行处理函数。

1
2
3
4
5
6
7
8
9
if(which_dev == 2) 
{
p->ticks++;
if(p->ticks == p->n) {
p->trapframe->epc = p->fn;
p->ticks = 0;
}
yield();
}

实现sys_sigreturn

这个函数调用要求我们在时钟中断返回后,能够回到原来的状态。

根据hint,我们需要在sys_sigalarm()中保存中断时的寄存器值

所以,在 kernel/proc.h 中的proc结构体加上相关信息,不妨直接设置一个备份的trapframe(包含所有寄存器的值):

1
2
struct trapframe *backup;    // Save registers
int cur_fn; // Prevent repetitive calls to the handler function(test2 要用)

这样,在调用sigreturn时,我们只需要把backup复制回原来的trapframe即可,同时设cur_fn为0表示时钟中断处理函数结束;hint4也提醒我们要把返回值设为a0的值。

1
2
3
4
5
6
7
8
uint64 
sys_sigreturn(void)
{
struct proc* p = myproc();
*p->trapframe = *p->backup;
p->cur_fn = 0;
return p->backup->a0;
}

然后对应的时钟中断处理函数,加一个 if(p->cur_fn == 0) 判断不会重复调用,还有就是复制原来的trapframe。

1
2
3
4
5
6
7
8
9
10
11
12
if(which_dev == 2) 
{
p->ticks++;
if(p->ticks == p->n && p->cur_fn == 0)
{
*p->backup = *p->trapframe;
p->trapframe->epc = p->fn;
p->ticks = 0;
p->cur_fn = 1;
}
yield();
}

其它还有一些细节部分,该分配内存和释放内存的地方,参考原本有的部分

kernel/proc.callocproc 中,给新加的两个变量分配内存:

1
2
3
4
5
6
7
p->cur_fn = 0;

if((p->backup = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}

freeproc 中释放内存:

1
2
3
4
5
6
7
8
9
if(p->backup) 
kfree((void*) p->backup);
p->backup = 0;

p->n = 0;
p->ticks = 0;
p->fn = 0;
p->cur_fn = 0;
p->state = UNUSED;