实验二: System Call Tracing(添加系统调用接口及其实现)

  • https://pdos.lcs.mit.edu/6.828/2024/labs/syscall.html

用户进程调用系统调用流程分析

  • 用户程序定义了函数的prototype(signature)

  • 同时在usys.pl里定义了stub,关于prototype和stub以及函数实现的不同,后面也有相关介绍

  • sutb里定义了通过指令ecall以及系统调用的参数和函数index等信息告诉内核去调用真正的系统调用函数的实现(将系统调用的函数序号索引放入a7寄存器)

    • 以sys_***形式的函数进行定义和实现

    • 在syscall里进行调用,获取函数的序号和参数,并实现调用

    //num为函数数组的索引号,系统调用在内核中的实现都没有定义参数,参数可以通过函数argaddr进行获取
    p->trapframe->a0 = syscalls[num]();
    
  • ecall指令,RISC-V架构中,ecall中断的入口点由stvec寄存器指定。

  • uservec代码段的功能保存当前用户态执行的cpu的寄存器到trampframe结构中,并将trampframe中保存的内核的栈指针,页表地址等进行加载,并最后调用usertrap函数

  • usertrap最后调用syscall实现系统函数在内核中的真正执行

在 xv6 操作系统中,argraw 函数用于获取系统调用的原始参数。该函数通过访问当前进程的 trapframe 结构来获取系统调用时传递的参数值。
以下是对 argraw 函数的解释和如何与系统调用结合使用的说明。

对于系统调用来说,通常的实现是:

用户程序直接调用系统调用封装函数,并按照 RISC-V ABI 将参数放入寄存器。
系统调用的返回值也通过 a0 寄存器返回。
以 read(fd, buf, size) 为例:

fd(文件描述符)放入 a0。
buf(缓冲区指针)放入 a1。
size(读取的大小)放入 a2。
这些参数会在系统调用触发前(即执行 ecall 之前)被用户程序写入对应的寄存器。


static uint64
argraw(int n)
{
  struct proc *p = myproc();
  switch (n) {
  case 0:
    return p->trapframe->a0;
  case 1:
    return p->trapframe->a1;
  case 2:
    return p->trapframe->a2;
  case 3:
    return p->trapframe->a3;
  case 4:
    return p->trapframe->a4;
  case 5:
    return p->trapframe->a5;
  }
  panic("argraw");
  return -1;
}

// Fetch the nth 32-bit system call argument.
void
argint(int n, int *ip)
{
  *ip = argraw(n);
}

// Retrieve an argument as a pointer.
// Doesn't check for legality, since
// copyin/copyout will do that.
void
argaddr(int n, uint64 *ip)
{
  *ip = argraw(n);
}

// Fetch the nth word-sized system call argument as a null-terminated string.
// Copies into buf, at most max.
// Returns string length if OK (including nul), -1 if error.
int
argstr(int n, char *buf, int max)
{
  uint64 addr;
  argaddr(n, &addr);
  return fetchstr(addr, buf, max);
}

问题描述

