```Java ``` ## 一、System call tracing 思路 ### 1. 为什么要做系统调用追踪 在后续调试各种功能(尤其是内核相关功能)时,如果能在内核态"看见"当前进程调用了哪些系统调用、返回值是什么,会非常方便排查 Bug。因此,Lab2 的第一个子实验要求你为 xv6 添加一个 `trace` 系统调用,用于打开/关闭对应进程的"系统调用追踪"开关,并在追踪打开时打印出所有被追踪的系统调用的**名称**、**返回值**以及**发起调用的进程 PID**。 ### 2. 实验大致做法 1. **添加系统调用 `trace`** - 在 `kernel/syscall.h` 中定义 `SYS_trace` 的编号。 - 在 `kernel/syscall.c` 中把新的编号映射到你写好的内核处理函数 `sys_trace()`。 - 在 `kernel/sysproc.c` 里实现 `sys_trace()` 函数,核心逻辑就是给当前进程(`myproc()`)的 `syscall_trace` 赋值。 - 在用户态则需要在 `usys.pl`、`user.h` 等处为 `trace` 添加跳板函数和声明,如此才能从用户态方便地调用 `trace(mask)`。 2. **在 `proc.h` 中为进程添加一个 `syscall_trace` 字段** 这个字段用来存储"需要追踪哪些系统调用"的 bitmask,比如如果你传入的 mask 的某一位是 1,就代表需要追踪对应编号的系统调用。 - 父进程在 fork 的时候会把这个 `syscall_trace` 传给子进程,这样父进程设置了 trace 之后,子进程也自动开启对应的追踪。 3. **在 `syscall()` 这个"统一处理入口"里去打印日志** xv6 中所有系统调用都会在内核态走到 `syscall(void)` 函数,所以要做"哪几个系统调用被追踪并打印"的工作,自然就放在这里最好。 - 先根据寄存器 `a7` 拿到系统调用编号 `num`。 - 调用正确的内核处理函数,得到系统调用的返回值 (存入 `p->trapframe->a0`)。 - 如果进程的 `syscall_trace` 对应 bit 置位,就通过 `printf` 打印出 `pid`, `syscall name`, `return value` 等信息。 ## trace 的实现效果 在你完成 **trace** 系统调用并编译好 xv6 之后,你的 shell 里会多出一个用户态的可执行程序(通常叫 **`trace`**),用法大致是: ```Java trace <mask> <command> [<args>...] ``` - **`<mask>`**:一个整数,里面的各个 bit 控制是否追踪对应编号(`SYS_xxx`)的系统调用。 - **`<command>`**:要执行的命令或程序。 - **`<args>...`**:该命令需要的参数。 **当 trace 启动 `<command>` 时,只要 `<mask>` 对某个系统调用的编号进行"位掩码"匹配,这个系统调用就会被跟踪,并在命令执行期间每次系统调用返回前打印一行**。输出格式类似: ```Java <进程ID>: syscall <系统调用名> -> <返回值> ``` 下面给你举一些在 xv6 shell 里可能看到的例子,方便了解 trace 实际如何工作。 --- ### 1. 只追踪一个系统调用(比如 `read`) 假设在 xv6 内核的 `kernel/syscall.h` 中,`SYS_read` 的编号是 5,那么它的位掩码就是 `1 << 5 = 32`。于是: ```sh $ trace 32 grep hello README ``` 这会只追踪 `SYS_read`。示例输出可能是: ```Java 3: syscall read -> 1023 3: syscall read -> 966 3: syscall read -> 70 3: syscall read -> 0 3: syscall close -> 0 ``` > 这里显示进程 PID 为 3,`read` 每次返回了多少字节,最后还有一次 `close`(如果你也把 `close` 的掩码打开,就会显示它的踪迹)。 由于这里只设置了掩码 `32`(即只跟踪 `read`),所以 `exec`、`open` 等其他系统调用并不会输出。 --- ### 2. 跟踪所有系统调用 如果你想看某个程序的**所有系统调用**,可以把 31 位都置为 1,比如 `2147483647 = 0x7fffffff`,这是一个常见的"所有位都打开"的掩码: ```sh $ trace 2147483647 grep hello README ``` 输出示例(省略部分): ```Java 4: syscall trace -> 0 4: syscall exec -> 3 4: syscall open -> 3 4: syscall read -> 1023 4: syscall read -> 966 4: syscall read -> 70 4: syscall read -> 0 4: syscall close -> 0 ... ``` > 可以看到这时会打印 `trace`, `exec`, `open`, `read`, `close` 等所有调用及它们的返回值。 --- ### 3. 不追踪任何系统调用 如果 `<mask>` 是 0,那么不会打印任何跟踪信息: ```sh $ trace 0 grep hello README ``` 此时 grep 照常执行,但不会输出任何 "syscall … -> …" 行。 --- ### 4. 追踪 fork 及其子进程的 fork 假设 `SYS_fork` 的编号是 1,那么掩码就是 `1 << 1 = 2`。例如,有个测试程序叫 `forkforkfork`(会连续地 fork 好几次): ```sh $ trace 2 usertests forkforkfork usertests starting test forkforkfork: 407: syscall fork -> 408 408: syscall fork -> 409 409: syscall fork -> 410 410: syscall fork -> 411 409: syscall fork -> 412 410: syscall fork -> 413 409: syscall fork -> 414 411: syscall fork -> 415 ... ``` 因为你在父进程中打开了 trace,同时在 `fork()` 的实现里会把 `trace mask` 复制到子进程,所以后面所有子进程也继续带着同样的跟踪掩码——只要它们调用 `fork`,就会打出类似的行。 --- ### 5. 追踪多个系统调用 如果你想一次追踪多个系统调用,比如 `fork` (`SYS_fork`) 和 `read` (`SYS_read`),可以把对应的位掩码"或"起来。举例: - `SYS_fork` 编号是 1 → `1 << 1 = 2` - `SYS_read` 编号是 5 → `1 << 5 = 32` 那么 "2 | 32 = 34" 就会同时追踪 `fork` 和 `read`。你可以在 shell 中: ```sh $ trace 34 somecommand ... ``` --- ### 总结 - **`trace <mask> <command>`** 会启动 `<command>` 并对 `<mask>` 指定的系统调用进行跟踪。 - **输出格式**:`PID: syscall xxx -> return_value`。 - **子进程继承**:一旦某进程调用了 `trace(mask)`, 该进程后续的子进程也会带着相同的 `mask`。 通过这些示例,你就能看到 **trace** 在 xv6 shell 里具体如何工作、以及它如何帮助你调试或观察系统调用的执行状况。祝你实验顺利!