追踪系统调用
系统实验 (基于 xv6)
截止时间:2024年11月24日 (extended) 23:59
提交内容:
- 代码源文件
学号.zip
: 请注意代码中应有必要的注释;- 实验报告
学号.pdf
:简要描述实验中遇到的关键问题及解决方案、印象最深的 bugs、或者额外实现的功能等即可 (1~2 页)。提交方式:请参考实验页面的具体说明,本次实验
LAB ID
为2
。# code make package make submit # report make report # score curl "http://114.212.81.7:9999/download?token=[TOKEN]&lab_num=2"
*提交冷却时间 2 小时
系统调用 (System Call) 是操作系统向用户程序暴露其核心功能的接口,用户程序只能通过系统调用 (以操作系用所允许的方式) 来向操作系统请求服务。 在本实验中,我们需要为 xv6 添加一个新的系统调用,以此来熟悉操作系统中系统调用的实现及其处理流程。
在进行本次实验之前,请先参考实验页面配置 xv6 的实验环境,并阅读 xv6 book 的 Chapter 2.2~2.3、以及 Chapter 4.1~4.4。
运行如下命令下载实验框架代码,并切换到 lab-syscall
分支:
git clone https://git.nju.edu.cn/oslab/os-lab-2024fall.git
cd os-lab-2024fall
git checkout lab-syscall
🗒️ 实验内容
在本次实验中,我们需要为 xv6 实现一个新的系统调用 sys_etrace
(Easy System Call Trace),该系统调用将激活对其调用者的 tracing,并打印追踪到的系统调用信息:
int etrace(char *syscall_name, int follow_forks);
- 参数
syscall_name
指定要追踪的系统调用名称,如果有多个,使用,
分隔 (例如"open,read,close"
);如果该参数为0
(即空指针),则默认追踪所有系统调用; - 参数
follow_forks
指定是否追踪子进程产生的系统调用,1
为追踪子进程,0
为不追踪子进程; - 调用成功时返回
0
,失败时返回-1
。
基于上述系统调用,框架代码里提供了一个名为 strace
的命令行工具,可在激活 tracing 的情况下执行给定的应用程序 (源代码见 user/strace.c
)。该工具的使用方式为:
strace [--trace|-t=SYSCALL_NAME] [--follow-forks|-f] command
其中,--trace
指定要追踪的单个系统调用名称 SYSCALL_NAME
,如不指定则默认追踪所有系统调用;--follow-forks
指定是否追踪子进程的系统调用,如果不指定,则默认不追踪子进程;command
为要追踪的可执行程序。例如:
# 追踪执行 ls 时产生的所有系统调用
$ strace ls
# 追踪执行 ls 时产生的 read 和 write 系统调用
$ strace --trace=read,write ls
$ strace -t=read,write ls
# 追踪 usertests forkforkfork (xv6 内置的一个测试集) 执行过程中产生的所有系统调用 (包括子进程)
$ strace --follow-forks usertests forkforkfork
$ strace -f usertests forkforkfork
在 sys_etrace
正确实现的情况下,在 xv6 的 Shell 中执行 strace grep hello README
时应能观察到如下的输出 (追踪到的每个系统调用都会打印系统调用 ID、相关参数、以及系统调用返回值):
$ make qemu
# ...
# in xv6
$ strace grep hello README
3: syscall exec("grep", 0x4e10) -> 3
3: syscall open("README", 0) -> 3
3: syscall read(3, 0x1010, 1023) -> 1023
3: syscall read(3, 0x1044, 971) -> 971
3: syscall read(3, 0x104e, 961) -> 298
3: syscall read(3, 0x1010, 1023) -> 0
3: syscall close(3) -> 0
3: syscall exit(0) -> ?
$
我们在本实验中实现的是一种非常简化的 System Call Trace。你可以参考 Linux 的
strace
工具、以及相关的ptrace()
系统调用来进一步了解如何在真实世界中实现一个进程 (tracer) 对另一进程 (tracee) 执行行为的观察和控制。
输出格式说明
当激活 tracing 后,对于进程随后产生的每一个系统调用,需要按如下格式进行打印 (注意不要缺少或增加空格):
[pid]: syscall [syscall_name]([arg1], [arg2], ...) -> [return_value]`
其中,[pid]
为进程 ID,[syscall_name]
为系统调用名称,[arg1]
、[arg2]
等为系统调用的参数值,[return_value]
为系统调用的返回值。
xv6 系统调用涉及的参数类型一共有五种 (空、整数、地址、字符串、文件描述符),不同参数类型分别按如下方式打印:
- 空参数:不打印,例如
3: syscall fork() -> 4
- 整数参数:直接打印,例如
3: syscall exit(0) -> ?
- 地址参数:按十六进制打印地址,例如
3: syscall read(3, 0x1010, 1023) -> 1023
- 字符串参数:打印字符串,并添加引号
"
,例如3: syscall etrace("exec", 0) -> 0
kernel/param.h
中定义了打印参数的最大长度MAX_STR_P
,如果字符串长度超过MAX_STR_P
,则只打印字符串的前MAX_STR_P
个字符,并添加省略号...
- 例如,对于
strace --trace=exec grep hello README
, 当MAX_STR_P=128
时打印3: syscall exec("grep", 0x3e50) -> 3
;当MAX_STR_P=3
时打印3: syscall exec("gre"..., 0x3e50) -> 3
- 文件描述符参数:直接按整型打印文件描述符,例如
3: syscall close(3) -> 0
一些特殊的情况:
- 对于
exit
系统调用,由于在执行该系统调用之后进程终止、且没有返回值,此时请使用半角问号?
来代替返回值; - 如果输入参数指定了不存在的系统调用,直接忽略该系统调用即可。例如执行
strace --trace=abc grep hello README
不会打印任何追踪信息,而执行strace --trace=abc,read grep hello README
则只追踪read
系统调用。
其它需求说明
在本实验中,对于追踪到的系统调用信息,只需要在 xv6 内核中直接用 printf()
进行打印即可,但需要考虑在哪个位置打印更合适。
你可以采用不同的实现方式来完成本实验,但对 xv6 代码的修改应局限于以下文件:
user/strace.c
user/usys.pl
user/user.h
kernel/proc.h
kernel/proc.c
kernel/syscall.h
kernel/syscall.c
kernel/sysproc.c
kernel/param.h
💭 Tips
在 xv6 中,当用户程序执行一个系统调用时,会首先执行该系统调用在用户空间的 stub (位于 user/usys.S
,该文件由 user/usys.pl
生成),通过汇编代码实现系统调用的参数传递,并最终执行 ecall
指令来陷入内核。 这一步骤将依次经过 uservec (kernel/trampoline.S
) 和 usertrap (kernel/trap.c
),并最终通过 syscall (kernel/syscall.c
) 来跳转到处理所指定系统调用的具体函数。
为了实现 sys_etrace
,你可以首先考虑跑通上面的系统调用处理流程。例如:
- 如何在
user/usys.pl
中添加合适的内容来生成sys_etrace
的 stub; - 如何为
sys_etrace
分配一个新的系统调用 ID; - 如何在内核中获取系统调用参数,并跳转到特定的函数来处理系统调用。
在此基础上,可以再考虑 system call traing 具体功能逻辑的实现。例如:
- 如何在内核中维护特定的数据结构来记录必要信息;
- 如何确定在哪个位置打印系统调用信息更合适。
📊 结果评估
你可以使用如下测试用例先在本地进行测试 (pid
取决于系统的运行情况,但其他内容应该相同):
1) 追踪 grep hello README
产生的所有系统调用:
$ strace grep hello README
3: syscall exec("grep", 0x4e10) -> 3
3: syscall open("README", 0) -> 3
3: syscall read(3, 0x1010, 1023) -> 1023
3: syscall read(3, 0x1044, 971) -> 971
3: syscall read(3, 0x104e, 961) -> 298
3: syscall read(3, 0x1010, 1023) -> 0
3: syscall close(3) -> 0
3: syscall exit(0) -> ?
2) 追踪 grep hello README
使用 read
系统调用的情况:
$ strace --trace=read grep hello README
4: syscall read(3, 0x1010, 1023) -> 1023
4: syscall read(3, 0x1044, 971) -> 971
4: syscall read(3, 0x104e, 961) -> 298
4: syscall read(3, 0x1010, 1023) -> 0
3) 追踪 usertests forkforkfork
使用 fork
系统调用的情况,并追踪子进程 (fork() 的返回值是子进程的 pid,因此可能会不同):
$ strace --trace=fork -follow-forks usertests forkforkfork
usertests starting
5: syscall fork() -> 6
test forkforkfork:
5: syscall fork() -> 7
7: syscall fork() -> 8
8: syscall fork() -> 9
# ...
8: syscall fork() -> -1
9: syscall fork() -> -1
OK
5: syscall fork() -> 68
ALL TESTS PASSED # 注意这个 PASS 并不代表你的实现一定正确,只是 xv6 的 usertests 通过了
4) 追踪 usertests forkforkfork
使用 exec
系统调用的情况,并追踪子进程:
$ strace --trace=exec --follow-forks usertests forkforkfork
69: syscall exec("usertests", 0x4e00) -> 2
usertests starting
test forkforkfork:
OK
ALL TESTS PASSED