创建一个新的trace的系统调用来控制trace,应有一个参数(为整数的mask,其bit位定义了需要trace哪些系统调用,比如需要trace fork系统调用,程序还可以调用 trace(1 << SYS_fork)
SYS_fork为kernel/syscall.h中定义的系统调用的number号,具体任务为修改xv6的kenel代码在每一个系统调用准备返回的时候打印一行,如果系统调用的号在mask的bit位中进行了设置, 打印的内容需要包含其进程id,系统调用的接口名称以及返回值(不需要打印系统调用的参数)

  • trace.c代码的内容

#include "kernel/param.h"
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"

int
main(int argc, char *argv[])
{
  int i;
  char *nargv[MAXARG];

  if(argc < 3 || (argv[1][0] < '0' || argv[1][0] > '9')){
    fprintf(2, "Usage: %s mask command\n", argv[0]);
    exit(1);
  }

  if (trace(atoi(argv[1])) < 0) {
    fprintf(2, "%s: trace failed\n", argv[0]);
    exit(1);
  }
  
  for(i = 2; i < argc && i < MAXARG; i++){
    nargv[i-2] = argv[i];
  }
  nargv[argc-2] = 0;
  exec(nargv[0], nargv);
  printf("trace: exec failed\n");
  exit(0);
}
  • 步骤说明:

    • 第一步:启动trace程序,如trace 32 ....(其中32为trace打印的系统调用的函数和其返回值,...为执行特定的程序,在traced的进程里通过exec覆盖当前进程空间内容)

    • 第二步:由于在user/usys.pl中定义了entry("trace"),根据entry的定义,相当于定义了SYS_trace这个系统调用,通过系统调用trace(将需要打印的系统调用的函数number标识进行位标记后作为参数),修改了进程的结构体信息,添加了trace_mask来存储传入的参数,并且在系统调用是syscall函数时,判断该系统调用是否在需要trace的范畴里

    • Makefile中的相关依赖定义

      $U/usys.S : $U/usys.pl
      perl $U/usys.pl > $U/usys.S
      
      $U/usys.o : $U/usys.S
      $(CC) $(CFLAGS) -c -o $U/usys.o $U/usys.S
      
    • usys.pl和usys.S代码说明

      #!/usr/bin/perl -w
    
      //Generate usys.S, the stubs for syscalls.
    
      print "# generated by usys.pl - do not edit\n";
    
      print "#include \"kernel/syscall.h\"\n";
    
      sub entry {
          my $name = shift;
          print ".global $name\n";
          print "${name}:\n";
          print " li a7, SYS_${name}\n";
          print " ecall\n";
          print " ret\n";
      }
        
      entry("fork");
      entry("exit");
      entry("wait");
      entry("pipe");
      entry("read");
      entry("write");
      entry("close");
      entry("kill");
      entry("exec");
      entry("open");
      entry("mknod");
      entry("unlink");
      entry("fstat");
      entry("link");
      entry("mkdir");
      entry("chdir");
      entry("dup");
      entry("getpid");
      entry("sbrk");
      entry("sleep");
      entry("uptime");
      entry("trace");
    
      部分usys.S
    
      //generated by usys.pl - do not edit
      #include "kernel/syscall.h"
      .global fork
      fork:
      li a7, SYS_fork
      ecall  //内核接收到 ecall 后,根据 a7 寄存器中的系统调用号,确定调用哪个具体的系统调用服务例程。服务例程完成相应的功能后,将结果返回给用户程序。
      ret
      .global exit
      exit:
      li a7, SYS_exit
      ecall
      ret
      .global wait
      wait:
      li a7, SYS_wait
      ecall
      ret
      .global pipe
      pipe:
      li a7, SYS_pipe
      ecall
      ret
      .global read
      read:
      li a7, SYS_read
      ecall
      ret
      .global write
      write:
      li a7, SYS_write
      ecall
      ret
      .global close
      close:
      li a7, SYS_close
      ecall
      ret
    
    

具体实验步骤

    1. 添加 $U/_trace到Makefile中的 UPROGS

    1. 运行 make qemu发现编译环境并没有编译 user/trace.c,因为用户空间为trace系统调用的存根不存在,我们需要:

    • 在user/user.h中添加trace的prototype(类似于function signature,int trace(int)

    • 在user/usys.pl中添加存根(entry("trace"))

    • 在kernel/syscall.h中添加一个syscall的number(#define SYS_trace  22),Makefile会触发perl脚本user/usys.pl,会生成user/usys.S, 会生成实际的系统调用的存根(stubs),会使用RISC-V ecall指令切换到内核态,解决了编译问题, 运行 trace 32 grep hello README时仍然会失败,因为在内核中还尚未实现这个系统调用。 trace 32 grep hello README其中(grep hello README为后面的命令操作,grep为程序名, hello README为参数) 其中The 32 is 1 << SYS_read,因为 SYS_read等于5,所以将1左移5位结果为32。

    1. 进一步在内核中的实现,在 xv6 中添加新的系统调用,并通过 argaddr 函数获取参数,具体的方式可以参考./xv6-labs-2024/kernel/syscall.c

    • 在kernel/proc.h的文件中的 proc数据结构中添加整形变量 track_mask

    • 在kernel/sysproc.c中添加代码:

//第一个参数在在uservec代码段中的`trapframe`数据结构的a0成员存入,

uint64
sys_trace(void)
{
  uint64 p;
  argaddr(0, &p);
  myproc()->trace_mask = p;

  return 0;
}
  • 在kernel/syscall.c中添加 [SYS_trace]   sys_trace,static uint64 (*syscalls[])(void)函数指针数组中

  • 在kernel/proc.c中将trace_mask拷贝到子进程的proc结构中

  • 修改syscall函数内部的实现,如下:

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

  num = p->trapframe->a7;
  //num = * (int *) 0;
  if(num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    // Use num to lookup the system call function for num, call it,
    // and store its return value in p->trapframe->a0
    p->trapframe->a0 = syscalls[num]();
    int trace_ = 1 << num;
    if(trace_ & p->trace_mask == 1)
    {
      printf("sys call %s -> %d\n",
              syscall_name[num], p->trapframe->a0);
    }
  } else {
    printf("%d %s: unknown sys call %d\n",
            p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}
trace 2147483647 grep hello README
4: syscall trace -> 0
trace 2147483647 grep hello README
4: syscall trace -> 0

调用示例:

trace 2147483647 grep hello README
trace 32 grep hello README
trace 160 grep hello README
trace 128 grep hello README
trace 2147483646 grep hello README
trace 2147483000 grep hello README
trace 2100000000 grep hello README
trace 32001 grep hello README

注释: 在 C 语言中,你可以使用数组的索引来初始化特定位置的元素。这种方式称为部分初始化。以下是一些示例:

static char *syscall_names[] = {
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
[SYS_read]    "read",
[SYS_kill]    "kill",
[SYS_exec]    "exec",
[SYS_fstat]   "fstat",
[SYS_chdir]   "chdir",
[SYS_dup]     "dup",
[SYS_getpid]  "getpid",
[SYS_sbrk]    "sbrk",
[SYS_sleep]   "sleep",
[SYS_uptime]  "uptime",
[SYS_open]    "open",
[SYS_write]   "write",
[SYS_mknod]   "mknod",
[SYS_unlink]  "unlink",
[SYS_link]    "link",
[SYS_mkdir]   "mkdir",
[SYS_close]   "close",
[SYS_trace]   "trace",
};

int arr[5] = { [0] = 1, [2] = 3 }; // arr[0] = 1, arr[1] = 0, arr[2] = 3, arr[3] = 0, arr[4] = 0

问题:下面两端代码有何不同,为何第一段成功执行trace 2147483647 grep hello README,而第二段不行 第一段:

    if ((p->trace_mask >> num) & 1) {
      printf("%d: syscall %s -> %lu\n",
              p->pid, syscall_names[num], p->trapframe->a0);
    }
    int trace_ = 1 << num;
    //printf("%d, %lu, %s\n", trace_, p->trace_mask, syscall_name[num]);
    if((trace_ & p->trace_mask) != 0)
    {
      printf("sys call %s -> %lu\n",
              syscall_name[num-1], p->trapframe->a0);
    }


位与操作实验,一个32位的整数(如变量位int track_mask)进行位与操作实验

如
int track_mask = 2147483647;
num = 31;
if((trace_mask >> num) & 1)
{

}

int trace_ = 1 << num;
if((trace_ & trace_mask) != 0)

这两种有没有什么区别请将num从1到31进行测试

问题

1, 在user/user.h中定义的trace的prototype(signature)如何与usys中定义的trace相关联?

  1. 主要区别 特性 Prototype/Signature Stub 功能 描述函数接口,用于编译器检查类型和调用方式。 提供用户态到内核态的桥梁,负责传递参数和返回值。 是否包含实现 不包含实现,仅是声明或定义接口。 包含一个简单实现,但不执行核心逻辑。 所在位置 通常在头文件中(如 .h 文件)。 通常在汇编文件或低级代码(如 usys.S 文件)中。 作用层级 用户程序使用的函数接口。 用户态到内核态的连接器,涉及系统调用。 复杂性 简单,主要描述函数的接口信息。 需要处理参数、系统调用号和切换内核态。

  2. 关系 Prototype/Signature 是用户态程序的声明,用于告知编译器如何使用该函数。 Stub 是函数的用户态实现,调用时会利用系统调用机制将请求转发给内核。 二者通过名称和功能协作,形成完整的系统调用链。例如: user/user.h 定义了 int trace(int mask);。 usys.S 提供了 trace 的 stub,包装系统调用逻辑。 用户程序通过调用 trace(mask),间接调用了内核的 sys_trace 实现。

通过这种分层设计,用户态程序无需了解内核的具体实现细节,系统调用的设计也变得灵活且易于维护。

2, 何时触发syscall函数的调用?

当 CPU 执行 ecall 指令时会陷入内核态,CPU 识别到特权指令 ecall,切换到内核态(修改 CPU 的特权级)。
保存当前用户态的程序计数器(PC)等上下文信息,以便系统调用完成后能够返回用户态继续执行。跳转到内核的中断处理例程:

RISC-V 架构中,ecall 中断的入口点由stvec寄存器指定。 在 xv6 中,这个入口点通常是 kernel/trap.c 文件中的 usertrap() 函数:

//main函数中会调用trapinithart
// set up to take exceptions and traps while in the kernel.
void
trapinithart(void)
{
  w_stvec((uint64)kernelvec);
}

入口点

定义位置

用途

kernelvec

内核态陷入处理入口

处理发生在内核态的中断和异常。比如时钟中断或设备中断时,CPU已在内核态运行,陷入kernelvec。

uservec

用户态陷入处理入口

处理发生在用户态的陷入。比如用户程序执行ecall或遇到异常,陷入uservec。

void usertrap(void) {
    ...
    if (r_scause() == 8) {  // 判断是否为 ecall 系统调用
        syscall();          // 调用 syscall 函数处理系统调用
    } else {
        // 处理其他异常或中断
    }
    ...
}

kerneltrap
// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void 
kerneltrap()
{
  int which_dev = 0;
  uint64 sepc = r_sepc();
  uint64 sstatus = r_sstatus();
  uint64 scause = r_scause();
  
  if((sstatus & SSTATUS_SPP) == 0)
    panic("kerneltrap: not from supervisor mode");
  if(intr_get() != 0)
    panic("kerneltrap: interrupts enabled");

  if((which_dev = devintr()) == 0){
    // interrupt or trap from an unknown source
    printf("scause=0x%lx sepc=0x%lx stval=0x%lx\n", scause, r_sepc(), r_stval());
    panic("kerneltrap");
  }

  // give up the CPU if this is a timer interrupt.
  if(which_dev == 2 && myproc() != 0)
    yield();

  // the yield() may have caused some traps to occur,
  // so restore trap registers for use by kernelvec.S's sepc instruction.
  w_sepc(sepc);
  w_sstatus(sstatus);
}

3,myproc函数的实现细节

在 xv6 的实现中,myproc 通常使用 CPU 本地存储(cpu-local storage)来找到当前运行进程。

// Per-CPU state.
struct cpu {
  struct proc *proc;          // The process running on this cpu, or null.
  struct context context;     // swtch() here to enter scheduler().
  int noff;                   // Depth of push_off() nesting.
  int intena;                 // Were interrupts enabled before push_off()?
};

struct proc *
myproc(void)
{
    struct cpu *c;
    push_off();                // 关中断
    c = mycpu();               // 获取当前 CPU 的结构体指针
    struct proc *p = c->proc;  // 获取当前 CPU 上运行的进程
    pop_off();                 // 开中断
    return p;                  // 返回当前进程指针
}

4、系统调用返回时系统的行为?

确认kernelvec和uservec的不同,以及stvec在何时设置kernelvec和uservec?

kernelvec位于kernelvec.S

        #
        # interrupts and exceptions while in supervisor
        # mode come here.
        #
        # the current stack is a kernel stack.
        # push registers, call kerneltrap().
        # when kerneltrap() returns, restore registers, return.
        #
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
        # make room to save registers.
        addi sp, sp, -256

        # save caller-saved registers.
        sd ra, 0(sp)
        sd sp, 8(sp)
        sd gp, 16(sp)
        sd tp, 24(sp)
        sd t0, 32(sp)
        sd t1, 40(sp)
        sd t2, 48(sp)
        sd a0, 72(sp)
        sd a1, 80(sp)
        sd a2, 88(sp)
        sd a3, 96(sp)
        sd a4, 104(sp)
        sd a5, 112(sp)
        sd a6, 120(sp)
        sd a7, 128(sp)
        sd t3, 216(sp)
        sd t4, 224(sp)
        sd t5, 232(sp)
        sd t6, 240(sp)

        # call the C trap handler in trap.c
        call kerneltrap

        # restore registers.
        ld ra, 0(sp)
        ld sp, 8(sp)
        ld gp, 16(sp)
        # not tp (contains hartid), in case we moved CPUs
        ld t0, 32(sp)
        ld t1, 40(sp)
        ld t2, 48(sp)
        ld a0, 72(sp)
        ld a1, 80(sp)
        ld a2, 88(sp)
        ld a3, 96(sp)
        ld a4, 104(sp)
        ld a5, 112(sp)
        ld a6, 120(sp)
        ld a7, 128(sp)
        ld t3, 216(sp)
        ld t4, 224(sp)
        ld t5, 232(sp)
        ld t6, 240(sp)

        addi sp, sp, 256

        # return to whatever we were doing in the kernel.
        sret

uservec位于trampoline.S中定义

.section trampsec
.globl trampoline
.globl usertrap
trampoline:
.align 4
.globl uservec
uservec:    
	#
        # trap.c sets stvec to point here, so
        # traps from user space start here,
        # in supervisor mode, but with a
        # user page table.
        #

        # save user a0 in sscratch so
        # a0 can be used to get at TRAPFRAME.
        csrw sscratch, a0 //将a0寄存器的值写入sscratch寄存器(临时存储寄存器,供操作系统或异常处理使用。),a0一般为函数参数,这里腾出a0来供加载TRAMFRAME内存地址

        # each process has a separate p->trapframe memory area,
        # but it's mapped to the same virtual address
        # (TRAPFRAME) in every process's user page table.
        li a0, TRAPFRAME
        
        # save the user registers in TRAPFRAME
        sd ra, 40(a0)
        sd sp, 48(a0)
        sd gp, 56(a0)
        sd tp, 64(a0)
        sd t0, 72(a0)
        sd t1, 80(a0)
        sd t2, 88(a0)
        sd s0, 96(a0)
        sd s1, 104(a0)
        sd a1, 120(a0)
        sd a2, 128(a0)
        sd a3, 136(a0)
        sd a4, 144(a0)
        sd a5, 152(a0)
        sd a6, 160(a0)
        sd a7, 168(a0)
        sd s2, 176(a0)
        sd s3, 184(a0)
        sd s4, 192(a0)
        sd s5, 200(a0)
        sd s6, 208(a0)
        sd s7, 216(a0)
        sd s8, 224(a0)
        sd s9, 232(a0)
        sd s10, 240(a0)
        sd s11, 248(a0)
        sd t3, 256(a0)
        sd t4, 264(a0)
        sd t5, 272(a0)
        sd t6, 280(a0)

	# save the user a0 in p->trapframe->a0
        csrr t0, sscratch //,sscratch为刚才暂存的a0,然后将其读入t0寄存器。
        sd t0, 112(a0) 保存用户态 a0 到 trapframe->a0//将系统调用接口的参数a0(第一个参数)保存到trapframe数据结构,其他的如a1,a2等已经在上面的代码中存入

        # initialize kernel stack pointer, from p->trapframe->kernel_sp
        ld sp, 8(a0)

        # make tp hold the current hartid, from p->trapframe->kernel_hartid
        ld tp, 32(a0)

        # load the address of usertrap(), from p->trapframe->kernel_trap
        ld t0, 16(a0)

        # fetch the kernel page table address, from p->trapframe->kernel_satp.
        ld t1, 0(a0)

        # wait for any previous memory operations to complete, so that
        # they use the user page table.
        sfence.vma zero, zero

        # install the kernel page table.
        csrw satp, t1

        # flush now-stale user entries from the TLB.
        sfence.vma zero, zero

        # jump to usertrap(), which does not return
        jr t0

.globl userret
userret:
        # userret(pagetable)
        # called by usertrapret() in trap.c to
        # switch from kernel to user.
        # a0: user page table, for satp.

        # switch to the user page table.
        sfence.vma zero, zero
        csrw satp, a0
        sfence.vma zero, zero

        li a0, TRAPFRAME

        # restore all but a0 from TRAPFRAME
        ld ra, 40(a0)
        ld sp, 48(a0)
        ld gp, 56(a0)
        ld tp, 64(a0)
        ld t0, 72(a0)
        ld t1, 80(a0)
        ld t2, 88(a0)
        ld s0, 96(a0)
        ld s1, 104(a0)
        ld a1, 120(a0)
        ld a2, 128(a0)
        ld a3, 136(a0)
        ld a4, 144(a0)
        ld a5, 152(a0)
        ld a6, 160(a0)
        ld a7, 168(a0)
        ld s2, 176(a0)
        ld s3, 184(a0)
        ld s4, 192(a0)
        ld s5, 200(a0)
        ld s6, 208(a0)
        ld s7, 216(a0)
        ld s8, 224(a0)
        ld s9, 232(a0)
        ld s10, 240(a0)
        ld s11, 248(a0)
        ld t3, 256(a0)
        ld t4, 264(a0)
        ld t5, 272(a0)
        ld t6, 280(a0)

	# restore user a0
        ld a0, 112(a0)
        
        # return to user mode and user pc.
        # usertrapret() set up sstatus and sepc.
        sret

trapframe 数据结构的具体定义

// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
  /*   0 */ uint64 kernel_satp;   // kernel page table
  /*   8 */ uint64 kernel_sp;     // top of process's kernel stack
  /*  16 */ uint64 kernel_trap;   // usertrap()
  /*  24 */ uint64 epc;           // saved user program counter
  /*  32 */ uint64 kernel_hartid; // saved kernel tp
  /*  40 */ uint64 ra;
  /*  48 */ uint64 sp;
  /*  56 */ uint64 gp;
  /*  64 */ uint64 tp;
  /*  72 */ uint64 t0;
  /*  80 */ uint64 t1;
  /*  88 */ uint64 t2;
  /*  96 */ uint64 s0;
  /* 104 */ uint64 s1;
  /* 112 */ uint64 a0;
  /* 120 */ uint64 a1;
  /* 128 */ uint64 a2;
  /* 136 */ uint64 a3;
  /* 144 */ uint64 a4;
  /* 152 */ uint64 a5;
  /* 160 */ uint64 a6;
  /* 168 */ uint64 a7;
  /* 176 */ uint64 s2;
  /* 184 */ uint64 s3;
  /* 192 */ uint64 s4;
  /* 200 */ uint64 s5;
  /* 208 */ uint64 s6;
  /* 216 */ uint64 s7;
  /* 224 */ uint64 s8;
  /* 232 */ uint64 s9;
  /* 240 */ uint64 s10;
  /* 248 */ uint64 s11;
  /* 256 */ uint64 t3;
  /* 264 */ uint64 t4;
  /* 272 */ uint64 t5;
  /* 280 */ uint64 t6;
};