Lab: Traps
Trap,中断,这一章实验主要考验中断及系统调用的理解。
涉及trap、trampoline、stack pointer、supervisor、risc-V assembly
RISC-V assembly
阅读call.asm的内容并回答相关问题:
- Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
- 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.)
- At what address is the function printf located?
- What value is in the register ra just after the jalr to printf in main?
- 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?
Here's a description of little- and big-endian and a more whimsical description.
- 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);
翻译版本如下:
- 哪些寄存器包含函数的参数?例如,在 main 函数调用 printf 时,哪个寄存器保存着 13?
回答:a0-a7, a2
- 在主函数(main)的汇编代码中,函数 f 和 g 的调用位置在哪里?(提示:编译器可能会内联函数。)。
回答: 没有代码的位置,被一层一层内联到main里了
- 函数 printf 位于什么地址?
回答:0x0000000000000628
- 在
main函数中jalr跳转到printf后的ra寄存器中的值是什么?
回答:0x0000000000000038,即jalr的下一条指令地址
- 运行以下代码。输出是什么?
unsigned int i = 0x00646c72 ;
printf ( "H%x Wo%s" , 57616 , &i);
这里有一个 ASCII 表,它将字节映射到字符。输出取决于 RISC-V 是小端序这一事实。如果 RISC-V 是大端序,为了得到相同的输出,你会将i设置为什么值?你需要将57616更改为不同的值吗? 这里有小端序和大端序的描述以及一个更奇特的描述。
回答:输出"He110 World",不需要更改,十六进制和内存中的表示不是同一个概念
- 在下面的代码中,
'y='后面会打印什么?(注意:答案不是一个特定的值。)为什么会这样?
printf ( "x=%d y=%d" , 3 );
回答:会打印随机值,取决于寄存器a2的内容
在项目根目录添加answers-traps.txt并记录回答内容。
Backtrace
实现backtrace()打印调用栈。
在./kernel/defs.h中声明
// printf.c
void backtrace(void);
在./kernel/riscv.h中添加fp - frame pointer的获取方法,内联汇编函数
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x));
return x;
}
然后是实现backtrace,本质非常简单,首先我们要明确调用栈的结构:
fp指向栈顶,sp指向栈底。栈空间在xv6中是由上往下扩展的,所以fp始终大于等于sp。当我们进行调用时,会往下开辟新的空间,即新的栈帧,此时sp会减少一定的字节数,fp设置成新sp的大小。
一个栈帧中从高到低第一个8字节fp-8存储return address,即函数调用返回地址,第二个8字节fp-16存储previous address,即上一个栈帧的fp地址,栈帧中还会保存一些寄存器、局部变量等信息,但是大小总是至少16个字节。
xv6中,栈大小为一个页面,那么在我们递归回退着打印调用栈时,根据栈是从上往下开辟的,我们就是在从下往上走,fp的值会逐渐增大,直到回到栈顶,也就是一个页面的顶部,使用PGROUNDUP来判断是否到达页面顶部,作为退出条件。
在./kernel/printf.c实现backtrace
backtrace(){
uint64 fp = r_fp();
while(fp!=PGROUNDUP(fp)){
uint64 ra = *(uint64 *)(fp - 8);
printf("%p\n",ra);
fp = *(uint64 *)(fp - 16);
}
}
在sys_sleep中调用backtrace()
uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace();
...
}
Make qemu编译运行,使用bttest测试有如下输出
$ bttest
0x0000000080002110
0x0000000080001fee
0x0000000080001cd8
Alarm
首先添加系统调用sigalarm和sigreturn,步骤在Lab2有描述,这里再次给出:
- 在user.user.h中注册
- 在user/usys.pl中增加系统调用存根 entry("XXXX")
- 在kernel/syscall.h中定义系统调用号 #define SYS_XXXXX XX
- 在kernel/syscall.c中使用extern定义系统调用函数,并将其调用号-调用函数指针的映射关系加入syscalls中
- 在kernel中实现系统调用
然后,在proc结构体中添加alarm相关字段:
- alarm_interval:定时器时钟周期,0说明禁用
- alarm_handler:时钟回调处理函数
- alarm_ticks:距离下一次时钟的tick数
- alarm_trapframe:中断处理的trapframe,用于中断处理完成后恢复程序正常执行
- alarm_goingoff:是否有一个时钟中断正在处理,防止覆盖
// Per-process state
struct proc {
...
// Alarm
int alarm_interval; // Alarm interval (0 - disabled)
void(*alarm_handler)(); // Alarm handler
int alarm_ticks; // Ticks left for alarm
struct trapframe * alarm_trapframe; // A copy of the trapframe before alarm
int alarm_goingoff; // Is an alarm going off
};
在sysproc.c中添加这两个系统调用对应的实现
uint64 sys_sigalarm(void)
{
int n;
uint64 fn;
if (argint(0, &n) < 0)
{
return -1;
}
if (argaddr(1, &fn) < 0)
{
return -1;
}
return sigalarm(n, (void (*)())fn);
}
uint64 sys_sigreturn(void)
{
return sigreturn();
}
其最终调用的2个处理函数sigalarm和sigreturn是我们新增的,首先在defs.h中添加定义
// trap.c
...
int sigalarm(int ticks, void (*handler)());
int sigreturn();
在trap.c中添加实现
int sigalarm(int ticks, void (*handler)())
{
struct proc *p = myproc();
p->alarm_interval = ticks;
p->alarm_handler = handler;
p->alarm_ticks = ticks;
return 0;
}
int sigreturn()
{
struct proc *p = myproc();
*p->trapframe = *p->alarm_trapframe;
p->alarm_goingoff = 0;
return 0;
}
在proc.c中的allocproc和freeproc中添加初始化对应字段和释放的逻辑
static struct proc*
allocproc(void)
{
struct proc *p;
...
found:
...
// Allocate a trapframe page for alarm_trapframe
if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
...
p->alarm_interval=0;
p->alarm_handler=0;
p->alarm_goingoff=0;
p->alarm_ticks=0;
return p;
}
static void
freeproc(struct proc *p)
{
...
if(p->alarm_trapframe)
kfree((void*)p->alarm_trapframe);
p->alarm_trapframe = 0;
...
p->alarm_interval=0;
p->alarm_handler=0;
p->alarm_goingoff=0;
p->alarm_ticks=0;
p->state = UNUSED;
}
在trap.c的usertrap函数中实现时钟的具体逻辑,在每次时钟中断的时候,若检测到该进程有设置时钟周期,alarm_interval != 0,则将其alarm_tick减小,进行时钟计时,当时钟倒计时结束alarm_tick <= 0且目前没有进行中断处理!alarm_goingoff时,保存原本的程序栈帧trapframe,通过修改epc将程序跳转到alarm_handler中处理,alarm_handler执行完后恢复trapframe回到原本的程序。
void usertrap(void)
{
...
// give up the CPU if this is a timer interrupt.
if (which_dev == 2)
{
if (p->alarm_interval != 0)
{
if (--p->alarm_ticks <= 0)
{
if (!p->alarm_goingoff)
{
p->alarm_ticks = p->alarm_interval;
*p->alarm_trapframe = *p->trapframe;
p->trapframe->epc = (uint64)p->alarm_handler;
p->alarm_goingoff = 1;
}
}
}
yield();
}
usertrapret();
}
测试程序是alarmtest,添加到Makefile中
UPROGS=\
...
$U/_alarmtest\
编译运行:
$ 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
实验完成,make grade验证:
== Test answers-traps.txt == answers-traps.txt: OK
== Test backtrace test ==
$ make qemu-gdb
backtrace test: OK (6.2s)
== Test running alarmtest ==
$ make qemu-gdb
(3.2s)
== Test alarmtest: test0 ==
alarmtest: test0: OK
== Test alarmtest: test1 ==
alarmtest: test1: OK
== Test alarmtest: test2 ==
alarmtest: test2: OK
== Test usertests ==
$ make qemu-gdb
usertests: OK (140.4s)
== Test time ==
time: FAIL
Cannot read time.txt
Score: 84/85