2018年以前的是x86平台,2019年开始改为risc-v平台。
https://pdos.csail.mit.edu/6.828/2025/xv6.html

xv6 中文文档

环境搭建

ubuntu24.04虚拟机

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

源码结构

1. kernel/ 目录 - 内核核心代码

这是xv6操作系统的内核部分,包含所有核心功能:

进程管理:

  • proc.c, proc.h - 进程控制块(PCB)和进程管理

  • exec.c - 程序执行和加载

  • swtch.S - 进程上下文切换的汇编代码

内存管理:

  • kalloc.c - 内核内存分配器

  • vm.c, vm.h - 虚拟内存管理

  • memlayout.h - 内存布局定义

文件系统:

  • file.c, file.h - 文件操作接口

  • fs.c, fs.h - 文件系统实现

  • log.c - 文件系统日志和崩溃恢复

  • bio.c, buf.h - 块设备I/O和缓冲区管理

系统调用:

  • syscall.c, syscall.h - 系统调用分发器

  • sysfile.c - 文件相关系统调用实现

  • sysproc.c - 进程相关系统调用实现

硬件抽象层:

  • trap.c - 中断和异常处理

  • uart.c - 串口通信

  • virtio_disk.c, virtio.h - 磁盘I/O

  • plic.c - 平台级中断控制器

同步机制:

  • spinlock.c, spinlock.h - 自旋锁实现

  • sleeplock.c, sleeplock.h - 睡眠锁实现

其他核心组件:

  • main.c - 内核启动入口

  • entry.S - 内核入口汇编代码

  • start.c - 系统启动初始化

  • console.c - 控制台I/O

  • printf.c - 格式化输出

  • string.c - 字符串操作函数

2. user/ 目录 - 用户空间程序

包含所有用户态程序和工具:

系统工具:

  • cat.c - 文件内容显示

  • echo.c - 回显命令

  • grep.c - 文本搜索

  • ls.c - 目录列表

  • mkdir.c - 创建目录

  • rm.c - 删除文件

  • ln.c - 创建链接

  • wc.c - 字符/词/行计数

  • sleep.c - 休眠命令

  • kill.c - 终止进程

Shell和系统:

  • sh.c - Shell解释器

  • init.c - 系统初始化进程

  • memdump.c - 内存转储工具

测试程序:

  • usertests.c - 用户态测试套件

  • forktest.c - 进程创建测试

  • grind.c - 压力测试

  • stressfs.c - 文件系统压力测试

  • logstress.c - 日志压力测试

库函数:

  • ulib.c - 用户态库函数

  • umalloc.c - 用户态内存分配

  • printf.c - 用户态格式化输出

  • user.h - 用户态头文件定义

系统调用接口:

  • usys.pl - 生成系统调用汇编代码的Perl脚本

  • user.ld - 用户程序链接脚本

3. mkfs/ 目录 - 文件系统工具

  • mkfs.c - 创建xv6文件系统镜像的工具

4. conf/ 目录 - 配置文件

  • lab.mk - 实验配置的Makefile片段

5. 根目录文件

  • Makefile - 主构建文件

  • README - 项目说明文档

  • LICENSE - 许可证文件

  • test-xv6.py - 自动化测试脚本

  • grade-lab-util - 实验评分脚本

  • gradelib.py - 评分库

Lab1:Unix utilities

1.添加sleep系统调用

  • kernel/syscall.h:新增系统调用号,添加 #define SYS_sleep (选择未占用的编号,保持与现有顺序一致)
  • kernel/syscall.c:注册分发入口,声明 extern uint64 sys_sleep(void);,在 static uint64 (*syscalls[])(void) 表中加入 [SYS_sleep] = sys_sleep(修改:把SYS_sleep映射到sys_pause)
  • kernel/sysproc.c:实现内核侧处理函数,实现 uint64 sys_sleep(void):,用 argint(0, &n) 取参数,读取并比较 ticks,在 tickslock 下调用 sleep(&ticks, &tickslock) 等待,处理中途被 killed 的情况,返回 -1 或 0,提醒:sleep(void *chan, struct spinlock *lk) 在 kernel/proc.c 已实现,可直接复用
  • user/user.h:声明用户态原型,添加 int sleep(int);
  • user/usys.pl:生成用户态封装(a0 放参数,a7 放号,执行 ecall)
  • user/usys.S:手动新增 sleep 的封装(通常不需要,make 会由 usys.pl 自动生成),(无需修改)陷阱/返回路径
  • kernel/trap.c、kernel/trampoline.S 无需改动,sleep 走标准系统调用路径即可

