1
2
3
$ git fetch
$ git checkout traps
$ make clean

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 寄存器用于传递函数参数
a0a1 也用于返回值

搜索main,可以找到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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: 00001517 auipc a0,0x1
2c: 88850513 addi a0,a0,-1912 # 8b0 <malloc+0x106>
30: 6c6000ef jal 6f6 <printf>
exit(0);
34: 4501 li a0,0
36: 298000ef jal 2ce <exit>

其中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
2
3
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, (char *) &i);

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
2
3
4
5
6
7
8
9
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int main() {
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, (char *) &i);
exit(0);
}

在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
高地址
+----------------+
| 调用者的栈帧 |
+----------------+
| 保存的寄存器 |
+----------------+
| 返回地址(ra) | ← fp - 8
+----------------+
| 上一帧fp | ← fp - 16 (当前fp指向这里)
+----------------+
| 局部变量 |
+----------------+
| 参数空间 |
+----------------+
低地址

首先修改kernel/riscv.h

在#ifndef __ASSEMBLER__部分添加帧指针读取函数:

1
2
3
4
5
6
7
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x));
return x;
}

r_fp():函数名,”read frame pointer”的缩写,该函数读取了帧指针

修改kernel/defs.h

1
void            backtrace(void);

修改 kernel/printf.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void
backtrace(void)
{
printf("backtrace:\n");

// 获取当前帧指针
uint64 fp = r_fp();

// 栈页的顶部(每个内核栈占用一个页面)
uint64 top = PGROUNDUP(fp);

// 遍历栈帧
while (fp < top) {
// 返回地址在 fp-8
uint64 ra = *(uint64*)(fp - 8);
printf("0x%lx\n", ra);

// 上一帧指针在 fp-16
uint64 prev_fp = *(uint64*)(fp - 16);

// 检查是否到达栈底(prev_fp 应该大于当前 fp)
if (prev_fp <= fp) {
break;
}

fp = prev_fp;
}
}

在sysproc.c中添加调用:

1
2
3
4
5
6
uint64
sys_pause(void)
{
backtrace(); // 添加这行
return 0;
}

最后验证

1
2
3
4
5
$ bttest
backtrace:
0x80001e9c
0x80001d18
0x80001a9c
1
2
3
4
5
6
7
huzayn@huzayn-VMware-Virtual-Platform:~/xv6/xv6-labs-2025$ addr2line -e kernel/kernel
0x80001e9c
/home/huzayn/xv6/xv6-labs-2025/kernel/sysproc.c:85
0x80001d18
/home/huzayn/xv6/xv6-labs-2025/kernel/syscall.c:141 (discriminator 1)
0x80001a9c
/home/huzayn/xv6/xv6-labs-2025/kernel/trap.c:80

Alarm

要实现在用户程序执行过程中,周期性地插入执行自定义的函数,然后无缝返回继续执行。

系统调用的实现流程:

1
用户程序 → 系统调用接口 → 内核处理 → 返回用户程序

进程控制块(PCB):每个进程有独立的PCB,内核通过PCB管理进程状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// kernel/proc.h
struct proc {
// 原有字段
struct spinlock lock;
int pid;
// ...

// alarm实验新增字段
int alarm_interval; // 警报间隔
void (*alarm_handler)(); // 处理函数指针
int ticks_passed; // 已过ticks计数
int alarm_pending; // 防止重入标志
struct trapframe *saved_trapframe; // 保存的陷阱帧
};

原字段有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
2
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

sigalarm用于设置警报:每间隔ticks个时钟周期,调用handler函数
sigreturn从警报处理程序返回,恢复原始执行上下文

更新系统调用表

user/usys.pl中添加:

1
2
entry("sigalarm");
entry("sigreturn");

kernel/syscall.h中添加系统调用号:

1
2
#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c中更新系统调用数组:

1
2
3
4
5
6
7
8
extern uint64 sys_sigalarm(void);
extern uint64 sys_sigreturn(void);

static uint64 (*syscalls[])(void) = {
// ... 其他系统调用
[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn,
};

修改进程结构体:在 kernel/proc.hstruct proc中添加字段:

1
2
3
4
5
int alarm_interval;          // 警报间隔(ticks)
void (*alarm_handler)(); // 警报处理函数指针
int ticks_count; // 当前tick计数
int alarm_pending; // 是否有待处理的警报
struct trapframe *alarm_trapframe; // 保存的陷阱帧

初始化alarm字段,在 kernel/proc.c中添加

1
2
3
4
5
6
7
8
9
10
11
static struct proc* allocproc(void) {
// ... 现有代码

p->alarm_interval = 0;
p->alarm_handler = 0;
p->ticks_count = 0;
p->alarm_pending = 0;
p->alarm_trapframe = 0;

return p;
}

实现系统调用处理函数:在 kernel/sysproc.c中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
uint64
sys_sigalarm(void)
{
int interval;
uint64 handler;

argint(0, &interval);
argaddr(1, &handler);

struct proc *p = myproc();

// 设置警报参数
p->alarm_interval = interval;
p->alarm_handler = (void(*)())handler;
p->ticks_count = 0;
p->alarm_pending = 0;

return 0;
}

uint64
sys_sigreturn(void)
{
struct proc *p = myproc();

if(p->alarm_trapframe == 0) {
return -1;
}

memmove(p->trapframe, p->alarm_trapframe, sizeof(struct trapframe));

// 保存原始 a0 的值
uint64 original_a0 = p->trapframe->a0;

// 释放保存的陷阱帧
kfree(p->alarm_trapframe);
p->alarm_trapframe = 0;
p->alarm_pending = 0;

// 返回原始的系统调用返回值
return original_a0;
}

修改陷阱处理逻辑:在 kernel/trap.cusertrap()函数中添加定时器中断处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void usertrap(void) {
// ... 现有代码

if(which_dev == 2) { // 定时器中断
if(p->alarm_interval > 0) { // 设置了警报
p->ticks_count++;

if(p->ticks_count >= p->alarm_interval && !p->alarm_pending) {
// 保存当前陷阱帧
if(p->alarm_trapframe == 0) {
p->alarm_trapframe = (struct trapframe*)kalloc();
}
memmove(p->alarm_trapframe, p->trapframe, sizeof(struct trapframe));

// 设置陷阱帧以跳转到处理程序
p->trapframe->epc = (uint64)p->alarm_handler;
p->alarm_pending = 1;
p->ticks_count = 0; // 重置计数器
}
}
}

// ... 现有代码
}

释放资源:在 kernel/proc.cfreeproc()函数中:

1
2
3
4
5
6
7
8
9
10
static void freeproc(struct proc *p) {
// ... 现有代码

if(p->alarm_trapframe) {
kfree(p->alarm_trapframe);
p->alarm_trapframe = 0;
}

// ... 现有代码
}

alarmtest.c添加到 MakefileUPROGS

1
2
3
make qemu
alarmtest
usertests -q