xv6 Syscall Tracing

In this lab, we will implement a simple strace-like syscall tracing on xv6 to understand what system calls are made by an user program.

This lab has two parts: We will start with a warm-up exercise: you will add the find.c user program into xv6.

After you are not cold, you can complete the lab by following three steps:

Step 1: Add a new trace system call to mark a process for syscall tracing.

Step 2: Extend the kernel system call dispatcher to log syscalls made by traced processes.

Step 3: Write a simple user-space strace program that attaches to a child and observes its syscalls, similar to strace on Linux.

By the end of this lab, you will know how to …

  • Implement new system calls in xv6
  • Understand process structure and the flow of dispatching a system call
  • Create a user program in xv6

Getting Started

Please clone the starter code from Github:

open in GitHub.

Warm-up exercise: Add find Command

Before starting the main lab, you will add a simple find command to get familiar with xv6 file operations (we provide you the code, you just copy paste it 😘😘). find command recursively searches for files matching a pattern:

find [directory_name] [filename_pattern]

The find command should:

  • Recursively traverse all subdirectories under the given directory

  • Print the full path of all files whose names match the given pattern

  • Skip the . (current) and .. (parent) directory entries

  • Handle nested directory structures correctly

Expected Output Format

When you run these instruction:

$ echo > b
$ mkdir a  
$ echo > a/b
$ mkdir a/aa
$ echo > a/aa/b

and

$ find . b

Your output will be:

./b
./a/b
./a/aa/b

If you have already run the above command, running it again will fail because the file has already been created.

$ echo > b
$ echo > b
exec echo failed

Create the file

  • Copy and paste the following code into user/find.c:
#include "kernel/types.h"
#include "kernel/stat.h"
#include "user/user.h"
#include "kernel/fs.h"

int ismatch(char*s,char*p){
  int advance = 1 ;//advance p
  if(*p == 0)
    return *s == 0;
  if(*p && *(p+1) && *(p+1)=='*'){
    if(ismatch(s,p+2))
      return 1;
    advance = 0;
  }
  if((*s&&*p=='.')||*s==*p)
    return ismatch(s+1,p+advance);
  return 0;
}


char buf[512];
char*
fmtname(char *path)
{
  static char buf[DIRSIZ+1];
  char *p;

  // Find first character after last slash.
  for(p=path+strlen(path); p >= path && *p != '/'; p--)
    ;
  p++;

  // Return blank-padded name.
  if(strlen(p) >= DIRSIZ)
    return p;
  memmove(buf, p, strlen(p));
  memset(buf+strlen(p), '\0', DIRSIZ-strlen(p));
  return buf;
}

void
find(char *path,char*name)
{
  char *p;
  int fd;
  struct dirent de;
  struct stat st;

  if((fd = open(path, 0)) < 0){
    fprintf(2, "ls: cannot open %s\n", path);
    return;
  }

  if(fstat(fd, &st) < 0){
    fprintf(2, "ls: cannot stat %s\n", path);
    close(fd);
    return;
  }

  switch(st.type){
  case T_FILE:
    if(ismatch(fmtname(path),name)!=0){
        printf("%s\n",path);
    }
    break;

  case T_DIR:
    if(strlen(path) + 1 + DIRSIZ + 1 > sizeof buf){
      printf("ls: path too long\n");
      break;
    }
    strcpy(buf, path);
    p = buf+strlen(buf);
    *p++ = '/';
    while(read(fd, &de, sizeof(de)) == sizeof(de)){
      if(de.inum == 0 || strcmp(de.name,".")==0 || strcmp(de.name,"..")==0)
        continue;
      memmove(p, de.name, DIRSIZ);
      p[DIRSIZ] = 0;
      find(buf,name);
    }
    break;
  }
  close(fd);
}

int
main(int argc, char *argv[])
{
  find(argv[1],argv[2]);

  exit(0);
}

Modify the Makefile

Add find to the UPROGS list in the Makefile.

Testing

If you complete all the steps, run the grading script to see if the behavior of find meets the testing data.

python3 grade-find.py

Main lab: Syscall tracing

Once you complete the warm-up exercise and pass the tests, we can start the main lab: syscall tracing.

Step 1: Add Process Tracing State

Add tracing functionality to the process structure.

How?: Try to add a tracing flag traced to the process structure in kernel/proc.h.

Step 2: Implement find_proc_by_pid()

Implement find_proc_by_pid() to find a process by PID.

Modify the file

  • kernel/proc.c: Please implement find_proc_by_pid()

  • kernel/defs.h: Add the prototypes for find_proc_by_pid()

this function will:

  • Take PID as parameter
  • Search through the process table
  • Return a pointer to the matching process if found; otherwise, return NULL.
  • Handle the case where PID is invalid or process doesn’t exist

Pseudo Code

struct proc* now_proc;

// Search through the process table

  // if the matching process is found

  // return pointer to the matching process


// the matching process is not found
return NULL;

Step 3: Implement sys_trace syscall