2.添加sixfive命令

在user/sixfive.c写sixfive函数,修改Makefile,添加到 UPROGS

思路:

打开指定的文件。

从文件中逐字符读取,提取数字(支持负数)。

- \r \t \n . / , 作为数字分隔符。

对于提取到的每个数字,计算其绝对值。

如果能被 5 或 6 整除,就输出这个绝对值(每行一个)。

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
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

static const char* seqs="-\r\t\n./,";
static int is_seq(char c){
return strchr(seqs,c)!=0;
}

static void process_fd(int fd){
char c;
int in_num=0;
int negative=0;
int num=0;
while(read(fd,&c,1)==1){
if(c>='0'&&c<='9'){
if(!in_num){
in_num=1;
negative=0;
num=0;
}
num=num*10+(c-'0');
}
else if(is_seq(c)){
if(in_num){
long val=negative?-num:num;
long absval=val<0?-val:val;
if(absval%5==0||absval%6==0){
printf("%ld\n",absval);
}
in_num=0;
num=0;
negative=0;
}
else if(c=='-'){
in_num=1;
negative=1;
num=0;
}
}
else {
if(in_num){
long val=negative?-num:num;
long absval=val<0?-val:val;
if(absval%5==0||absval%6==0){
printf("%ld\n",absval);
}
}
}
}
if(in_num){
long val=negative?-num:num;
long absval=val<0?-val:val;
if(absval%5==0||absval%6==0){
printf("%ld\n",absval);
}
}
}

int main(int argc, char* argv[]){
if(argc < 2){
fprintf(2,"Usage: sixfive file...\n");
exit(1);
}
for (int i = 1; i < argc; i++) {
int fd = open(argv[i], 0);
if (fd < 0) {
fprintf(2, "sixfive: cannot open %s\n", argv[i]);
continue;
}
process_fd(fd);
close(fd);
}
exit(0);

}

3.memdump

在user/memdump.c写memedump函数,修改Makefile添加到 UPROGS

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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include "kernel/types.h"
#include "user/user.h"
#include "kernel/fcntl.h"

void memdump(char *fmt, char *data);

int
main(int argc, char *argv[])
{
if(argc == 1){
printf("Example 1:\n");
int a[2] = { 61810, 2025 };
memdump("ii", (char*) a);

printf("Example 2:\n");
memdump("S", "a string");

printf("Example 3:\n");
char *s = "another";
memdump("s", (char *) &s);

struct sss {
char *ptr;
int num1;
short num2;
char byte;
char bytes[8];
} example;

example.ptr = "hello";
example.num1 = 1819438967;
example.num2 = 100;
example.byte = 'z';
strcpy(example.bytes, "xyzzy");

printf("Example 4:\n");
memdump("pihcS", (char*) &example);

printf("Example 5:\n");
memdump("sccccc", (char*) &example);
} else if(argc == 2){
// format in argv[1], up to 512 bytes of data from standard input.
char data[512];
int n = 0;
memset(data, '\0', sizeof(data));
while(n < sizeof(data)){
int nn = read(0, data + n, sizeof(data) - n);
if(nn <= 0)
break;
n += nn;
}
memdump(argv[1], data);
} else {
printf("Usage: memdump [format]\n");
exit(1);
}
exit(0);
}

