原文地址:YSBLOG
参考:[mit6.s081] 笔记 Lab4: Traps | 中断陷阱
实验目的:探索如何通过trap实现系统调用。
阅读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()
函数的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 address
printf("%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 CPUs
for(;;)
;
}
实验结果如下:
该任务的目的是通过定时器中断实现定期警报功能,经过一定的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 time
void (*handler)(); // alarm handle function
uint64 ticks; // how many ticks have passed since the last call
struct trapframe *alarm_trapframe; // A copy of trapframe right before running alarm_handler
int 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 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
if (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();
}
实验结果: