1. RISC-V assembly
1.1 要求
It will be important to understand a bit of RISC-V assembly, which you were exposed to in 6.004. There is a file user/call.c in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program in user/call.asm.
Read the code in call.asm for the functions g, f, and main. The instruction manual for RISC-V is on the reference page. Here are some questions that you should answer (store the answers in a file answers-traps.txt):
阅读 call.c
和 call.asm
- call.c
#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
int g(int x) {
return x+3;
}
int f(int x) {
return g(x);
}
void main(void) {
unsigned int i = 0x00726c64;
printf("H%x Wo%s", 57616, &i);
printf("%d %d\n", f(8)+1, 13);
exit(0);
}
1.2 实现
根据 call.c
和 call.asm
回答如下问题
1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
a0 ~ a7 register, a2 register
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.)
line 45 ~ 46, function f and function g all be inline by compiler
3. At what address is the function printf located?
0x630
4. What value is in the register ra just after the jalr to printf in main?
0x38
5. Run the following code.
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?
a. output : HE110 World
b. i order : 0x00726c64
c. 57616 no need to change, because it printf by %x , not %s , %s print every character.
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);**
it will print a2 register value
2. Backtrace
2.1 要求
Implement a backtrace() function in kernel/printf.c. Insert a call to this function in sys_sleep, and then run bttest, which calls sys_sleep. Your output should be as follows:
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
After bttest exit qemu. In your terminal: the addresses may be slightly different but if you run addr2line -e kernel/kernel (or riscv64-unknown-elf-addr2line -e kernel/kernel) and cut-and-paste the above addresses as follows:
$ addr2line -e kernel/kernel
0x0000000080002de2
0x0000000080002f4a
0x0000000080002bfc
Ctrl-D
You should see something like this:
kernel/sysproc.c:74
kernel/syscall.c:224
kernel/trap.c:85
实现 backtrace
,打印函数调用堆栈地址,可以通过 addr2line
将该地址转换成具体的代码位置。
2.2 分析
该测试重点在于,了解函数栈帧,在调用函数的时候,每个函数都会有其栈帧,表现如下:
- 获取当前函数的 caller 大致位置
当前栈帧的起止大致为 fp
寄存器到 sp
寄存器,每次调用新的函数时,会先将当前的 fp
寄存器压入栈中,然后将当前 sp
保存到 fp
寄存器,作为新函数的 fp
。此外,调用函数时,call
指令会把执行函数完后的下一条指令地址,即 return address
压入到栈中,因此只需要获取该指令地址就可以知道 caller
的大致位置。
- 获取整个堆栈
由于栈帧的格式是统一的,故 fp
寄存器到 return address
的偏移固定为 -8(因为当前为 64 位系统),获取整个堆栈只需要递归获取 fp
寄存器,依次根据偏移获取 return address
即可。目前用户栈的大小分配为 4kb ,即一页,因此可以通过 sp & (PGSIZE - 1)
来获取栈顶,当 fp >= stack_top
时,停止递归
2.3 实现
void
backtrace()
{
uint64 fp = r_fp();
uint64 stack_top = PGROUNDUP(fp);
while (fp < stack_top)
{
uint64 ret_addr = *(uint64*)(fp - 8);
fp = *(uint64*)(fp - 16);
printf("%p\n", ret_addr);
}
}
3. Alarm
3.1 要求
In this exercise you'll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action. More generally, you'll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example. Your solution is correct if it passes alarmtest and usertests.
3.2 分析
该实验可分为 2 部分
3.2.1 test0: invoke handler
第一部分要求,实现接口如下:
int sigalarm(int ticks, void (*handler)());
假如执行 sigalarm(2, test);
,则表示每过 2 个 tick,执行一次 test
函数,这里一次 tick
表示执行了一次内核时钟中断。
这里的问题在于,如何执行 test
函数,触发条件很容易实现,通过在调用 sigalarm
的时候,设置初始值,每次时间中断统计当前执行过了多少个 tick
即可。
满足条件后,执行指定函数,由于内核和进程使用不同页表,地址空间不同,所以无法直接调用该函数,也不适合将该函数的代码数据等通过拷贝传入内核,消耗太大,其次可能有虚拟地址冲突。
由于判断逻辑的条件在中断当中,中断返回时,需要设置 epc
寄存器,确定中断返回后要执行的代码。故可以修改该寄存器,将值改为 handler
的地址即可。
3.2.2 test1/test2(): resume interrupted code
第二部分的要求主要是为了弥补第一部分的实现导致的问题,直接强行设置 epc
寄存器为 handler
,会导致执行 handler
时的上下文寄存器等资源实际上是中断发生时的上下文,当执行完 handler
时,当前的堆栈是异常的。
为了保证执行完 handler
之后,能正常返回到中断时的代码,该实验提供了一个额外的机制,即 handler
执行完毕时,需要执行 sigreturn
系统调用。
此时的问题就在于,如何利用 sigreturn
,将上下文环境恢复到满足执行 handler
的时钟中断的时候。这里需要了解中断的上下文是如何保存的,只需关注用户态中断,在发生用户态中断时,会进入中断入口 uservec
,这里会将上下文所有寄存器保存到 struct proc.trapframe
中,退出中断时,执行 usertrapret
,将 struct proc.trapframe
的数据恢复到寄存器中。
因此,此时问题简单化为,在触发满足执行 handler
的条件的时候(此时处于时钟中断),将当前被保存的 trapframe
备份一份,然后退出中断时会跳转到 handler
,然后执行完 handler
时,会执行 sigreturn
,此时又会触发中断,再将之前备份的 trapframe
覆盖回去即可。
3.3 实现
- 中断相关实现
void handle_alarm(){
struct proc *p = myproc();
p->alarm_passed_ticks++;
if (p->alarm_passed_ticks > p->alarm_ticks)
return;
if (p->alarm_ticks == p->alarm_passed_ticks){
memmove(p->alarm_trapframe, p->trapframe, PGSIZE);
p->trapframe->epc = p->alarm_handler;
}
}
void usertrap(void)
{
// some code ...
// give up the CPU if this is a timer interrupt.
if(which_dev == 2){
if (p->alarm_ticks != 0){
handle_alarm();
}
// ... some code
}
}
- 系统调用
uint64 sys_sigalarm(void)
{
uint64 handler;
int alarm_ticks;
if(argint(0, &alarm_ticks) < 0)
return -1;
if(argaddr(1, &handler) < 0)
return -1;
struct proc* p = myproc();
p->alarm_ticks = alarm_ticks;
if (alarm_ticks == 0)
return 0;
p->alarm_handler = handler;
p->alarm_passed_ticks = 0;
return 0;
}
uint64 sys_sigreturn(void)
{
struct proc* p = myproc();
memmove(p->trapframe, p->alarm_trapframe, PGSIZE);
p->alarm_passed_ticks = 0;
return 0;
}