void
memdump(char *fmt, char *data)
{
// Your code here.
char *ptr=data;
for(int i=0;fmt[i]!='\0';i++){
switch(fmt[i]){
case 'i': {
//接下来的 4 个字节的数据以十进制 32 位整数的格式打印出来
int *int_ptr=(int*)ptr;
printf("%d\n",*int_ptr);
ptr+=4;
break;
}
case 'p': {
//接下来的 8 个字节的数据以十六进制形式打印为 64 位整数
long *long_ptr=(long*)ptr;
printf("%lx\n",*long_ptr);
ptr+=8;
break;
}
case 'h': {
// 将数据中的下一个 2 个字节以十进制形式的 16 位整数打印出来
short *short_ptr=(short*)ptr;
printf("%d\n",*short_ptr);
ptr+=2;
break;
}
case 'c': {
//将数据中的下一个 1 个字节以 8 位 ASCII 字符形式打印出来
printf("%c\n",*ptr);
ptr+=1;
break;
}
case 's': {
//数据中的接下来的 8 个字节包含一个指向 C 字符串的 64 位指针;打印该字符串
char **str_ptr=(char**)ptr;
printf("%s\n",*str_ptr);
ptr+=8;
break;
}
case 'S': {
//数据的剩余部分包含一个空终止的 C 字符串的字节;打印该字符串
printf("%s\n",ptr);
return;
}
}
}
}

4.find

  1. 打开与获取文件类型
  • 用 open(path, O_RDONLY) 打开路径,失败就报错返回。

  • 用 fstat(fd, &st) 获取该路径对应的 struct stat,以判断类型(文件/设备/目录)。

  1. 处理“文件/设备”节点:只比较最后一段名字
  • 如果是 T_FILE 或 T_DEVICE,取出 path 的最后一段名字(从末尾往前找到最后一个 / 后的子串)。

  • 若该名字与 target 相等,打印整条 path。

  1. 处理目录:遍历目录项并递归
  • 先检查拼接子路径是否会超过临时缓冲区 char buf[512] 的容量:strlen(path) + 1 + DIRSIZ + 1。

  • 将 path 复制到 buf,在末尾加上 ‘/‘,指针 p 指向可写入子名的位置。

  • 通过 read(fd, &de, sizeof(de)) 逐条读取目录项 struct dirent de:

  • 跳过 de.inum == 0 的空项。

  • 用 memmove(p, de.name, DIRSIZ); p[DIRSIZ]=0; 把该目录项的名字接到 buf 后,形成形如 buf = “/“ 的完整路径。

  • 跳过名字为 “.” 和 “..”(避免无限递归)。

  • 调用 stat(buf, &st) 获取该子路径的类型:

  • 若是目录,递归调用 find(buf, target)。

  • 若是文件/设备,直接比较当前名字 p 是否等于 target,相等则打印 buf。

  1. 边界和健壮性
  • 路径过长时打印 “find: path too long”。

  • open/fstat/stat 失败时打印错误并跳过。

  • 每个 open 最后都 close(fd),避免资源泄露。

    为什么要跳过 “.” 和 “..”,“.” 指向当前目录,“..” 指向父目录;如果递归进入它们会导致死循环(在当前目录与父目录之间来回)。

5.exec

find.c添加:

  • 参数组装:
  • 将 -exec 后的命令及其参数复制到 argv。
  • 把“匹配到的完整文件路径”作为最后一个参数自动追加到 argv,并以 0 结尾。
  • 有 MAXARG 限制,若命令参数太多会报错并跳过执行。
  • 执行模型:
  • fork() 子进程,子进程 exec(argv[0], argv) 运行命令。
  • 父进程 wait(0) 等待,故对每个匹配都是顺序、同步执行(不并行)。
  • 失败处理:
  • fork 失败打印错误并返回。
  • exec 失败在子进程内打印错误并 exit(1)。

可选:

添加uptime系统调用

创建user/uptime.c

测试结果

image-20250911083446998

Lab2:System call

1.gdb调试

一个终端:make qemu-gdb

另一个终端:gdb-multiarch -nx kernel/kernel

target remote localhost:26000

b syscall

c

layout src

backtrace

image-20250911230708929

执行完struct proc *p = myproc();

image-20250911231739514

这个结构体是 xv6 中的 进程控制块(PCB),对应 kernel/proc.h 里的 struct proc

p->trapframe->a7的值是 用户态代码在 ecall 前放到 a7 的系统调用号 user/init.asm

易知若trap是来自用户模式则sstatus中的SPP位为0,若trap来自监管者模式则sstatus中的SPP位为1。

