LAB 4: Traps
原文地址:YSBLOG
参考:[mit6.s081] 笔记 Lab4: Traps | 中断陷阱
实验目的:探索如何通过trap实现系统调用。
RISC-V assembly (easy)
阅读call.asm
函数代码,回答一下问题:
1、哪些寄存器保存函数的参数?例如,在main
对printf
的调用中,哪个寄存器保存13?
在risc-v中a0-a7寄存器保存函数参数,如果函数参数超过8个,则保存在内存中,函数调用的默认第一个参数为函数本身名字,存放在a0寄存器,所以13作为printf的第二个参数存放在a2寄存器。
2、main
的汇编代码中对函数f
的调用在哪里?对g
的调用在哪里(提示:编译器可能会将函数内联)
在call.asm第45行可以看出(li a1, 12)main中直接把f(8)+1的值传递给了a1寄存器,所以在汇编中main函数进行了内联优化处理,在mian
的汇编代码中并有没对f进行函数调用。
3、printf
函数位于哪个地址?
在call.asm第50行可以看出,printf的地址在0x640
4、在main
中printf
的jalr
之后的寄存器ra
中有什么值?
ra寄存器用来保存函数执行以后的下一个执行指令的地址,printf的jalr之后,ra寄存器应当是返回main函数的地址,为0x638
5、运行以下代码。
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);
程序的输出是什么?这是将字节映射到字符的ASCII码表。
程序的输出为"Hello World"
输出取决于RISC-V小端存储的事实。如果RISC-V是大端存储,为了得到相同的输出,你会把i
设置成什么?是否需要将57616
更改为其他值?
如果RISC-V是大端存储,为了得到相同的输出,需要把i设置成0x726c6400
,不需要把57616
修改为其他值。
在下面的代码中,“y=
”之后将打印什么(注:答案不是一个特定的值)?为什么会发生这种情况?
printf("x=%d y=%d", 3);
y=%d
中的%d
会替换为当前a2寄存器中的值,因为当前a2寄存器中值不明确,所以答案不是一个特定的值
Backtrace(moderate)
该任务的目的是输出调用backtrace()
函数的cpu的函数当前函数调用关系。
根据课程PPT中的结构图可以知道,fp指向当前函数栈的开头,sp执行当前函数栈的结尾。
每一个函数的前8位表示当前函数结束后返回上一级的地址(ra),8-16位表示上一级函数栈的fp,由于整个栈是由高地址向低地址扩展,所以uint64 ra = *(uint64*)(fp - 8); fp = *(uint64*)(fp - 16);
所以我们想要实现输出当前cpu所有函数栈,我们只需要通过获取当前fp寄存器的值后不断回到上一级fp,直到fp与当前页面顶部地址相同,则说明回到了第一级的fp。
1、在defs.h
中添加函数定义`
void backtrace(void);
2、根据提示,在riscv.h
中添加获取当前fp寄存器的方法
static inline uint64
r_fp()
{
uint64 x;asm volatile("mv %0, s0" : "=r" (x) );return x;
}
3、在printf.c
中实现backtrace()
void
backtrace(void) {
printf("backtrace:\n");// 读取当前帧指针uint64 fp = r_fp();while (fp != PGROUNDUP(fp)) {
// xv6中,用一个页来存储栈,如果fp已经达到了栈页的上届,说明已经达到栈底// 地址扩张是向低地址扩展,所以当fp到达最高地址时说明到达栈底uint64 ra = *(uint64*)(fp - 8); // return addressprintf("%p\n", ra);fp = *(uint64*)(fp - 16); // preivous fp}
}
4、在printf.c
的painc
中添加backtrack
的调用,这样就能在发生异常时定位问题了
void
panic(char *s)
{
backtrace();pr.locking = 0;printf("panic: ");printf(s);printf("\n");panicked = 1; // freeze uart output from other CPUsfor(;;);
}
实验结果如下:
Alarm(Hard)
该任务的目的是通过定时器中断实现定期警报功能,经过一定的CPU时间后,执行指定报警函数。
该任务的test0是完成对于报警函数的调用,不考虑函数的返回,在test1,test2中需要考虑在执行完报警函数后,能够正确返回被中断之前的状态(让报警函数对被中断的函数透明),所以需要保存中断之前的所有寄存器。
这里将全部要求一起实现,具体过程如下:
1、在makefile中添加 $U/_alarmtest\
2、在usys.pl中添加entry("sigalarm");``entry("sigreturn");
3、在syscall.h中添加#define SYS_sigalarm 22
*#define* SYS_sigreturn 23
4、在syscall.c中添加extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);
5、在syscall.c中添加[SYS_sigalarm] sys_sigalarm,[SYS_sigreturn] sys_sigreturn,
6、proc.h
中添加
struct proc {
... ... uint64 interval; // alarm interval timevoid (*handler)(); // alarm handle functionuint64 ticks; // how many ticks have passed since the last callstruct trapframe *alarm_trapframe; // A copy of trapframe right before running alarm_handlerint alarm_goingoff; // Is an alarm currently going off and hasn't not yet returned? (prevent re-entrance of alarm_handler)
}
7、在proc.c
的allocproc
函数,初始化新增字段
static struct proc*
allocproc(void)
{
... ...// Allocate a trapframe page.if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);return 0;}if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);return 0;}
... ...p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;return p;
}
8、在proc.c
的freeproc
函数中释放新增字段
static void
freeproc(struct proc *p)
{
if(p->trapframe)kfree((void*)p->trapframe);if (p->alarm_trapframe) {
kfree((void*)p->alarm_trapframe);}
... ...p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;p->state = UNUSED;
}
9、实现sys_sigalarm()
和sig_sigreturn()
函数,用于实现系统调用
uint64
sys_sigalarm(void) {
// sigalarm 的第一个参数为ticks,第二个参数为void(*handler)()int n;uint64 handler;if (argint(0, &n) < 0) {
return -1;}if (argaddr(1, &handler) < 0) {
return -1;}return sigalarm(n, (void(*)())(handler));
}
uint64
sys_sigreturn(void) {
return sigreturn();
}
10、在trap.c
中实现int sigalarm(int ticks, void(*handler)()
以及int sigreturn()
int sigalarm(int ticks, void(*handler)()) {
// 初始化alarm时设置该进程的计数大小以及对于alarm函数struct proc *p = myproc();p->interval = ticks;p->handler = handler;p->ticks = 0;return 0;
}
int sigreturn() {
// alarm返回时将备份的trapframe寄存器恢复,确保回退时cpu状态和进入中断时一致,对被中断函数透明struct proc *p = myproc();*(p->trapframe) = *(p->alarm_trapframe);// 清除进入alarm标志位,确保能再次进入p->alarm_goingoff = 0;return 0;
}
11、在trap.c
的usertrap
函数中添加定时器中断响应
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();if(r_scause() == 8){
// system callif(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){
// okif (which_dev == 2) {
// 当which_dev为2时,表示每个CPU时间触发的中断if (p->interval != 0) {
// 设定了时钟条件if (p->ticks == p->interval && p->alarm_goingoff == 0) {
// 达到计时次数后// 此时处于内核中断p->ticks = 0;// A程序进入内核时,会将pc的值存到spec中,离开内核时再从spec中读取该值,返回A中代码执行的地方// 此时修改spec寄存器的值为目标函数,就能够在离开中断时返回到我们想要的地方// 但在次之前要保存当前进程的寄存器值,保证在推出目标handler时能够回到A程序正确的位置中*(p->alarm_trapframe) = *(p->trapframe);p->trapframe->epc = (uint64)p->handler;p->alarm_goingoff = 1; // 不允许递归触发handler}p->ticks++; // cpu每产生一次timer中断计数++}}} 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.if(which_dev == 2)yield();usertrapret();
}
实验结果: