打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
OS Lab 2.1 - System call tracing (moderate)
1
实验要求

https://pdos.csail.mit.edu/6.828/2020/labs/syscall.html

2
实验前须知

阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件:

  • 系统调用的用户空间代码在 user/user.huser/usys.pl 中。
  • 内核空间代码在 kernel/syscall.hkernel/syscall.c 中。
  • 与进程相关的代码在 kernel/proc.hkernel/proc.c 中。

使用下面的命令切换到 syscall 分支。

$ git fetch
$ git checkout syscall
$ make clean

推荐新建文件夹,重新使用下面的命令下载代码作为实验 2 工作区。

$ git clone git://g.csail.mit.edu/xv6-labs-2020
$ git checkout syscall

3
实验目的
  • 添加一个系统调用跟踪功能,该功能可以在以后的实验中为你提供帮助。
  • 你将创建一个新的 trace 系统调用来控制跟踪。
  • 它应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。例如,为了跟踪 fork 系统调用,程序调用 trace (1 << SYS_fork) ,其中 SYS_fork 是来自 kernel/syscall.h 的系统调用号。
  • 如果掩码中设置了系统调用的编号,则必须修改 xv6 内核以在每个系统调用即将返回时打印出一行。
  • 该行应包含 进程 ID系统调用名称返回值 ;您不需要打印系统调用参数。trace 系统调用应该为调用它的进程和它随后派生的任何子进程启用跟踪,但不应影响其他进程。

4
实验要求及提示
  • $U/_trace 添加到 MakefileUPROGS
  • 运行 make qemu , 你将看到编译器无法编译 user/trace.c ,因为系统调用的用户空间存根还不存在:将系统调用的原型添加到 user/user.h ,将存根添加到 user/usys.pl ,以及将系统调用号添加到 kernel/syscall.h 中。Makefile 调用 perl 脚本 user/usys.pl ,它生成 user/usys.S ,实际的系统调用存根,它使用 RISC-V ecall 指令转换到内核。修复编译问题后,运行 trace 32 grep hello README ;它会失败,因为你还没有在内核中实现系统调用。
  • kernel/sysproc.c 中添加一个 sys_trace() 函数,该函数通过在 proc 结构中的新变量中记住其参数来实现新系统调用(请参阅 kernel/proc.h )。从用户空间检索系统调用参数的函数位于 kernel/syscall.c 中,你可以在 kernel/sysproc.c 中查看它们的使用示例。
  • 修改 fork() (参见 kernel/proc.c )以将跟踪的掩码从父进程复制到子进程。
  • 修改 kernel/syscall.c 中的 syscall() 函数以打印跟踪输出。你将需要添加要索引的系统调用名称数组。

5
实验思路
  1. 首先,根据实验前须知阅读 xv6 文档的第 2 章和第 4 章的 4.3 节和 4.4 节以及相关源文件。其中第 2 章讲的是 xv6 系统的组织结构,第 4 章的 4.3 节讲的是调用 system call 的过程,第 4 章的 4.4 节讲的是调用 system call 的参数。与本实验直接相关,所以必须依照源码进行阅读。
  2. 这里补充一点,做这个实验需要对 xv6 启动过程以及调用系统调用过程有一些了解,具体可以观看上课视频的结尾部分,视频地址:https://www.bilibili.com/video/BV19k4y1C7kA?p=2
  3. 具体过程解释见下面的实验步骤。

6
实验步骤

作为一个系统调用,我们先要定义一个系统调用的序号。系统调用序号的宏定义在 kernel/syscall.h 文件中。我们在 kernel/syscall.h 添加宏定义,模仿已经存在的系统调用序号的宏定义,我们定义 SYS_trace 如下:

#define SYS_trace  22

查看了一下 user 目录下的文件,发现官方已经给出了用户态的 trace 函数( user/trace.c ),所以我们直接在 user/user.h 文件中声明用户态可以调用 trace 系统调用就好了,但有一个问题,该系统调用的参数和返回值分别是什么类型呢?接下来我们还是得看一看 trace.c 文件,可以看到 trace(atoi(argv[1])) < 0 ,即 trace 函数传入的是一个数字,并和 0 进行比较,结合实验提示,我们知道传入的参数类型是 int ,并且由此可以猜测到返回值类型应该是 int 。这样就可以把 trace 这个系统调用加入到内核中声明了:

// system calls
int trace(int);

接下来我们查看 user/usys.pl 文件,这里 perl 语言会自动生成汇编语言 usys.S ,是用户态系统调用接口。所以在 user/usys.pl 文件加入下面的语句:

entry("trace");

如果你编译后查看 usys.S 文件,就能可以看到存在把系统调用号放入 a7 寄存器的指令,然后就直接使用命令 ecall 进入系统内核。不信我们先查看上一次实验编译后的 usys.S 文件,可以看到如下的代码块:

.global fork
fork:
li a7, SYS_fork
ecall
ret

li a7, SYS_fork 指令就是把 SYS_fork 的系统调用号放入 a7 寄存器,使用 ecall 指令进入系统内核。

那么,执行 ecall 指令会跳转到哪里呢?答案是跳转到 kernel/syscall.csyscall 那个函数处,执行此函数。下面是 syscall 函数的源码:

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