Add a new system call that allows us to trace a specific process (Learn how to add a new system call here

Implement sys_trace()

this function will:

  • Accept one integer argument (PID)
  • Find the process with that PID using find_proc_by_pid()
  • Set the tracing flag for matching process
  • Return 0 on success, -1 on failure (if process not found)

Pseudo Code


int pid;
// Accept argument (PID)

struct proc *p = find_proc_by_pid(pid); // implement this function
if (p != NULL){
  // The process with the specified PID exists
}
else{
  // No such process
}

What would happen if the tracing flag is not set for the matching process?

Modify the file

  • kernel/sysproc.c: Implement sys_trace()
  • kernel/syscall.h: Add syscall number definition
  • kernel/syscall.c: Register the syscall
  • user/user.h: Add user-space declaration
  • user/usys.pl: Add system call stub

Step 4: Modify syscall.c

Modify the syscall dispatcher to log traced system calls.

Modify the file

  • kernel/syscall.c:
  1. Define the syscall name table

At the top of syscall.c, Create a static array mapping syscall numbers to name strings

static char *syscall_names[] = {
[SYS_fork]    "fork",
[SYS_exit]    "exit",
[SYS_wait]    "wait",
[SYS_pipe]    "pipe",
// ... Complete me
};
  1. Add syscall argument printing in syscall()
Expected Output Format

For each traced syscall, we print:

[pid X] syscall_name(first_argument) = return_value
Argument Printing Rules
  • String arguments (for open, chdir, mkdir, unlink, link): Print as “string”
  • Exec syscall: Print the program name from argv[0] as “program” (When we run ls, the output should be exec(“ls”))
  • All other syscalls: Print first argument as integer
  • Invalid pointers: Print “<bad ptr>”
Pseudo Code
void syscall(void)
{

  int num;
  struct proc *p = myproc();
  num = p->trapframe->a7;
  if (num > 0 && num < NELEM(syscalls) && syscalls[num])
  {

    
    uint64 arg0 = p->trapframe->a0; // Save first argument BEFORE syscall dispatch

    p->trapframe->a0 = syscalls[num](); // Use num to lookup the system call function for num, call it, and store its return value in p->trapframe->a0


    if (p->traced)
    {

      // Print syscall name, first argument

      if (num == SYS_open || num == SYS_unlink ||  
          num == SYS_chdir || num == SYS_mkdir || num == SYS_link
          /* ignore the second string argument of `link` */)
      {
         // If the syscall is one of these five...  
      }
      else if ( num == SYS_exec ){
         // If the syscall is exec... 
      }
      else
      {
         // If the syscall is any of the others...
      }

      // Print the remaining part.
    }
  }
  else
  {
    printf("%d %s: unknown sys call %d\n",
           p->pid, p->name, num);
    p->trapframe->a0 = -1;
  }
}
Example Output
[pid 6] exec("grep") = 3
[pid 6] open("README") = 3
[pid 6] read(3) = 1023
[pid 6] close(3) = 0

What would happen if we don’t handle SYS_exec separately from the other five syscalls?

Step 5: Create a User Program strace

Create a user program that traces system calls of child processes.

Create the file user/strace.c

This user program will:

  • Accept command-line arguments: strace [args...]
  • Fork a child process to run the specified command
  • Enable tracing for the child process
  • Wait for child completion

This user program should:

  • Validate command-line arguments (at least 2 arguments required)
  • Use fork() to create child process
  • In child: exec() the target program with its arguments
  • In parent: call trace() on child PID, then wait() for completion
If you find something strange after modifying something, try running:
> make clean
> make

before executing

> make qemu

The Interleaved Output Problem

When you first run:

$ strace echo hello

you might see something like:

[pid 5] exec("echo") = 2
hello[pid 5] write(1) = 5
[pid 5] write(1) = 1

Notice that the "hello" output appears in the middle of the syscall trace? But this is not good. We don’t want the output of traced program and tracing messages to mix up.

  • Why does this happen?
  • Which part of xv6 is responsible for writing user output to the console?
  • When the kernel prints syscall tracing info, is it also writing to the same console device?

Try to reason it out, and try to dig the kernel with gdb.

How to Fix It

Hints:

  • User write() calls eventually reach consolewrite() in kernel/console.c.
  • Both the user’s output (hello) and your tracing output use the console.
  • But we only want the kernel tracer to print after or outside of user writes. To simplify, we completely drop the user write.
  • The current process can be accessed using myproc().

Think about how you can modify the behavior so that the syscall tracing and the user’s output don’t interfere with each other is dropped. Once you fix it, your result should look like this:

$ strace echo hello
[pid 5] exec("echo") = 2
[pid 5] write(1) = 5
[pid 5] write(1) = 1

Testing

If you have completed the above steps, please test your implementation:

$ strace grep hello README
[pid 6] exec("grep") = 3
[pid 6] open("README") = 3
[pid 6] read(3) = 1023
[pid 6] read(3) = 971
[pid 6] read(3) = 298
[pid 6] read(3) = 0
[pid 6] close(3) = 0

Understanding the Output

Example:

  • [pid 4] exec("grep") = 3 → Process 4 executed grep using the exec syscall.
  • [pid 6] open("README") = 3 → Process 6 opened file README and got file descriptor 3.

Run the Grading Script

When everything seems correct, verify your implementation using:

python3 grade-syscall_trace.py

Then also test:

strace find a b
strace ls a

Final Reflection & Demo

Make sure you can reason about the following questions:

  1. Why was the output interleaved earlier, and how did your fix resolve it?
  2. Why must we treat SYS_exec differently from other syscalls?
  3. What happens if the tracing flag is never cleared or reset?
  4. Why do we print only the first argument of each syscall?

TA will ask you these questions during the demo.

During Demo Time

Please bring your own laptop to the classroom. You will be asked to:

  1. Run the grading scripts:
python3 grade-find.py
python3 grade-syscall_trace.py
  1. Demonstrate that all tests pass.
  2. Explain the answers to the reflection questions above.
Back to top