此时的sstatus为22其2进制表示为000101100001 011000010110,第8位SSP位为0,故CPU以前的模式是用户模式。

syscall 的开头将语句 num = p->trapframe->a7; 替换为 num = * (int *) 0; ,运行 make qemu

出现内核崩溃

image-20250912001414899

找到kernel/kernel.S中spec对应的汇编指令

image-20250912001914257

num代表系统调用号,因此变量 num 对应a7寄存器

image-20250912003035039

确定了导致内核崩溃的汇编指令,num = * (int *) 0,一旦执行这条指令,CPU 就尝试访问虚拟地址 0,结果触发page fault→ 内核崩溃

image-20250912003649267

2.沙盒

在 syscall.h 中添加 SYS_interpose 定义,在 proc.h 中添加沙盒掩码字段,在 syscall.c 中注册 interpose 系统调用,修改系统调用分发逻辑,添加沙盒检查,在sysproc.c实现 sys_interpose 函数,在 defs.h 中添加 sys_interpose 的声明,proc.c的在 kfork 函数中添加沙盒掩码的继承,在 allocproc 中初始化沙盒掩码,在usys.pl添加用户态接口,在user.h添加函数声明

在 RISC-V 架构里,寄存器的 ABI 约定是:

  • a0 ~ a7 → 用作 函数参数寄存器
  • a0, a1 → 还用来保存 函数返回值(如果返回值超过 1 个寄存器,就用 a0 + a1 传)

在 xv6 的系统调用处理中:

  1. 用户态进程发起系统调用时,调用号放在 a7 里,参数放在 a0 ~ a5。

    • 这是和 RISC-V 的 syscall 约定一致的。
    • 比如 write(fd, buf, n)a0=fda1=bufa2=n
  2. 进入内核 trap 后,内核在 syscall() 里通过

    1
    num = p->trapframe->a7;

    拿到系统调用号。

  3. 内核调用对应的 syscalls[num]() 函数。

    • 这些函数会返回一个 intuint64,作为系统调用的返回值。
  4. 返回值被写回到

    1
    p->trapframe->a0 = syscalls[num]();

    也就是说:a0 用来保存系统调用的返回,在返回用户态时,用户进程能在 a0 中读到。

    image-20250912111946972

3.添加path参数

进程结构体扩展

在 kernel/proc.h 中添加了:

1
char allowed_path[MAXPATH]; // Allowed path for open/exec system calls

系统调用实现更新

修改了 kernel/sysproc.c 中的 sys_interpose() 函数:

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
uint64

sys_interpose(void)

{

int mask;

char path[MAXPATH];

argint(0, &mask);

argstr(1, path, sizeof(path));



*// Set the sandbox mask and allowed path*

myproc()->sandbox_mask = mask;

safestrcpy(myproc()->allowed_path, path, MAXPATH);

return 0;

}

路径检查函数

在 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
*// Check if a path is allowed for sandboxed open/e**xec calls*

int

is_path_allowed(char **path*)

{

struct proc *p = myproc();



*// If no allowed path is set, deny access*

if(p->allowed_path[0] == '\0')

return 0;



*// Check if the path matches the allowed path*

return strncmp(path, p->allowed_path,MAXPATH) == 0;

}

系统调用分发逻辑更新

修改了 syscall() 函数,为 open 和 exec 系统调用添加了特殊的路径检查:

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
*// Check if this system call is blocked by sandbox*

if(p->sandbox_mask & (1 << num)) {

*// Special handling for open and exec system calls*

if(num == SYS_open || num == SYS_exec) {

*// Get the path argument without consuming it*

uint64 path_addr = p->trapframe->a0;

char path[MAXPATH];

if(copyinstr(p->pagetable, path, path_addr, MAXPATH) >= 0) {

if(is_path_allowed(path)) {

*// Path is allowed, proceed with the system call*

p->trapframe->a0 = syscalls[num]();

return;

}

}

}

*// System call is blocked*

printf("%d %s: sandboxed sys call %d\n",

p->pid, p->name, num);

p->trapframe->a0 = -1;

return;

}

