xv6_2025_实验4:traps
1 | $ git fetch |
RISC-V assembly
先make fs.img,再打开user/call.asm
Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?
在RISC-V架构中:
a0-a7 寄存器用于传递函数参数
a0 和 a1 也用于返回值
搜索main,可以找到:
1 | void main(void) { |
其中24: 4635 li a2,13
因此13存放在a2寄存器中
Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)
函数内联是编译器的一种优化技术:
编译器直接将小函数的代码插入到调用位置,避免函数调用的开销(保存寄存器、跳转、返回等),对于像g(x) = x+3这样的小函数,内联是常见优化
在汇编代码中没有对f和g函数的显式调用指令,这是因为编译器进行了内联优化:f(8)+1在编译时被计算为常量12(因为f(8)=g(8)=8+3=11,11+1=12),所以汇编代码中直接使用”li a1,12”加载结果,避免了函数调用。
At what address is the function printf located?
1 | 30: 6c6000ef jal 6f6 <printf> |
6c6000ef是机器码,反汇编显示目标地址是0x6f6,jal = Jump And Link,RISC-V的跳转并链接指令功能:跳转到目标地址,同时将返回地址保存到ra寄存器(Return Address(返回地址寄存器))
What value is in the register ra just after the jalr to printf in main?
jal指令会将下一条指令地址存入ra寄存器,当前指令地址:0x30,下一条指令地址:0x30 + 4 = 0x34,因此ra = 0x34。
Run the following code.
1 | unsigned int i = 0x00646c72; |
What is the output? Here’s an ASCII table that maps bytes to characters.
The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value?
新建test.c
1 | #include "kernel/types.h" |
在Makefile里添加$U/_test\,然后make qemu,test,输出如下:
1 | HE110 World |
大端序调整:i = 0x726c6400,57616不需要改变
In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen?
xv6的Makefile中使用了严格的编译选项:
-Werror=format:将格式字符串警告视为错误
这意着任何不匹配的printf格式都会导致编译失败
Backtrace
Backtrace(回溯)实验的目标是实现一个内核调试功能,能够在程序出错或调试时显示当前的函数调用链。这类似于GDB中的bt命令。
栈帧(Stack Frame):每次函数调用时,系统会在栈上分配一块内存,称为”栈帧”,包含:
返回地址:函数执行完后应该返回的地址
上一帧指针:指向调用者的栈帧
局部变量:函数的局部数据
保存的寄存器:需要保护的寄存器值
帧指针(Frame Pointer):RISC-V中使用s0寄存器作为帧指针,指向当前函数的栈帧基地址,通过帧指针可以遍历整个调用链
栈帧布局:
1 | 高地址 |
首先修改kernel/riscv.h
在#ifndef __ASSEMBLER__部分添加帧指针读取函数:
1 | static inline uint64 |
r_fp():函数名,”read frame pointer”的缩写,该函数读取了帧指针
修改kernel/defs.h
1 | void backtrace(void); |
修改 kernel/printf.c
1 | void |
在sysproc.c中添加调用:
1 | uint64 |
最后验证
1 | $ bttest |
1 | huzayn@huzayn-VMware-Virtual-Platform:~/xv6/xv6-labs-2025$ addr2line -e kernel/kernel |
Alarm
要实现在用户程序执行过程中,周期性地插入执行自定义的函数,然后无缝返回继续执行。
系统调用的实现流程:
1 | 用户程序 → 系统调用接口 → 内核处理 → 返回用户程序 |
进程控制块(PCB):每个进程有独立的PCB,内核通过PCB管理进程状态
1 | // kernel/proc.h |
原字段有struct trapframe trapframe; // data page for trampoline.S,但是我们需要第二个陷阱帧,在 alarm 实验中,我们需要*同时保存两个状态:
原始程序状态:被中断的用户程序的状态
警报处理程序状态:警报处理函数执行时的状态
陷阱(Trap)处理机制:
1 | 用户程序执行 → 定时器中断 → usertrap() → 处理警报 → 返回用户程序 |
陷阱帧(Trapframe)保存了进程被中断时的完整上下文
定时器中断机制:每个tick是时间单位,由硬件决定,中断类型码为2(which_dev == 2)
先看一下alarmtests.c
test0 - 基本功能测试
设置每2个ticks调用一次警报处理程序
验证是否能正确调用用户定义的警报处理函数
期望输出:周期性打印 “alarm!”
test1 - 周期性调用测试
测试警报处理程序是否能被周期性调用
验证间隔是否正确
防止重入(确保前一个警报处理完成前不会调用新的警报)
test2 - 上下文恢复测试
测试警报处理完成后是否能正确恢复到被中断的代码
验证寄存器状态是否正确恢复
确保程序能从正确的位置继续执行
test3 - 综合测试
综合测试所有功能
验证边界情况和错误处理
下面开始写代码:
修改用户层接口在user/user.h中添加
1 | int sigalarm(int ticks, void (*handler)()); |
sigalarm用于设置警报:每间隔ticks个时钟周期,调用handler函数
sigreturn从警报处理程序返回,恢复原始执行上下文
更新系统调用表
在 user/usys.pl中添加:
1 | entry("sigalarm"); |
在 kernel/syscall.h中添加系统调用号:
1 | #define SYS_sigalarm 22 |
在 kernel/syscall.c中更新系统调用数组:
1 | extern uint64 sys_sigalarm(void); |
修改进程结构体:在 kernel/proc.h的 struct proc中添加字段:
1 | int alarm_interval; // 警报间隔(ticks) |
初始化alarm字段,在 kernel/proc.c中添加
1 | static struct proc* allocproc(void) { |
实现系统调用处理函数:在 kernel/sysproc.c中添加
1 | uint64 |
修改陷阱处理逻辑:在 kernel/trap.c的 usertrap()函数中添加定时器中断处理:
1 | void usertrap(void) { |
释放资源:在 kernel/proc.c的 freeproc()函数中:
1 | static void freeproc(struct proc *p) { |
将 alarmtest.c添加到 Makefile的 UPROGS中
1 | make qemu |
