Lab4 Traps
RISC-V assembly (easy)
阅读call.asm
汇编文件和RISC-V
指令集,使用GDB
,回答对应问题,学习RISC-V
汇编。
Q: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
A: a0-a7; a2;
Q: 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.)
A: There is none. g(x) is inlined within f(x) and f(x) is further inlined into main()
Q: At what address is the function printf located?
A: 0x616
Q: What value is in the register ra just after the jalr to printf in main?
A: 0x38
jalr's command is a bit puzzling, as you can see from the riscv instruction set, which has the following functions:
1. Write the value of PC + 4 as t
2. Set the value of the PC to $ra + 1510
3. Set the value of the RA register to T
Q: Run the following code.
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
What is the output?
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: "He110 World"; 0x726c6400; no, 57616 is 110 in hex regardless of endianness.
Q: 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);
A: The second argument `3` is passed in a1, and the register for the third argument, a2, is not set to any specific value before the
call, and contains whatever there is before the call.
Q: 哪些寄存器存储了函数调用的参数?举个例子,main 调用 printf 的时候,13 被存在了哪个寄存器中?
A: a0-a7; a2;
Q: main 中调用函数 f 对应的汇编代码在哪?对 g 的调用呢? (提示:编译器有可能会内链(inline)一些函数)
A: 没有这样的代码。 g(x) 被内链到 f(x) 中,然后 f(x) 又被进一步内链到 main() 中
Q: printf 函数所在的地址是?
A: 0x616
Q: 在 main 中 jalr 跳转到 printf 之后,ra 的值是什么?
A: 0x38
jalr这个指令有些令人费解,参考riscv指令集发现,其功能是:
1. 把pc + 4 的值记为t
2. 把pc的值设置成 $ra + 1510
3. 把ra寄存器的值设置成t
Q: 运行下面的代码
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
输出是什么?
如果 RISC-V 是大端序的,要实现同样的效果,需要将 i 设置为什么?需要将 57616 修改为别的值吗?
A: "He110 World"; 0x726c6400; 不需要,57616 的十六进制是 110,无论端序(十六进制和内存中的表示不是同个概念)
Q: 在下面的代码中,'y=' 之后会答应什么? (note: 答案不是一个具体的值) 为什么?
printf("x=%d y=%d", 3);
A: 输出的是一个受调用前的代码影响的“随机”的值。因为 printf 尝试读的参数数量比提供的参数数量多。
第二个参数 `3` 通过 a1 传递,而第三个参数对应的寄存器 a2 在调用前不会被设置为任何具体的值,而是会包含调用发生前的任何已经在里面的值
Backtrace (moderate)
添加backtrace
功能,打印出调用栈,用于调试。
首先在defs.h
中添加函数原型。
// printf.c
void printf(char*, ...);
void panic(char*) __attribute__((noreturn));
void printfinit(void);
void backtrace(void); // 我在这儿!!!
其次,在risc.h
中加入以下函数,该函数通过内联汇编获取寄存器s0
。s0
存放当前函数的栈指针。
// flush the TLB.
static inline void
sfence_vma()
{
// the zero, zero means flush all TLB entries.
asm volatile("sfence.vma zero, zero");
}
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
} // 我在这儿!!!
接着,在 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;
}
然后,在printf.c
中加入backtrace()
的函数实现。
void backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp(); // 调用函数来读取当前的帧指针,该函数使用内联汇编来读取s0
uint64 *frame = (uint64 *)fp;
uint64 up = PGROUNDUP(fp);
uint64 down = PGROUNDDOWN(fp); // 计算栈页面的顶部和底部地址
while (fp > down && fp < up)
{
printf("%p\n", frame[-1]);
fp = frame[-2];
frame = (uint64 *)fp;
}
} // 我在这儿!!!
fp 指向当前栈帧的开始地址,sp 指向当前栈帧的结束地址。 (栈从高地址往低地址生长,所以 fp 虽然是帧开始地址,但是地址比 sp 高。
栈帧中从高到低第一个 8 字节 fp-8
是 return address,也就是当前调用层应该返回到的地址。
栈帧中从高到低第二个 8 字节 fp-16
是 previous address,指向上一层栈帧的 fp 开始地址。
剩下的为保存的寄存器、局部变量等。一个栈帧的大小不固定,但是至少 16 字节。
在 xv6 中,使用一个页来存储栈,如果 fp 已经到达栈页的上界,则说明已经到达栈底。
注意栈的生长方向是从高地址到低地址,所以扩张是 -16,而回收是 +16。
最后在panic()
中加入backtrace()
的调用。
void panic(char *s)
{
pr.locking = 0;
printf("panic: ");
printf(s);
printf("\n");
panicked = 1; // freeze uart output from other CPUs
backtrace(); // 我在这儿!!!
for (;;)
;
}
Alarm(hard)
在进程使用CPU
的时间内,xv6
定期向进程发出警报。这对于那些希望限制CPU
时间消耗的受计算限制的进程,或者对于那些计算的同时执行某些周期性操作的进程可能很有用。
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
和sigreturn
系统调用。 - 目前来说,你的
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”就退出。要解决此问题,必须确保完成报警处理程序后返回到用户程序最初被计时器中断的指令执行。必须确保寄存器内容恢复到中断时的值,以便用户程序在报警后可以不受干扰地继续运行。最后,您应该在每次报警计数器关闭后“重新配置”它,以便周期性地调用处理程序。
作为一个起始点,我们为您做了一个设计决策:用户报警处理程序需要在完成后调用sigreturn
系统调用。请查看***alarmtest.c***中的periodic
作为示例。这意味着您可以将代码添加到usertrap
和sys_sigreturn
中,这两个代码协同工作,以使用户进程在处理完警报后正确恢复。
提示:
- 您的解决方案将要求您保存和恢复寄存器——您需要保存和恢复哪些寄存器才能正确恢复中断的代码?(提示:会有很多)
- 当计时器关闭时,让
usertrap
在struct proc
中保存足够的状态,以使sigreturn
可以正确返回中断的用户代码。 - 防止对处理程序的重复调用——如果处理程序还没有返回,内核就不应该再次调用它。
test2
测试这个。 - 一旦通过
test0
、test1
和test2
,就运行usertests
以确保没有破坏内核的任何其他部分。
添加代码实现。
前置要求不在赘述。sigalarm()
与sigreturn()
具体实现:
// sysproc.h
uint64
sys_sigalarm(void)
{
int ticks;
uint64 hander;
if ((argint(0, &ticks) < 0))
{
return -1;
}
if ((argaddr(1, &hander) < 0))
{
return -1;
}
struct proc *p = myproc();
p->ticks = ticks;
p->ticks_num = 0;
p->hander = hander;
return 0;
}
uint64
sys_sigreturn(void)
{
struct proc *p = myproc();
*p->trapframe = *p->tick_trapframe;
p->hander_exit = 0;
return 0;
}
在 proc.c
中添加初始化与释放代码:
static struct proc *
allocproc(void)
{
......
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
p->ticks = 0;
p->ticks_num = 0;
p->hander = 0;
p->hander_exit = 0;
if ((p->tick_trapframe = (struct trapframe *)kalloc()) == 0)
{
freeproc(p);
release(&p->lock);
return 0;
} // 这一堆!!!
return p;
}
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
......
p->ticks = 0;
p->ticks_num = 0;
p->hander = 0;
p->hander_exit = 0;
if (p->tick_trapframe)
kfree((void *)p->tick_trapframe); // 这一堆!!!
}
在usertrap()
函数中,实现时钟机制具体代码:
void usertrap(void)
{
......
if (p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if (which_dev == 2)
{
if (p->ticks > 0)
{
p->ticks_num++;
if (p->hander_exit == 0 && p->ticks_num > p->ticks)
{
p->ticks_num = 0;
*p->tick_trapframe = *p->trapframe;
p->trapframe->epc = p->hander;
p->hander_exit = 1;
}
}
yield();
}
usertrapret();
} // 这一堆!!!