继承机制

  • 在 allocproc() 中初始化 p->allowed_path[0] = ‘\0’

  • 在 kfork() 中添加 safestrcpy(np->allowed_path, p->allowed_path, MAXPATH); 确保子进程继承父进程的允许路径

image-20250912120358925

4.attack

通过 sbrk 分配一大段虚拟地址空间,然后在这块内存里搜索 secret 程序留下的标记 "This may help.",一旦找到就把标记后面 16 字节处的字符串当作“秘密”打印出来。它依赖于内核回收/重用物理页时不清零旧数据的行为,从而窃取别的进程曾经写入的敏感内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "kernel/types.h"
#include "kernel/fcntl.h"
#include "user/user.h"
#include "kernel/riscv.h"

#define PAGESIZE 4096
#define CHUNK_BYTES (8*PAGESIZE)
char find_data[CHUNK_BYTES];

int
main(int argc, char *argv[])
{
for(int i=0;i<CHUNK_BYTES-16;i++){
if(strcmp(find_data+i,"This may help.")==0){
printf("%s\n",find_data+i+16);
}
}

//printf("attack: secret not found\n");
exit(1);
}

image-20250913153246034

Lab3:page tables

1.解释用户进程的页表

image-20250913224020943

每一个页表项包含:va是用户态的虚拟地址,pte是页表项的原始64位值,包含物理页号+权限位,pa是计算得到的物理页基址,perm是权限位,
0x001 = V
0x002 = R
0x004 = W
0x008 = X
0x010 = U
0x020 = G
0x040 = A
0x080 = D
所以 0x5B = V|R|W|U|A|D。
有些pte=0表示还没分配实际内存页
前 10 页 (0x0 到 0x9000)
0x0:通常是代码段或起始的用户内存。
0x1000:用户栈或数据段的一部分。
有些是空洞 (pte=0),表示还没分配实际物理页。
后 10 页(接近 MAXVA 的高地址)
0xFFFFE000 和 0xFFFFF000 有映射,说明 xv6 把一些用户库函数(比如 ugetpid 的实现)放在高地址。
0xFFFFF000 指向物理 0x80006000,这是内核预留的一段共享页,用来提供用户可直接访问的只读数据(例如内核维护的 PID)。

在 USYSCALL 处建立一个只读共享页,用户进程可以在不陷入内核的情况下直接读取某些信息,从而 避免一次 trap/return,大大减少开销。
进程信息
pid(已经实现)
ppid(父进程 ID)
uid / gid(如果 xv6 扩展用户概念)
exit status(只读缓存,可能需要机制保证同步)
时间相关
当前 tick 数(内核全局时钟中断累计值)
uptime(系统启动到现在的时间)
(可选)高精度时钟值(如果要优化 gettimeofday() 这种调用)
内核信息
nproc(当前进程数,用于教学/实验)
loadavg(平均负载,简单版本)
调度相关
当前 cpu id(对应 cpuid())
当前进程的调度优先级/权重

2.加快系统调用

用户空间的 ugetpid 函数已经实现,它直接从 USYSCALL 地址读取 PID。

修改 proc 结构体,添加 usyscall 字段

修改这些函数来支持 USYSCALL 映射。首先修改 proc_pagetable 函数和proc_freepagetable函数

修改 allocproc 函数来分配和初始化 usyscall 页面

修改 freeproc 函数来释放 usyscall 页面

需要初始化 usyscall 结构体,存储当前进程的 PID。需要在 allocproc 函数中,在分配 usyscall 页面之后添加初始化代码

找到了 kfork 函数,这是实际的 fork 实现,在复制 trapframe 之后添加 usyscall 的复制

3.打印页表

image-20250914163323108

kernel/vm.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
#if defined(LAB_PGTBL) || defined(SOL_MMAP) || defined(SOL_COW)

static void vmprint_recursive(pagetable_t pagetable, int level, uint64 base_va);

void
vmprint(pagetable_t pagetable) {
// your code here
printf("page table %p\n",pagetable);
vmprint_recursive(pagetable, 0, 0);
}

