Lab4: traps
文章目录
RISC-V assembly (easy)
阅读***user/call.asm***中生成可读的汇编版,回答一下问题
- 哪些寄存器保存函数的参数?例如,在
main
对printf
的调用中,哪个寄存器保存13?main
的汇编代码中对函数f
的调用在哪里?对g
的调用在哪里(提示:编译器可能会将函数内联)printf
函数位于哪个地址?- 在
main
中printf
的jalr
之后的寄存器ra
中有什么值?- 运行以下代码。
unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i);
程序的输出是什么?这是将字节映射到字符的ASCII码表。
输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把
i
设置成什么?是否需要将57616
更改为其他值?
- 在下面的代码中,“
y=
”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?printf("x=%d y=%d", 3);
/* 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) {
printf("%d %d\n", f(8)+1, 13);
exit(0);
}
- 哪些寄存器保存函数的参数?例如,在
main
对printf
的调用中,哪个寄存器保存13?
a0保存寄存器返回值 0,a1寄存器保存12 (编译器计算好了), a2寄存器保存13
main
的汇编代码中对函数f
的调用在哪里?对g
的调用在哪里(提示:编译器可能会将函数内联)
编译器直接用计算结果替换了函数调用过程,因为始终未出现两函数地址而出现了计算结果
printf
函数位于哪个地址?
0000000000000630 <printf>:
34: 608080e7 jalr 1536(ra) # 630 <printf>
auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值+8。因此jalr会跳转1536+48=1594=0x630处,观察汇编代码可知确实在0000000000000630处。
- 在
main
中printf
的jalr
之后的寄存器ra
中有什么值?
0x30+8=0x38
- 运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
程序的输出是什么?输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i
设置成什么?是否需要将57616
更改为其他值?
输出为HE110 World
因为riscv为小端存储,从&i开始字节分别为0x72,0x6c,0x64, 0x00.分别对应’r’,‘l’,‘d’,'0’的ascii码,0x00作为字符串结束标志。
57616=0xE110
i应该设置为0x726c6400
- 运行printf(“x=%d y=%d”, 3);在y=后面输出什么?为什么会这样?
输出y=1。取决于寄存器a2(第3个参数)的值。
Backtrace(moderate) —— 打印每个栈的返回地址
回溯(Backtrace)通常对于调试很有用:它是一个存放于栈上用于指示错误发生位置的函数调用列表。
在***kernel/printf.c***中实现名为
backtrace()
的函数。在sys_sleep
中插入一个对此函数的调用,然后运行bttest
,它将会调用sys_sleep
。你的输出应该如下所示:backtrace: 0x0000000080002cda 0x0000000080002bb6 0x0000000080002898
在
bttest
退出qemu后。在你的终端:地址或许会稍有不同,但如果你运行addr2line -e kernel/kernel
(或riscv64-unknown-elf-addr2line -e kernel/kernel
),并将上面的地址剪切粘贴如下:$ addr2line -e kernel/kernel 0x0000000080002de2 0x0000000080002f4a 0x0000000080002bfc Ctrl-D
你应该看到类似下面的输出:
kernel/sysproc.c:74 kernel/syscall.c:224 kernel/trap.c:85
编译器向每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。你的
backtrace
应当使用这些帧指针来遍历栈,并在每个栈帧中打印保存的返回地址。
提示
- 在***kernel/defs.h***中添加
backtrace
的原型,那样你就能在sys_sleep
中引用backtrace
- GCC编译器将当前正在执行的函数的帧指针保存在
s0
寄存器,将下面的函数添加到***kernel/riscv.h***
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
并在backtrace
中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取s0
- 这个课堂笔记中有张栈帧布局图。注意返回地址位于栈帧帧指针的固定偏移(-8)位置,并且保存的帧指针位于帧指针的固定偏移(-16)位置
- XV6在内核中以页面对齐的地址为每个栈分配一个页面。你可以通过
PGROUNDDOWN(fp)
和PGROUNDUP(fp)
(参见***kernel/riscv.h***)来计算栈页面的顶部和底部地址。这些数字对于backtrace
终止循环是有帮助的。
一旦你的backtrace
能够运行,就在***kernel/printf.c***的panic
中调用它,那样你就可以在panic
发生时看到内核的backtrace
。
第一步
按照提示,在kernel/defs.h添加定义,在kernel/riscv.h增加r_fp()
第二步
第五课的图,理解这个图就理解了这个函数怎么写,xv6里一个栈帧大小是16个字节
backtrace
这一个lab可以说是为了能够深刻理解stack machine
机制的设计的,每当调用函数时,首先需要将返回地址,之前的栈帧地址入栈,而栈空间的地址也是从高地址往低地址增长的,所以当前的栈帧的偏移8个字节即为return address
,我们需要每次打印出返回地址,同时偏移16个字节则为前一个栈帧的地址,我们依次往前寻找,直到当前的栈帧的起始地址为PGROUNDUP(fp)
:
***kernel/printf.c***中实现名为backtrace()
的函数
void
backtrace(void)
{
uint64 sp = r_fp();
// 找到栈底结束
uint64 bottom = PGROUNDUP(fp);
uint64 return_addr;
printf("backtrace:\n");
while (sp < bottom)
{
return_addr = *(uint64*)(sp-8);
sp = *(uint64*)(sp-16);
printf("%p\n", return_addr);
}
}
第三步
在kernel/sysproc.c的sys_sleep()函数中调用backtrace()
uint64
sys_sleep(void)
{
int n;
uint ticks0;
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
backtrace();
return 0;
}
Alarm(Hard) —— 每个n个时钟周期,内核强制调用用户函数fn
在这个练习中你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。**这对于那些希望限制CPU时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。**例如,你可以在应用程序中使用类似的一些东西处理页面故障。如果你的解决方案通过了
alarmtest
和usertests
就是正确的。
你应当添加一个新的sigalarm(interval, handler)
系统调用,如果一个程序调用了sigalarm(n, fn)
,那么每当程序消耗了CPU时间达到n个“滴答”,内核应当使应用程序函数fn
被调用。当fn
返回时,应用应当在它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。如果一个程序调用了sigalarm(0, 0)
,系统应当停止生成周期性的报警调用。
你将在XV6的存储库中找到名为***user/alarmtest.c***的文件。将其添加到***Makefile***。注意:你必须添加了sigalarm
和sigreturn
系统调用后才能正确编译(往下看)。
alarmtest
在test0
中调用了sigalarm(2, periodic)
来要求内核**每隔两个滴答强制调用periodic()
,然后旋转一段时间。你可以在*user/alarmtest.asm***中看到alarmtest
的汇编代码,这或许会便于调试。当alarmtest
产生如下输出并且usertests
也能正常运行时,你的方案就是正确的:
$ 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
$
当你完成后,你的方案也许仅有几行代码,但如何正确运行是一个棘手的问题。我们将使用原始存储库中的***alarmtest.c***版本测试您的代码。你可以修改***alarmtest.c***来帮助调试,但是要确保原来的alarmtest
显示所有的测试都通过了。
test0: invoke handler(调用处理程序)
首先修改内核以跳转到用户空间中的报警处理程序,这将导致test0
打印“alarm!”。不用担心输出“alarm!”之后会发生什么;如果您的程序在打印“alarm!”后崩溃,对于目前来说也是正常的。以下是一些提示:
- 您需要修改***Makefile***以使***alarmtest.c***被编译为xv6用户程序。
- 放入***user/user.h***的正确声明是:
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);
- 更新***user/usys.pl***(此文件生成***user/usys.S***)、***kernel/syscall.h***和***kernel/syscall.c***以允许
alarmtest
调用sigalarm
和sigreurn
系统调用。 - 目前来说,你的
sys_sigreturn
系统调用返回应该是零。 - 你的
sys_sigalarm()
应该将报警间隔和指向处理程序函数的指针存储在struct proc
的新字段中(位于***kernel/proc.h***)。 - 你也需要在
struct proc
新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;您可以在***proc.c***的allocproc()
中初始化proc
字段。 - 每一个滴答声,硬件时钟就会强制一个中断,这个中断在***kernel/trap.c***中的
usertrap()
中处理。 - 如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
if(which_dev == 2) ...
- 仅当进程有未完成的计时器时才调用报警函数。请注意,用户报警函数的地址可能是0(例如,在***user/alarmtest.asm***中,
periodic
位于地址0)。 - 您需要修改
usertrap()
,以便当进程的报警间隔期满时,用户进程执行处理程序函数。当RISC-V上的陷阱返回到用户空间时,什么决定了用户空间代码恢复执行的指令地址? - 如果您告诉qemu只使用一个CPU,那么使用gdb查看陷阱会更容易,这可以通过运行
make CPUS=1 qemu-gdb
- 如果
alarmtest
打印“alarm!”,则您已成功。
test1/test2(): resume interrupted code(恢复被中断的代码)
alarmtest
打印“alarm!”后,很可能会在test0
或test1
中崩溃,或者alarmtest
(最后)打印“test1 failed”,或者alarmtest
未打印“test1 passed”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。
作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreurn
系统调用。请查看***alarmtest.c**中的periodic
作为示例。这意味着您可以将代码添加到usertrap
和sys_sigreurn
中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。
提示:
- 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
- 当计时器关闭时,让
usertrap
在struct proc
中保存足够的状态,以使sigreurn
可以正确返回中断的用户代码。 - 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。
test2
测试这个。 - 一旦通过
test0
、test1
和test2
,就运行usertests
以确保没有破坏内核的任何其他部分。
第一步
1、2、3、4按照提示完成即可
5、6 添加:报警间隔和指向处理程序函数的指针(int ticks, void (*handler)() )、经历了多少滴答的字段
并在***proc.c***的allocproc()
中初始化proc
字段。
// alarm
void (*handler)(); // alarm函数
int alarm_interval; // 报警间隔
int total_ticks; // 判断走过滴答数 要初始化的字段
7、8
如果产生了计时器中断,您只想操纵进程的报警滴答;你需要写类似下面的代码
// give up the CPU if this is a timer interrupt.
if(which_dev == 2) {
p->total_ticks++;
yield(); // Give up the CPU for one scheduling round.
}
第二步
9、10
修改usertrap()
,使得当进程的报警间隔期满时,用户进程执行处理程序函数。
添加一下32个寄存器,参考lec 6的内容
// 需要保存的历史寄存器用
uint64 his_epc;
uint64 his_ra;
uint64 his_sp;
uint64 his_gp;
uint64 his_tp;
uint64 his_t0;
uint64 his_t1;
uint64 his_t2;
uint64 his_t3;
uint64 his_t4;
uint64 his_t5;
uint64 his_t6;
uint64 his_a0;
uint64 his_a1;
uint64 his_a2;
uint64 his_a3;
uint64 his_a4;
uint64 his_a5;
uint64 his_a6;
uint64 his_a7;
uint64 his_s0;
uint64 his_s1;
uint64 his_s2;
uint64 his_s3;
uint64 his_s4;
uint64 his_s5;
uint64 his_s6;
uint64 his_s7;
uint64 his_s8;
uint64 his_s9;
uint64 his_s10;
uint64 his_s11;
sys_alarm的实现
uint64
sys_sigalarm(void)
{
int interval;
uint64 handler;
struct proc* p = myproc();
if (argint(0, &interval) < 0)
return -1;
if (argaddr(1, &handler) < 0)
return -1;
p->alarm_interval = interval;
p->handler = (void(*)())handler;
return 0;
}
sys_sigreturn的实验
uint64
sys_sigreturn(void)
{
// 重载寄存器
struct proc *p = myproc();
p->trapframe->epc = p->his_epc;
p->trapframe->ra = p->his_ra;
p->trapframe->sp = p->his_sp;
p->trapframe->gp = p->his_gp;
p->trapframe->tp = p->his_tp;
p->trapframe->a0 = p->his_a0;
p->trapframe->a1 = p->his_a1;
p->trapframe->a2 = p->his_a2;
p->trapframe->a3 = p->his_a3;
p->trapframe->a4 = p->his_a4;
p->trapframe->a5 = p->his_a5;
p->trapframe->a6 = p->his_a6;
p->trapframe->a7 = p->his_a7;
p->trapframe->t0 = p->his_t0;
p->trapframe->t1 = p->his_t1;
p->trapframe->t2 = p->his_t2;
p->trapframe->t3 = p->his_t3;
p->trapframe->t4 = p->his_t4;
p->trapframe->t5 = p->his_t5;
p->trapframe->t6 = p->his_t6;
p->trapframe->s0 = p->his_s0;
p->trapframe->s1 = p->his_s1;
p->trapframe->s2 = p->his_s2;
p->trapframe->s3 = p->his_s3;
p->trapframe->s4 = p->his_s4;
p->trapframe->s5 = p->his_s5;
p->trapframe->s6 = p->his_s6;
p->trapframe->s7 = p->his_s7;
p->trapframe->s8 = p->his_s8;
p->trapframe->s9 = p->his_s9;
p->trapframe->s10 = p->his_s10;
p->trapframe->s11 = p->his_s11;
p->is_handler_in = 1;
return 0;
}
然后就是修改usertrap完成最后要求
if(which_dev == 2) {
p->total_ticks++;
if (p->is_handler_in)
{
p->is_handler_in = 0;
// 保存当前trampframe上的用户寄存器
// 用于alarm函数调用完之后
p->his_epc = p->trapframe->epc;
p->his_ra = p->trapframe->ra;
p->his_sp = p->trapframe->sp;
p->his_gp = p->trapframe->gp;
p->his_tp = p->trapframe->tp;
p->his_t0 = p->trapframe->t0;
p->his_t1 = p->trapframe->t1;
p->his_t2 = p->trapframe->t2;
p->his_t3 = p->trapframe->t3;
p->his_t4 = p->trapframe->t4;
p->his_t5 = p->trapframe->t5;
p->his_t6 = p->trapframe->t6;
p->his_a0 = p->trapframe->a0;
p->his_a1 = p->trapframe->a1;
p->his_a2 = p->trapframe->a2;
p->his_a3 = p->trapframe->a3;
p->his_a4 = p->trapframe->a4;
p->his_a5 = p->trapframe->a5;
p->his_a6 = p->trapframe->a6;
p->his_a7 = p->trapframe->a7;
p->his_s0 = p->trapframe->s0;
p->his_s1 = p->trapframe->s1;
p->his_s2 = p->trapframe->s2;
p->his_s3 = p->trapframe->s3;
p->his_s4 = p->trapframe->s4;
p->his_s5 = p->trapframe->s5;
p->his_s6 = p->trapframe->s6;
p->his_s7 = p->trapframe->s7;
p->his_s8 = p->trapframe->s8;
p->his_s9 = p->trapframe->s9;
p->his_s10 = p->trapframe->s10;
p->his_s11 = p->trapframe->s11;
// 将当前PC值改为alarm函数的地址
// 当函数从内核返回时调用handler
p->trapframe->epc = (uint64)p->handler;
p->total_ticks = 0;
} else {
p->total_ticks -= 1;
}
yield(); // Give up the CPU for one scheduling round.
}