Lab链接:Lab: Traps
Lab源码:momo/MIT-6S081/traps - Gitee.com
1 RISC-V assembly
使用make fs.img命令生成call.c文件的汇编文件call.asm
int g(int x) {
0: 1141 addi sp,sp,-16
2: e422 sd s0,8(sp)
4: 0800 addi s0,sp,16
return x+3;
}
6: 250d addiw a0,a0,3
8: 6422 ld s0,8(sp)
a: 0141 addi sp,sp,16
c: 8082 ret
000000000000000e <f>:
int f(int x) {
e: 1141 addi sp,sp,-16
10: e422 sd s0,8(sp)
12: 0800 addi s0,sp,16
return g(x);
}
14: 250d addiw a0,a0,3
16: 6422 ld s0,8(sp)
18: 0141 addi sp,sp,16
1a: 8082 ret
000000000000001c <main>:
void main(void) {
1c: 1141 addi sp,sp,-16
1e: e406 sd ra,8(sp)
20: e022 sd s0,0(sp)
22: 0800 addi s0,sp,16
printf("%d %d\n", f(8)+1, 13);
24: 4635 li a2,13
26: 45b1 li a1,12
28: 00000517 auipc a0,0x0
2c: 7b850513 addi a0,a0,1976 # 7e0 <malloc+0xea>
30: 00000097 auipc ra,0x0
34: 608080e7 jalr 1544(ra) # 638 <printf>
exit(0);
38: 4501 li a0,0
3a: 00000097 auipc ra,0x0
3e: 276080e7 jalr 630(ra) # 2b0 <exit>
回答以下问题:
Which registers contain arguments to functions? For example, which register holds 13 in main’s call to
printf?RISC - V 架构,函数调用者在调用前,将参数按顺序依次存储在
a0 - a7寄存器中,然后再调用函数。第24行,
li a2 13指令把立即数13存入寄存器a2,a2存入参数13,a1存入f(8)+1的结果,a0存入格式字符串的地址。Where is the call to function
fin the assembly code for main? Where is the call tog?第26行,
li a1 12指令表明main函数并未调用函数f,编译器直接计算f(8)+1的结果并存入寄存器a1。At what address is the function
printflocated?第34行,注释指出prinf函数位于用户空间虚拟地址0x638处。
What value is in the register
rajust after thejalrtoprintfinmain?第30行,
auipc ra, 0x0把当前PC值高 20 位加载到ra寄存器,jalr 1544(ra)跳转到printf函数,同时把下一条指令(地址为 0x38)的地址保存到ra寄存器。所以,在执行完jalr指令跳转到printf函数后,ra寄存器的值是 0x38。
2 Backtrace
题目
实现backtrace()函数:遍历当前程序的调用栈,按照函数调用的先后顺序,从当前时刻起,逆向打印每个栈帧中的返回地址
运行bttest,该程序调用sys_sleep,sys_sleep()调用backtrace()打印当前堆栈上的函数调用列表。打印的结果如下:
backtrace:
0x0000000080002cda
0x0000000080002bb6
0x0000000080002898
退出qemu,执行addr2line -e kernel/kernel命令并输入上述结果,得到地址对应的函数名和所在文件
在终端中输入 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
思路
在
kernel/riscv.c文件中添加用于读取当前栈顶寄存器值的函数r_fp使用
PGROUNDUP()获取栈所在页的顶部地址xv6栈的结构如下图所示,栈帧从内存较高地址处起始,随着函数调用朝内存较低方向生成栈帧
在遍历当前程序内核栈的过程中,针对每一个栈帧,打印该栈帧的函数地址,即 “返回地址”;同时借助 “t指向前一个栈帧” 所指示的信息,获取调用该函数的栈帧的地址,进而推进栈的遍历流程

源码
void backtrace(void) {
uint64 base, fp, *p;
fp = r_fp();
base = PGROUNDUP(fp);
while(fp < base) {
p = (uint64*)fp;
printf("%p\n", *(p-1));
fp = (uint64)(*(p-2));
}
}
指针所指向的地址虽然可表示为 64 位整数,但要获取指针所指向的数据内容,
fp的类型理应定义为uint64*,而非uint64。这是因为只有uint64*类型才能正确解引用,从而访问到目标数据。当
fp被定义为uint64类型的指针时,对该指针执行-1操作,实际上是在内存地址上向前移动了8 byte。这是由于在 64 位系统中,uint64类型数据占据 8 个字节,指针运算会根据其所指向的数据类型大小来调整偏移量 。
p-8 // x
3 Alarm
题目
本实验要求在 xv6 中实现一个用户级报警处理机制,当程序消耗特定数量的 CPU 时间片后,系统能够自动调用应用程序之前注册的一个报警处理函数。具体而言,用户程序通过 sigalarm(int ticks, void (*handler)()) 系统调用配置报警的相关参数,其中sigalarm(0,0)用于禁止报警。
思路
test0:实现基本报警功能
在进程控制块中扩展与报警相关的字段:
- 报警配置信息:每隔多少ticks报警,报警报警处理函数
- 当前报警状态:是否启用报警,距上次报警有多少ticks
在 allocproc() 中需要对新增的报警相关字段进行初始化,默认关闭报警功能。
sigalarm()系统调用主要负责配置报警参数,包括设置时间间隔和处理函数地址、启用报警功能;同时需要特殊处理 sigalarm(0, 0) 的情况以禁用报警功能。
真正执行报警的过程是由硬件定时器中断触发的:当硬件向正在运行的应用程序发出中断信号后,系统陷入内核态并进入 usertrap() 中断处理流程,在此过程中,内核会递增 alarmticks 计数器,当计数值达到预设的报警阈值时,立即重置计数器并触发报警机制。
如何在程序返回至用户态时,能够跳转到预定义的报警处理函数是一个难点,这里选择修改 trapframe 中的 epc 而非 ra 寄存器是经过深思熟虑的:epc 寄存器专门用于保存异常或中断发生时被打断的指令地址,控制着返回用户态后的执行流向;而 ra 寄存器仅用于常规函数调用中保存返回地址,其作用范围局限于函数调用栈内,在中断处理场景中并不适用。
test1:保存中断前的所有寄存器
test0 能够通过是因为其报警处理函数未使用任何通用寄存器,因此即使未保存和恢复寄存器状态,也不会影响主程序的执行。然而,test1 中的报警处理函数会修改通用寄存器的值,而现有的 usertrap 实现并未在触发报警前保存中断发生时的寄存器状态(即 trapframe 的内容)。同时,sigreturn 系统调用中也缺少相应的寄存器恢复逻辑。其结果是,唯一存留的寄存器值(即被中断前的主程序状态)在执行报警处理函数时遭到破坏,导致返回到主程序时出现状态不一致或数据错误的问题。
为了解决这个问题,需要在触发报警前保存完整的 trapframe 上下文,在 sigreturn 系统调用中恢复保存的上下文,确保所有通用寄存器和状态寄存器都能正确恢复。
test0:
硬件中断前,将当前pc+4存入ra,trap陷入usertrap函数中
设置p->trapframe->epc值为报警处理函数的虚拟地址,强迫应用程序返回用户态后立即调用报警处理函数,但寄存器的值是应用程序中断前的值
通过汇编可知,报警处理函数先向栈中创建栈帧
sigreturn系统调用结束后,ret,抛出栈帧,返回到中断前的下一条指令
test2:防止重复进入中断处理函数
为了防止报警处理函数在执行过程中被再次触发,避免无限递归或状态混乱,需要在进程控制块中添加 alarm_handling 标志位,在进入报警处理函数时设置该标志,在定时器中断检查中跳过已处于处理状态的进程,在 sigreturn 中清除该标志,从而确保报警处理函数的执行不会被打断。
全流程总结
- 硬件向正在运行的应用程序发出中断,应用程序保存上下文后,进入
usertrap usertrap中增加ticks计数,当计数达到预值,保存当前trapframe上下文,同时修改 trapframe->epc 使其指向用户定义的报警处理函数,确保处理器返回用户态后立即执行相应的报警处理程序- 程序返回至用户态,立即执行报警处理函数
- 报警处理函数通过
sigreturn()通知内核恢复原始上下文,程序从中断点继续执行
源码
kernel/proc.h:
struct proc {
// these are private to the process, so p->lock need not be held.
int alarm; // 是否启用报警装置
int alarminterval; // 每隔多少ticks报警
void *alarmhandler; // 报警函数(用户态页表地址)
int alarmticks; // 距上次报警有多少ticks
int alarmhandling; // 是否正在运行报警中断函数
struct trapframe pretrapframe; // 中断前的trapframe
};
kernel/sysproc.c
uint64 sys_sigalarm(void) {
if (ticks == 0)
p->alarm = 0;
else {
p->alarm = 1;
p->alarminterval = ticks;
p->alarmhandler = handler_user_addr;
p->alarmticks = 0;
}
}
uint64 sys_sigreturn(void) {
struct proc *p = myproc();
memmove(p->trapframe, &p->pretrapframe, sizeof(struct trapframe));
p->alarmhandling = 0;
return 0;
}
kernel/trap.c
void usertrap(void) {
if(r_scause() == 8){
// .......
} else if((which_dev = devintr()) != 0){
if (which_dev == 2) {
p->alarmticks++;
if (p->alarmticks >= p->alarminterval) {
p->alarmticks = 0;
p->trapframe->epc = p->alarmhandler;
}
}
}
// ....
usertrapret();
}