FDU Operating System Lab4
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-a7
和 fa0-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 就应该
是 0x726c6400
,57616
不用变。
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 | // printf.c |
进一步按照提示,在 kernel/riscv.h
添加如下函数获取s0寄存器中保存的fp的值
1 | static inline uint64 r_fp() { |
在 kernel/sysproc.c
的sys_sleep()中调用backtrace()
1 | uint64 |
backtrace函数实现
我们可以先看 https://pdos.csail.mit.edu/6.1810/2022/lec/l-riscv.txt 中对函数调用栈的结构的介绍
1 | Stack |
每个函数使用一个栈帧,栈从高地址向低地址生长。由图,我们可以明白,每个栈帧的起始地址由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
13void
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
4addr2line -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_sigalarm
和 sys_sigreturn
,可以参看Lab2的操作
修改一下Makefile文件来编译alarmtest.c
1 | UPROGS=\ |
在 user/user.h
中添加系统调用声明
1 | // system calls |
在 kernel/syscall.h
中,添加两个宏定义
1 |
在 kernel/syscall.c
指定系统调用的主体函数
1 | extern uint64 sys_sigalarm(void); |
在 user/usys.pl
中添加系统调用的存根
1 | entry("sigalarm"); |
以上就做完函数调用的准备了,接下来,先实现 sys_sigalarm()
函数
修改 kernel/proc.h
中的proc结构体,添加sigalarm的两个参数(时间间隔n、调用函数fn),还需要记录从上次调用sigalarm后经过的时间间隔ticks
1 | int n; // the alarm interval |
在 kernel/proc.c
中的allocproc()函数中初始化新加的参数
1 | found: |
在 kernel/sysproc.c
中实现 sys_sigalarm()
函数(暂时 sys_sigreturn
只返回0)。
1 | uint64 |
修改 kernel/trap.c
对时间中断的处理,如果是时钟中断(which_dev==2),就增加记录的中断数。如果中断数到达指定间隔,就将处理函数fn的指针传给epc,以执行处理函数。
1 | if(which_dev == 2) |
实现sys_sigreturn
这个函数调用要求我们在时钟中断返回后,能够回到原来的状态。
根据hint,我们需要在sys_sigalarm()中保存中断时的寄存器值
所以,在 kernel/proc.h
中的proc结构体加上相关信息,不妨直接设置一个备份的trapframe(包含所有寄存器的值):
1 | struct trapframe *backup; // Save registers |
这样,在调用sigreturn时,我们只需要把backup复制回原来的trapframe即可,同时设cur_fn为0表示时钟中断处理函数结束;hint4也提醒我们要把返回值设为a0的值。
1 | uint64 |
然后对应的时钟中断处理函数,加一个 if(p->cur_fn == 0)
判断不会重复调用,还有就是复制原来的trapframe。
1 | if(which_dev == 2) |
其它还有一些细节部分,该分配内存和释放内存的地方,参考原本有的部分
在 kernel/proc.c
的 allocproc
中,给新加的两个变量分配内存:
1 | p->cur_fn = 0; |
在 freeproc
中释放内存:
1 | if(p->backup) |