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
f
in 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
printf
located?第34行,注释指出prinf函数位于用户空间虚拟地址0x638处。
What value is in the register
ra
just after thejalr
toprintf
inmain
?第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();
}