void
vmprint_recursive(pagetable_t pagetable, int level, uint64 base_va) {
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){
uint64 va = base_va + (i << PXSHIFT(level));
uint64 pa = PTE2PA(pte);

// Print indentation based on level
for(int j = 0; j <= level; j++){
printf(" ..");
}
// printf("0x%016lx: pte 0x%016lx pa 0x%016lx\n",
// va, (uint64)pte, (uint64)pa);
printf("%p: pte %p pa %p\n", (void*)va, (void*)pte, (void*)pa);


// If this is not a leaf page (no R, W, or X bits set), recurse
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
vmprint_recursive((pagetable_t)pa, level + 1, va);
}
}
}
}
#endif

上面这个没过测试

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
#if defined(LAB_PGTBL) || defined(SOL_MMAP) || defined(SOL_COW)

void vmprint_helper(pagetable_t pagetable, uint64 va, int level)
{
char *dots;
switch (level)
{
case 0:
dots = " ..";
break;
case 1:
dots = " .. ..";
break;
case 2:
dots = " .. .. ..";
break;
default:
dots = "............";
break;
}
for (int i = 0; i < 512; i++)
{
pte_t pte = pagetable[i];
if (pte & PTE_V)
{
uint64 child = PTE2PA(pte);
uint64 child_va = va + (i << (9 * (2 - level) + 12));
printf("%s%p: pte %p pa %p\n", dots, (void *)child_va, (void *)pte, (void *)child);
if (level < 2)
{
vmprint_helper((pagetable_t)child, child_va, level + 1);
}
}
}
}
void
vmprint(pagetable_t pagetable) {
// your code here
printf("page table %p\n", pagetable);
vmprint_helper(pagetable, 0, 0);
}

#endif

4.超级页

image-20250914214523659

