追踪系统调用

系统实验 (基于 xv6)

截止时间:2024年11月24日 (extended) 23:59

提交内容

  1. 代码源文件 学号.zip: 请注意代码中应有必要的注释;
  2. 实验报告 学号.pdf:简要描述实验中遇到的关键问题及解决方案、印象最深的 bugs、或者额外实现的功能等即可 (1~2 页)。

提交方式:请参考实验页面的具体说明,本次实验 LAB ID2

# 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