其中,我们可以看到, num = p->trapframe->a7; 从寄存器 a7 中读取系统调用号,所以上面的 usys.S 文件就是系统调用用户态和内核态的切换接口。接下来是 p->trapframe->a0 = syscalls[num](); 语句,通过调用 syscalls[num](); 函数,把返回值保存在了 a0 寄存器中。我们看看 syscalls[num](); 函数,这个函数在当前文件中。该函数调用了系统调用命令。

static uint64 (*syscalls[])(void) = {
  [SYS_fork]    sys_fork,
  [SYS_exit]    sys_exit,
  ...
}

所以我们把新增的 trace 系统调用添加到函数指针数组 *syscalls[] 上:

static uint64 (*syscalls[])(void) = {
  ...
  [SYS_trace]   sys_trace,
};

接下来在文件开头给内核态的系统调用 trace 加上声明,在 kernel/syscall.c 加上:

extern uint64 sys_trace(void);

在实现这个函数之前,我们可以看到实验最后要输出每个系统调用函数的调用情况,依照实验说明给的示例,可以知道最后输出的格式如下:

<pid>: syscall <syscall_name> -> <return_value>

其中, <pid> 是进程序号, <syscall_name> 是函数名称, <return_value> 是该系统调用的返回值。注意:冒号和 syscall 中间有个空格,刚开始的时候自己就踩了一个坑。

根据提示,我们的 trace 系统调用应该有一个参数,一个整数“mask(掩码)”,其指定要跟踪的系统调用。所以,我们在 kernel/proc.h 文件的 proc 结构体中,新添加一个变量 mask ,使得每一个进程都有自己的 mask ,即要跟踪的系统调用。

struct proc {
  ...
  int mask;               // Mask
};

然后我们就可以在 kernel/sysproc.c 给出 sys_trace 函数的具体实现了,只要把传进来的参数给到现有进程的 mask 就好了:

uint64
sys_trace(void)
{
  int mask;
  // 取 a0 寄存器中的值返回给 mask
  if(argint(0, &mask) < 0)
    return -1;
  
  // 把 mask 传给现有进程的 mask
  myproc()->mask = mask;
  return 0;
}

接下来我们就要把输出功能实现,因为 RISCV 的 C 规范是把返回值放在 a0 中,所以我们只要在调用系统调用时判断是不是 mask 规定的输出函数,如果是就输出。

因为 proc 结构体(见 kernel/proc.h )里的 name 是整个线程的名字,不是函数调用的函数名称,所以我们不能用 p->name ,而要自己定义一个数组,我这里直接在 kernel/syscall.c 中定义了,这里注意系统调用名字一定要按顺序,第一个为空,当然你也可以去掉第一个空字符串,但要记得取值的时候索引要减一,因为这里的系统调用号是从 1 开始的。

static char *syscall_names[23] = {
  """fork""exit""wait""pipe"
  "read""kill""exec""fstat""chdir"
  "dup""getpid""sbrk""sleep""uptime"
  "open""write""mknod""unlink""link"
  "mkdir""close""trace"};

然后我们就可以在 kernel/syscall.c 中的 syscall 函数中添加打印调用情况语句。mask 是按位判断的,所以判断使用的是按位运算。进程序号直接通过 p->pid 就可以取到,函数名称需要从我们刚刚定义的数组中获取,即 syscall_names[num] ,其中 num 是从寄存器 a7 中读取的系统调用号,系统调用的返回值就是寄存器 a0 的值了,直接通过 p->trapframe->a0 语句获取即可。注意上面说的那个空格。

void
syscall(void)
{
  int num;
  struct proc *p = myproc();

  num = p->trapframe->a7;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    p->trapframe->a0 = syscalls[num]();
    // 下面是添加的部分
    if((1 << num) & p->mask) {
      printf("%d: syscall %s -> %d\n", p->pid, syscall_names[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}

然后在 kernel/proc.cfork 函数调用时,添加子进程复制父进程的 mask 的代码:

int
fork(void)
{
  ...

  pid = np->pid;

  np->state = RUNNABLE;

  // 子进程复制父进程的 mask 
  np->mask = p->mask;

  ...
}

最后在 MakefileUPROGS 中添加:

UPROGS=\
  ...
  $U/_trace\

7
实验结果

编译并运行 xv6 进行测试。

$ make qemu
...
init: starting sh
$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 966
3: syscall read -> 70
3: syscall read -> 0
$
$ trace 2147483647 grep hello README
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
$
$ grep hello README
$
$ 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
...
$   
8
测试实验结果

退出 xv6 ,运行单元测试检查结果是否正确。

./grade-lab-syscall trace

通过测试样例。

== Test trace 32 grep == trace 32 grep: OK (2.6s) 
== Test trace all grep == trace all grep: OK (1.0s) 
== Test trace nothing == trace nothing: OK (0.5s) 
== Test trace children == trace children: OK (10.8s) 

👇周一至周五更新,期待你的关注👇

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
Using perf
向linux内核添加系统调用新老内核比较
利用 HookZz 实现反调试与绕过的奇淫技巧
深入理解Linux的系统调用
系统调用
关于linux系统如何实现fork的研究
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服