1813 字
9 分钟
Lab:traps
Lab:traps
RISC-V assembly (easy)
- 理解 RISC-V 汇编
- 函数内联
- 大端小端存储
Backtrace(moderate)
题目:
backtrace 是一个**存放于栈上用于指示错误发生位置的函数调用列表。**
在kernel/printf.c中实现名为backtrace()的函数。在sys_sleep中插入一个对此函数的调用,然后运行bttest,它将会调用sys_sleep。你的输出应该如下所示:
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
解决方案:
编译器在每一个栈帧中放置一个帧指针(frame pointer)保存调用者帧指针的地址。通过使用这些帧指针我们可以遍历栈,并在每个栈帧中打印保存的返回地址。
tips:
- 在 kernel/defs.h **中添加 ****backtrace ****的原型,那样你就能在sys_sleep中引用 **backtrace
- GCC编译器将当前正在执行的函数的帧指针保存在s0寄存器,将下面的函数添加到kernel/riscv.h(并在backtrace中调用此函数来读取当前的帧指针。这个函数使用内联汇编来读取
**<font style="color:rgb(51, 51, 51);background-color:rgb(247, 247, 247);">s0</font>**
) - XV6在内核中以页面对齐的地址为每个进程的栈分配一个页面****。你可以通过PGROUNDDOWN(fp) 和 PGROUNDUP(fp)(参见kernel/riscv.h)来计算栈页面的顶部和底部地址。这些数字对于
**backtrace**
终止循环是有帮助的。
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}
void backtrace()
{
printf("backtrace:\n");
uint64 this_fp = r_fp();
backtrace_help(&this_fp);
return;
}
void backtrace_help(uint64 *this_fp)
{
// 判断是否超出栈,若超出栈,停止
if ((PGROUNDUP(*this_fp) - PGROUNDDOWN(*this_fp)) != PGSIZE)
{return;}
// 返回地址保存在 -8 偏移的位置
printf("%p\n", *(uint64 *)(*this_fp - 8));
// 前一个帧指针保存在-16偏移的位置
uint64 last_fp = *this_fp - 16;
backtrace_help(&(*(uint64 *)last_fp)); // 注意这里的指针的转换为什么是必要的
return;
}
Alarm(Hard)
题目:
你将向XV6添加一个特性,在进程使用CPU的时间内,XV6定期向进程发出警报。这对于那些希望限制CPU 占用时间的的进程,或者计算的同时执行某些周期性操作的进程很有用。
更普遍的来说,你将实现用户级中断/故障处理程序的一种初级形式。
例如,你可以在应用程序中使用类似的一些东西处理页面故障。
- **应当添加一个新的 ****sigalarm(interval, handler) **系统调用
- **如果一个程序调用了 sigalarm(n, fn),那么程序每消耗了CPU时间达到 n 个“滴答”,内核应当使应用程序函数 ****fn **被调用。
- **当 ****fn **返回时,应用应从它离开的地方恢复执行。在XV6中,一个滴答是一段相当任意的时间单元,取决于硬件计时器生成中断的频率。
- 如果一个程序调用了 sigalarm(0, 0),系统应当停止生成周期性的报警调用。
第一步:****invoke handler(调用处理程序)
首先修改内核以跳转到用户空间中的报警处理程序:
- 修改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系统调用返回应该是零。
- 现在我们已经有了**sigalarm和sigreturn****系统调用,但不具有实际的功能。**
- 我们需要存储**sigalarm**调用的参数
- sys_sigalarm()****将报警间隔和指向处理程序函数的指针存储在
**<font style="background-color:rgb(247, 247, 247);">struct proc</font>**
的新字段中(位于kernel/proc.h)。 - 需要在struct proc新增一个新字段。用于跟踪自上一次调用(或直到下一次调用)到进程的报警处理程序间经历了多少滴答;(用于循环,当经历了足够滴答后报警)
- 现在我们已经存储了报警间隔和处理程序函数,我们需要在报警后调用处理程序
- 每一个滴答声,硬件时钟就会强制一个中断,这个中断在kernel/trap.c中的
**<font style="background-color:rgb(247, 247, 247);">usertrap()</font>**
中处理,下面是处理代码
void usertrap(void)
{
int which_dev = 0;
if ((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
// save user program counter.
p->trapframe->epc = r_sepc();
/** 判断是什么原因调用了usertrap函数*/
// 是否是系统调用
if (r_scause() == 8)
{
// system call
if (p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
// 关闭中断
intr_on();
syscall();
}
// 是否是设备中断
else if ((which_dev = devintr()) != 0)
{
// ok
}
// 处理意外的陷阱
else
{
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if (p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
// 如果这是一个定时器中断,则放弃 CPU。
if (which_dev == 2){
#ifdef lab_traps
if (p->alarm_ticks != 0 && p->alarm_status != 1)
{
p->total_ticks++;
if (p->alarm_ticks == p->total_ticks)
{
p->total_ticks = 0;
// void *handler() = (void *)p->handler;
//void (*handler)(void) = (void (*)(void))p->handler;
//handler();
// 需要保存寄存器内容,我们要在执行完报警程序后回到原本的代码执行处
memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));
// 正常情况下,中断恢复执行epc指向的代码,也就是恢复原本代码,但我们这个lab中
// 通过更改epc来改变中断后执行的函数,最后再通过 sigreturn 进行恢复
// 这就是为什么在上面我们保存了寄存器的内容
p->trapframe->epc = p->handler;
p->alarm_status = 1;
}
}
#endif
yield();
}
usertrapret();
}
- 在跳转执行特定的处理函数后,我们需要返回到原本的代码值
- **这个特定的处理函数需要配合 **sigreturn 一起使用,处理函数处理完后通过 sigreturn 来返回到原本的代码
void
periodic()
{
count = count + 1;
printf("alarm!\n");
sigreturn();
}
- sys_sigreturn 和正常的中断恢复不同,是我们人为的恢复数据,和正常的中断恢复的流程不一样
uint64
sys_sigreturn(void){
myproc()->alarm_status = 0;
// 恢复原本的数据,sepc也变为
memmove(myproc()->trapframe, myproc()->alarm_trapframe, sizeof(struct trapframe));
return 0;
}