好难。。。(失败

image-20250915175831830

关于vm.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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
 内核页表管理函数

__kvmmake()__ - 创建内核的直接映射页表

- 分配并初始化内核页表
- 映射各种硬件设备(UART、VIRTIO、PLIC)
- 映射内核代码段(只读可执行)和数据段(读写)
- 映射 trampoline 页用于陷入/退出处理
- 为每个进程分配内核栈

__kvminit()__ - 初始化内核页表

- 调用 kvmmake() 创建内核页表
- 设置全局内核页表指针

__kvminithart()__ - 切换到内核页表

- 刷新 TLB(转换后备缓冲区)
- 设置 satp 寄存器指向内核页表
- 启用分页

## 页表遍历和查找函数

__walk()__ - 遍历页表查找 PTE(页表项)

- 根据虚拟地址查找对应的页表项
- 支持三级页表遍历(RISC-V Sv39 方案)
- alloc 参数控制是否分配新的页表页

__walkaddr()__ - 查找虚拟地址对应的物理地址

- 只能用于查找用户页
- 检查 PTE_V(有效位)和 PTE_U(用户位)

## 内存映射函数

__kvmmap()__ - 添加内核页表映射

- 仅在启动时使用
- 不刷新 TLB 或启用分页
- 调用 mappages() 实现实际映射

__mappages()__ - 创建页表映射

- 为虚拟地址创建指向物理地址的 PTE
- va 和 size 必须页对齐
- 返回 0 表示成功,-1 表示失败

## 用户页表管理函数

__uvmcreate()__ - 创建空的用户页表

- 分配并初始化用户页表
- 返回 0 表示内存不足

__uvmunmap()__ - 取消用户页表映射

- 移除从 va 开始的 npages 个映射
- do_free 参数控制是否释放物理内存

__uvmalloc()__ - 分配用户内存

- 增长进程内存从 oldsz 到 newsz
- 分配物理内存并创建页表映射

__uvmdealloc()__ - 释放用户内存

- 缩小进程内存从 oldsz 到 newsz
- 调用 uvmunmap() 释放内存

__freewalk()__ - 递归释放页表页

- 递归释放所有页表页
- 所有叶映射必须已移除

__uvmfree()__ - 释放用户内存和页表

- 释放用户内存页
- 释放页表页

## 进程内存操作函数

__uvmcopy()__ - 复制进程内存

- 将父进程内存复制到子进程
- 复制页表和物理内存
- 用于 fork() 系统调用

__uvmclear()__ - 清除用户访问权限

- 将 PTE 标记为无效的用户访问
- 用于 exec() 的用户栈保护页

## 内核与用户空间数据复制函数

__copyout()__ - 从内核复制到用户空间

- 复制 len 字节从内核 src 到用户 dstva
- 处理页错误和权限检查

__copyin()__ - 从用户空间复制到内核

- 复制 len 字节从用户 srcva 到内核 dst
- 处理页错误

__copyinstr()__ - 从用户空间复制字符串到内核

- 复制以 null 结尾的字符串
- 直到遇到 '\0' 或达到 max

## 页错误处理函数

__vmfault()__ - 处理页错误

- 为延迟分配的内存分配和映射物理页
- 用于 sys_sbrk() 的懒分配机制

__ismapped()__ - 检查地址是否已映射

- 检查虚拟地址是否已有有效的页表映射

## 调试和工具函数

__vmprint()__ - 打印页表结构(条件编译)

- 递归打印页表层次结构
- 用于调试和实验

__pgpte()__ - 获取页表项(条件编译)

- 返回指定虚拟地址的 PTE

这个文件实现了 xv6 操作系统的完整虚拟内存管理系统,包括内核和用户空间的页表管理、内存分配、进程间内存复制以及内核与用户空间的数据传输等功能。

Lab4:traps

由于2025年的实验代码没有写好,从lab4改用2024年的版本

1.RISC-V assembly

image-20250917112445551

image-20250917112816760

哪些寄存器包含传递给函数的参数?例如,main 调用 printf 时,哪个寄存器存储 13?
a1存储12,a2存储13,a1a2 包含函数参数。
在 main 的汇编代码中, f 函数的调用在哪里? g 函数的调用在哪里?
f 和 g 的调用参数由于都是编译期常数,都被编译器直接计算了,a1 里存的 12 就是 f(8)+1 的计算结果。
printf 函数位于哪个地址?
6bc
在 jalr 到 printf 的 main 中,寄存器 ra 中包含什么值?
ra=0x34
unsigned int i = 0x00646c72;
printf(“H%x Wo%s”, 57616, (char *) &i);
输出是什么
He110 World
在以下代码中, ‘y=’ 之后将要打印什么?(注意:答案不是特定值。)为什么会这样?
printf(“x=%d y=%d”, 3);
printf 会继续在调用栈上“读”一个不存在的参数位置。y 会打印 a2 的值,因为 a2 是第三个函数传入参数。

image-20250917124924399

image-20250917125423570

2.backtrace

kernel/printf.c 中实现一个 backtrace() 函数,在 sys_sleep 中插入对该函数的调用

riscv.h中添加

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

defs.h中添加backtrace()的声明

printf.h中写backtrace函数

1
2
3
4
5
6
7
8
9
10
void backtrace(void)
{
uint64 fp = r_fp();
uint64 page = PGROUNDDOWN(fp);
while (PGROUNDDOWN(fp) == page)
{
printf("%p\n", (void *)(*(uint64 *)(fp - 8)));
fp = *(uint64 *)(fp - 16);
}
}

3.alarmtest

添加sysalarm和sysreturn 系统调用

trap.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 如果是定时器中断,可能需要抢占 CPU 或处理 alarm
if (which_dev == 2)
{
struct proc *p = myproc();
if (p->alarm_interval > 0) // 进程设置了 alarm
{
p->alarm_ticks--; // 每次 timer interrupt 减少计数
if (p->alarm_ticks == 0 && p->in_handler == 0)
{
// alarm 到期且当前未在信号处理函数中
// 保存当前 trapframe,方便返回时恢复
memmove(&p->sig_trapframe, p->trapframe, sizeof(struct trapframe));
p->in_handler = 1; // 标记进入信号处理函数
p->alarm_ticks = p->alarm_interval; // 重置 alarm 计数
p->trapframe->epc = (uint64)p->alarm_handler; // 设置 epc 指向处理函数
// 这样返回用户态时,会执行 alarm_handler
}
}
yield(); // 抢占 CPU,把 CPU 分给其他进程
}

image-20250917144438724

image-20250917144954559