xv6, line by line
lab 1

Extension labs · lab 1 · The system-call and trap path · ★☆☆☆☆

trace: logging system calls

You add a new system call, trace(mask), and a command, trace MASK command args.... While a process’s mask has bit i set, the kernel prints one line each time system call number i returns: 3: syscall read -> 1023. Children made by fork inherit the mask, and exec keeps it, so trace 32 grep hello README shows every read that grep makes.

The feature is small (about fifty lines of kernel code, half of them a table of names), but building it walks you through every layer a system call crosses, from the three-instruction user stub to the trapframe and back. You will see every place a new system call must be registered and why each one exists, where per-process state lives and what fork copies under which lock, and why the only place that can print the return value is right after the call in syscall. On three harts, you will also see that printk’s lock keeps trace lines whole with respect to each other, but not with respect to a user program’s output.

Read first: Tour 5: Life of a system call, Tour 6: System-call arguments and user pointers, Tour 20: fork, Tour 22: exec, Tour 38: Output to the console from three harts · The stacks of xv6, Locks and interrupt state

What this lab teaches

  • The full path of a system call: the system call stub puts the number in a7, ecall traps, uservec saves every register in the trapframe, usertrap calls syscall, which indexes syscalls[], and the return value travels back to user space through the saved a0.
  • Every place a new system call must be registered (the number, the stub generator, the user prototype, the kernel table and its prototype) and what breaks, at compile time, link time or run time, when one is missing.
  • Where per-process state lives in struct proc, which fields need p->lock and which are private to the process, and what kfork copies while it holds the child’s lock.
  • Why a forked child never passes through syscall on its way back from fork, and why a successful exit never returns to it.
  • What pr.lock in printk guarantees on three harts (whole lines among kernel messages) and what it does not (it does not exclude user output written through uartwrite).
  • How argint reads an integer argument from the trapframe, and why pointer arguments need copyin instead.

The reference branch

ext/01-trace in ShowMeTheStack/xv6-riscv-labs, branched from the frozen commit 06aad25; 6 commits.

git clone https://github.com/ShowMeTheStack/xv6-riscv-labs
cd xv6-riscv-labs
git checkout -b my-trace 06aad25   # start your own
git diff 06aad25 origin/ext/01-trace   # only when you want the answer

1. The spec

The system call. int trace(int mask) sets the calling process’s trace mask and returns the previous mask. Bit i of the mask enables tracing of system call number i (the numbers in kernel/syscall.h; trace itself gets number 23). It never fails.

What gets printed. When a traced system call returns to the process, the kernel prints one line on the console:

<pid>: syscall <name> -> <return value>

The value is printed as a signed 64-bit number, so a failure shows as -1. A system call that never returns (a successful exit) prints nothing.

Inheritance. A child created by fork starts with its parent’s mask. exec keeps the mask. A child changing its own mask does not change its parent’s.

The command. trace MASK command args... sets the mask, then execs the command:

$ trace 32 grep hello README
3: syscall read -> 1023
3: syscall read -> 965
3: syscall read -> 453
3: syscall read -> 0

(32 is 1 << 5, and SYS_read is 5. 2147483647 sets every bit up to 30 and traces everything.)

The test. tracetest checks, and prints OK or FAIL for each: that trace returns the previous mask, that fork copies the mask to the child, that a child’s change leaves the parent alone, and that exec keeps the mask. A program cannot read the console, so the printing itself is checked by eye: between a begin and an end marker only getpid is traced, the parent and a child each call getpid once (plus untraced uptime, fork and wait calls, and the child’s exit; the markers themselves are printed outside that window), and the test then prints the two lines that should have appeared:

tracetest: begin
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: end
tracetest: expected between the markers, and nothing else:
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: 4 checks OK; now compare the lines between the markers by eye

The last line deliberately does not claim the printing is right: only your comparison can say that.

Constraints. Untraced processes must behave exactly as before, and usertests -q must still print ALL TESTS PASSED on three harts. Do not change any existing system call’s number.

2. Think first

Answer each question in your head (or on paper) before opening a hint. Hints get more specific; the reference answer comes last.

1Where must a new system call be registered?

List every file you must touch before a user program can call trace(32) and reach a kernel function sys_trace. For each, say what would go wrong without it: a compile error, a link error, or a failure at run time.

Check yourself

1warm-upMatch the pairs

Match each change to what it provides.

2How do the number and the mask reach sys_trace?

The trace program calls trace(32). Trace the two values, 23 and 32, from that call to the moment sys_trace holds 32 in a local variable. Name the register each value travels in, where it is saved, and who reads it back.

Check yourself

1solidPut in order

Put the steps of trace(32) in the order they happen.

  1. argint reads trapframe->a0
  2. The compiler’s call puts 32 in a0
  3. usertrap adds 4 to the saved epc and turns interrupts on
  4. uservec saves a0 and a7 in the trapframe
  5. ecall: the hart enters supervisor mode at uservec
  6. The stub executes li a7,23
  7. syscall() reads trapframe->a7 and calls syscalls[23]
2solidFill in the machine state

sys_trace is running its first line, argint(0, &mask). Fill in the machine state of the hart running it.

3Why is argint enough here?

sys_trace reads its argument with one call to argint and never checks it. Is that safe? What would you have to do differently if the argument were a pointer, say trace(int *mask)?

Check yourself

1solidChoose one

Suppose trace took int *mask and sys_trace did argaddr(0, &a); mask = *(int *)a;. What is wrong?

4Where does the mask live, and who may touch it?

The mask belongs to a process. Which structure gets the new field, in which section, and which lock (if any) must be held to read or write it? Consider the three places that will touch it: sys_trace, syscall() and kfork.

Check yourself

1solidTrue or false, and why

True or false: in kfork, reading the parent’s p->tracemask must be done while holding the parent’s p->lock.

Why?

5Where exactly do you print?

You need the system call’s number, its name and its return value. Which function is the one place every system call passes through, and on which line must the print go so that the return value is known?

Check yourself

1warm-upClick the line

Click the line right after which the trace check must go.

kernel/syscall.c
136void
139 int num;
140 struct proc *p = myproc();
143 if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
144 // Use num to lookup the system call function for num, call it,
145 // and store its return value in p->trapframe->a0
147 } else {
148 printk("%d %s: unknown sys call %d\n", p->pid, p->name, num);
149 p->trapframe->a0 = -1;
150 }

Your pick: none yet (click a line in the code)

2solidType a number

Suppose the print is placed before line 146. grep calls read(3, buf, 1023) and the call returns 1023. What number does the trace line show after ->?

decimal, 0x hex or 0b binary

6What should fork, exec and exit do with the mask?

The spec says children inherit the mask and exec keeps it. Which functions must change? And when a traced sh forks, how many fork lines should the trace print, one or two? You know where the print is now (the previous question); use that.

Check yourself

1solidChoose one

A shell running with every bit of its mask set forks a child. How many lines mention fork?

7How do you turn a number into a name?

The output needs read, not 5. Where do the names come from, and how do you make sure name i really is system call i, even if someone later adds a number in the middle?

Check yourself

1solidChoose one

The names are written as a plain list, {"fork", "exit", "wait", "pipe", "read", "kill", ...}, in the order of syscall.h. What does trace 32 grep hello README print?

8Three harts, one console. Will the lines come out whole?

With three harts, two traced processes can return from system calls at the same moment, and a third process can be writing its own output. What keeps a trace line from being mixed with another trace line? Is anything keeping it from being mixed with a user program’s output?

Check yourself

1deepChoose all that apply

Which of these does pr.lock guarantee on three harts?

3. Build it

Start from the frozen commit, in your own clone of xv6 (not this site’s xv6/):

git checkout -b my-trace 06aad25

Work in this order; each milestone can be tested before the next.

  1. The plumbing. Add SYS_trace (23) to kernel/syscall.h, entry("trace") to user/usys.pl and int trace(int); to user/user.h. Run make. Nothing calls it yet, but user/usys.S should now contain a trace: stub. Check with grep -A3 'trace:' user/usys.S.
  2. The mask and the handler. Add int tracemask; to struct proc (in the private section), write sys_trace in kernel/sysproc.c, add its extern and table entry in kernel/syscall.c, and clear the field in freeproc. A quick test: a tiny program that calls trace(5) and prints trace(0) should print 5. Without the table entry you would see unknown sys call 23 instead.
  3. Inheritance. One line in kfork, before release(&np->lock).
  4. The print. The names table and the printk in syscall(). Now trace works from a test program: set a mask, make a call, see the line.
  5. The command. user/trace.c (check argc, trace(atoi(argv[1])), exec, print an error if exec returns) and $U/_trace in UPROGS. Try trace 32 grep hello README and trace 2147483647 echo hi.
  6. The test. tracetest (or your own), then usertests -q.

Debugging. Run QEMU with three harts (make qemu uses CPUS := 3). If a line is missing, ask in order: is the bit set (print the mask in sys_trace)? Did the call come through syscall() (a forked child’s first return does not)? Is the name table indexed like syscalls[]? For a crash inside the print, attach gdb (the GNU debugger) (make qemu-gdb in one terminal, ${TOOLPREFIX}gdb kernel/kernel and target remote in another), break panic, and look at bt, p/x $scause, p/x $stval and p/x $sepc: clinic 3 shows what that looks like. TOOLPREFIX is your RISC-V toolchain’s prefix, the same one xv6’s Makefile detects (riscv64-unknown-elf-, riscv64-linux-gnu- or riscv64-elf-); set it with export TOOLPREFIX=riscv64-unknown-elf- or whichever you have. On Debian/Ubuntu/WSL, gdb-multiarch also works as the debugger.

4. Debugging clinic

Each of these bugs was put into the reference solution on purpose and run on three harts. The symptom is exactly what happened. Try to explain it before revealing why.

1Forgot to copy the mask in kfork

The one line in kfork is missing:

   safestrcpy(np->name, p->name, sizeof(p->name));

-  // the child inherits the parent's trace mask.
-  np->tracemask = p->tracemask;
-
   pid = np->pid;

What happened when we ran it

$ tracetest
tracetest: trace returns the previous mask: OK
tracetest: fork copies the mask to the child: FAIL
tracetest: the child's trace(0) leaves the parent alone: OK
tracetest: exec keeps the mask: FAIL
tracetest: begin
3: syscall getpid -> 3
tracetest: end
tracetest: expected between the markers, and nothing else:
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: 2 FAILED
$ trace 65536 sh
$ 7: syscall write -> 2
echo hi
hi
$ 7: syscall write -> 2

2Printed before calling the handler

The check moved above the call in syscall():

-    p->trapframe->a0 = syscalls[num]();
     if (p->tracemask & (1 << num))
       printk("%d: syscall %s -> %ld\n", p->pid, syscallnames[num],
              p->trapframe->a0);
+    p->trapframe->a0 = syscalls[num]();

What happened when we ran it

$ trace 32 grep hello README
3: syscall read -> 3
3: syscall read -> 3
3: syscall read -> 3
3: syscall read -> 3
$ trace 2147483647 echo hi
4: syscall exec -> 16336
4: syscall write -> 1
hi4: syscall write -> 1

4: syscall exit -> 0
$

(a separate boot, same kernel:)
$ tracetest
tracetest: trace returns the previous mask: OK
tracetest: fork copies the mask to the child: OK
tracetest: the child's trace(0) leaves the parent alone: OK
tracetest: exec keeps the mask: OK
tracetest: begin
3: syscall getpid -> 0
6: syscall getpid -> 0
tracetest: end
tracetest: expected between the markers, and nothing else:
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: 4 checks OK; now compare the lines between the markers by eye

3Off-by-one in the names table

The names are a plain list in syscall.h order, without designated initializers:

 static char *syscallnames[] = {
   // clang-format off
-  [SYS_fork]    = "fork",
-  [SYS_exit]    = "exit",
+  "fork",
+  "exit",
   ...
-  [SYS_trace]   = "trace",
+  "trace",

What happened when we ran it

$ trace 32 grep hello README
3: syscall kill -> 1023
3: syscall kill -> 965
3: syscall kill -> 453
3: syscall kill -> 0
$ trace 2147483647 echo hi
4: syscall panic: acquire

(a second run, fresh boot, `trace 2147483647 echo hi` as the first command,
with gdb attached and a breakpoint on panic, on hart 1:)
#0  panic (s=s@entry=0x80007048 "acquire") at kernel/printk.c:139
#1  0x0000000080000c28 in acquire (lk=lk@entry=0x8000faa8 <pr>) at kernel/spinlock.c:26
#2  0x000000008000057a in printk (fmt=fmt@entry=0x80007350 "scause=0x%lx sepc=0x%lx stval=0x%lx\n") at kernel/printk.c:71
#3  0x0000000080002758 in kerneltrap () at kernel/trap.c:151
#4  0x0000000080005648 in kernelvec () at kernel/kernelvec.S:38
(gdb) p/x $scause
$1 = 0xd
(gdb) p/x $stval
$2 = 0x400000000
(gdb) p/x $sepc
$3 = 0x8000072c
(gdb) x/2wx 0x80007908+23*8
0x800079c0 <first.1>:	0x00000000	0x00000004

4Forgot the usys.pl entry

Everything else is in place (syscall.h, user.h, the kernel side), but user/usys.pl has no entry("trace");.

What happened when we ran it

${TOOLPREFIX}ld -z max-page-size=4096 -T user/user.ld -o user/_trace user/trace.o user/ulib.o user/usys.o user/printf.o user/umalloc.o
${TOOLPREFIX}ld: user/trace.o: in function `main':
./user/trace.c:12:(.text+0x3e): undefined reference to `trace'
make: *** [user/_trace] Error 1

5Declared the mask as a char

A learner saves space in struct proc:

-  int tracemask;               // bit i set: trace system call i
+  char tracemask;              // bit i set: trace system call i

Everything else is unchanged, including p->tracemask & (1 << num).

What happened when we ran it

$ tracetest
tracetest: trace returns the previous mask: FAIL
tracetest: fork copies the mask to the child: FAIL
tracetest: the child's trace(0) leaves the parent alone: FAIL
tracetest: exec keeps the mask: FAIL
tracetest: begin
tracetest: end
tracetest: expected between the markers, and nothing else:
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: 4 FAILED
$ trace 32 grep hello README
7: syscall read -> 1023
7: syscall read -> 965
7: syscall read -> 453
7: syscall read -> 0
$ trace 65536 echo hi
hi
$

6Forgot to add trace to UPROGS

The program is written, but the Makefile is unchanged:

   $U/_sync\
-  $U/_trace\
   $U/_tracetest\

What happened when we ran it

$ trace 32 grep hello README
exec trace failed
$ tracetest
tracetest: trace returns the previous mask: OK
tracetest: fork copies the mask to the child: OK
tracetest: the child's trace(0) leaves the parent alone: OK
tracetest: exec keeps the mask: OK
tracetest: begin
4: syscall getpid -> 4
7: syscall getpid -> 7
tracetest: end
tracetest: expected between the markers, and nothing else:
4: syscall getpid -> 4
7: syscall getpid -> 7
tracetest: 4 checks OK; now compare the lines between the markers by eye

5. The reference solution

Take the guided tour through the reference solution, one commit at a time, with the machine state at every step:

Open the reveal tour →

Or read the commits

  1. d3bb16a Add the trace system call number and user stub

    kernel/syscall.h

    @@ -20,4 +20,5 @@
    2020#define SYS_link 19
    2121#define SYS_mkdir 20
    2222#define SYS_close 21
    2323#define SYS_sync 22
    24#define SYS_trace 23

    user/user.h

    @@ -24,8 +24,9 @@ int getpid(void);
    2424char *sys_sbrk(int, int);
    2525int pause(int);
    2626int uptime(void);
    2727int sync(void);
    28int trace(int);
    2829
    2930// ulib.c
    3031int stat(const char *, struct stat *);
    3132char *strcpy(char *, const char *);

    user/usys.pl

    @@ -42,4 +42,5 @@ entry("getpid");
    4242entry("sbrk");
    4343entry("pause");
    4444entry("uptime");
    4545entry("sync");
    46entry("trace");
  2. 51c89f3 Add a trace mask to struct proc and sys_trace

    kernel/proc.c

    @@ -166,8 +166,9 @@ freeproc(struct proc *p)
    166166 p->name[0] = 0;
    167167 p->chan = 0;
    168168 p->killed = 0;
    169169 p->xstate = 0;
    170 p->tracemask = 0;
    170171 p->state = UNUSED;
    171172}
    172173
    173174// Create a user page table for a given process, with no user memory,

    kernel/proc.h

    @@ -100,5 +100,6 @@ struct proc {
    100100 struct context context; // swtch() here to run process
    101101 struct file *ofile[NOFILE]; // Open files
    102102 struct inode *cwd; // Current directory
    103103 char name[16]; // Process name (debugging)
    104 int tracemask; // bit i set: trace system call i
    104105};

    kernel/syscall.c

    @@ -102,8 +102,9 @@ extern uint64 sys_unlink(void);
    102102extern uint64 sys_link(void);
    103103extern uint64 sys_mkdir(void);
    104104extern uint64 sys_close(void);
    105105extern uint64 sys_sync(void);
    106extern uint64 sys_trace(void);
    106107
    107108// An array mapping syscall numbers from syscall.h
    108109// to the function that handles the system call.
    109110static uint64 (*syscalls[])(void) = {
    @@ -129,8 +130,9 @@ static uint64 (*syscalls[])(void) = {
    129130 [SYS_link] = sys_link,
    130131 [SYS_mkdir] = sys_mkdir,
    131132 [SYS_close] = sys_close,
    132133 [SYS_sync] = sys_sync,
    134 [SYS_trace] = sys_trace,
    133135 // clang-format on
    134136};
    135137
    136138void

    kernel/sysproc.c

    @@ -109,4 +109,18 @@ sys_uptime(void)
    109109 xticks = ticks;
    110110 release(&tickslock);
    111111 return xticks;
    112112}
    113
    114// set the calling process's trace mask: bit i traces
    115// system call number i. returns the previous mask.
    116uint64
    117sys_trace(void)
    118{
    119 int mask, old;
    120 struct proc *p = myproc();
    121
    122 argint(0, &mask);
    123 old = p->tracemask;
    124 p->tracemask = mask;
    125 return old;
    126}
  3. ee3b6ff Inherit the trace mask in kfork

    kernel/proc.c

    @@ -289,8 +289,11 @@ kfork(void)
    289289 np->cwd = idup(p->cwd);
    290290
    291291 safestrcpy(np->name, p->name, sizeof(p->name));
    292292
    293 // the child inherits the parent's trace mask.
    294 np->tracemask = p->tracemask;
    295
    293296 pid = np->pid;
    294297
    295298 release(&np->lock);
    296299
  4. 05641a3 Print traced system calls as they return

    kernel/syscall.c

    @@ -134,8 +134,37 @@ static uint64 (*syscalls[])(void) = {
    134134 [SYS_trace] = sys_trace,
    135135 // clang-format on
    136136};
    137137
    138// System call names, for trace output.
    139static char *syscallnames[] = {
    140 // clang-format off
    141 [SYS_fork] = "fork",
    142 [SYS_exit] = "exit",
    143 [SYS_wait] = "wait",
    144 [SYS_pipe] = "pipe",
    145 [SYS_read] = "read",
    146 [SYS_kill] = "kill",
    147 [SYS_exec] = "exec",
    148 [SYS_fstat] = "fstat",
    149 [SYS_chdir] = "chdir",
    150 [SYS_dup] = "dup",
    151 [SYS_getpid] = "getpid",
    152 [SYS_sbrk] = "sbrk",
    153 [SYS_pause] = "pause",
    154 [SYS_uptime] = "uptime",
    155 [SYS_open] = "open",
    156 [SYS_write] = "write",
    157 [SYS_mknod] = "mknod",
    158 [SYS_unlink] = "unlink",
    159 [SYS_link] = "link",
    160 [SYS_mkdir] = "mkdir",
    161 [SYS_close] = "close",
    162 [SYS_sync] = "sync",
    163 [SYS_trace] = "trace",
    164 // clang-format on
    165};
    166
    138167void
    139168syscall(void)
    140169{
    141170 int num;
    @@ -145,8 +174,11 @@ syscall(void)
    145174 if (num > 0 && num < NELEM(syscalls) && syscalls[num]) {
    146175 // Use num to lookup the system call function for num, call it,
    147176 // and store its return value in p->trapframe->a0
    148177 p->trapframe->a0 = syscalls[num]();
    178 if (p->tracemask & (1 << num))
    179 printk("%d: syscall %s -> %ld\n", p->pid, syscallnames[num],
    180 p->trapframe->a0);
    149181 } else {
    150182 printk("%d %s: unknown sys call %d\n", p->pid, p->name, num);
    151183 p->trapframe->a0 = -1;
    152184 }
  5. 5419df2 Add the trace user program

    Makefile

    @@ -149,8 +149,9 @@ UPROGS=\
    149149 $U/_logstress\
    150150 $U/_forphan\
    151151 $U/_dorphan\
    152152 $U/_sync\
    153 $U/_trace\
    153154
    154155fs.img: mkfs/mkfs README $(UPROGS)
    155156 mkfs/mkfs fs.img README $(UPROGS)
    156157

    user/trace.c

    @@ -0,0 +1,20 @@
    1#include "kernel/types.h"
    2#include "kernel/stat.h"
    3#include "user/user.h"
    4
    5// trace MASK command [args...]
    6// run command with system-call tracing set to MASK.
    7int
    8main(int argc, char *argv[])
    9{
    10 if (argc < 3 || argv[1][0] < '0' || argv[1][0] > '9') {
    11 fprintf(2, "usage: trace mask command [args...]\n");
    12 exit(1);
    13 }
    14
    15 trace(atoi(argv[1]));
    16
    17 exec(argv[2], &argv[2]);
    18 fprintf(2, "trace: exec %s failed\n", argv[2]);
    19 exit(1);
    20}
  6. 54113c1 Add tracetest

    Makefile

    @@ -150,8 +150,9 @@ UPROGS=\
    150150 $U/_forphan\
    151151 $U/_dorphan\
    152152 $U/_sync\
    153153 $U/_trace\
    154 $U/_tracetest\
    154155
    155156fs.img: mkfs/mkfs README $(UPROGS)
    156157 mkfs/mkfs fs.img README $(UPROGS)
    157158

    user/tracetest.c

    @@ -0,0 +1,97 @@
    1// Tests for the trace system call.
    2
    3#include "kernel/types.h"
    4#include "kernel/stat.h"
    5#include "kernel/syscall.h"
    6#include "user/user.h"
    7
    8#define MASK (1 << SYS_uptime)
    9
    10int fails;
    11
    12void
    13check(int ok, char *what)
    14{
    15 printf("tracetest: %s: %s\n", what, ok ? "OK" : "FAIL");
    16 if (!ok)
    17 fails++;
    18}
    19
    20// fork a child that runs f() and exits with its result;
    21// return the child's exit status.
    22int
    23inchild(int (*f)(void))
    24{
    25 int pid, status;
    26
    27 pid = fork();
    28 if (pid < 0) {
    29 printf("tracetest: fork failed\n");
    30 exit(1);
    31 }
    32 if (pid == 0)
    33 exit(f());
    34 if (wait(&status) != pid)
    35 return -1;
    36 return status;
    37}
    38
    39// child: report whether the mask arrived, then clear it.
    40int
    41childmask(void)
    42{
    43 return trace(0) == MASK ? 0 : 1;
    44}
    45
    46// child: exec tracetest again, which checks its own mask.
    47int
    48execmask(void)
    49{
    50 char *argv[] = {"tracetest", "exec", 0};
    51 exec("/tracetest", argv);
    52 return 2;
    53}
    54
    55int
    56main(int argc, char *argv[])
    57{
    58 int pid, child;
    59
    60 // run by execmask(): did the mask survive exec?
    61 if (argc == 2 && strcmp(argv[1], "exec") == 0)
    62 exit(trace(0) == MASK ? 0 : 1);
    63
    64 trace(0);
    65 trace(MASK);
    66 check(trace(MASK) == MASK, "trace returns the previous mask");
    67 check(inchild(childmask) == 0, "fork copies the mask to the child");
    68 check(trace(MASK) == MASK, "the child's trace(0) leaves the parent alone");
    69 check(inchild(execmask) == 0, "exec keeps the mask");
    70 trace(0);
    71
    72 // only getpid is traced between the markers: the parent's and
    73 // the child's getpid print one line each, nothing else prints.
    74 pid = getpid();
    75 printf("tracetest: begin\n");
    76 trace(1 << SYS_getpid);
    77 getpid();
    78 uptime();
    79 child = fork();
    80 if (child == 0) {
    81 getpid();
    82 exit(0);
    83 }
    84 wait(0);
    85 trace(0);
    86 printf("tracetest: end\n");
    87 printf("tracetest: expected between the markers, and nothing else:\n");
    88 printf("%d: syscall getpid -> %d\n", pid, pid);
    89 printf("%d: syscall getpid -> %d\n", child, child);
    90
    91 if (fails == 0)
    92 printf("tracetest: 4 checks OK; now compare the lines between the "
    93 "markers by eye\n");
    94 else
    95 printf("tracetest: %d FAILED\n", fails);
    96 exit(fails != 0);
    97}

6. Verify and measure

On the branch, on three harts (QEMU -smp 3), tracetest passes:

$ tracetest
tracetest: trace returns the previous mask: OK
tracetest: fork copies the mask to the child: OK
tracetest: the child's trace(0) leaves the parent alone: OK
tracetest: exec keeps the mask: OK
tracetest: begin
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: end
tracetest: expected between the markers, and nothing else:
3: syscall getpid -> 3
6: syscall getpid -> 6
tracetest: 4 checks OK; now compare the lines between the markers by eye

Compared by eye, the two traced lines are exactly the expected ones, and the untraced uptime, fork and wait calls in that window printed nothing. And the rest of the kernel is unharmed:

$ usertests -q
usertests starting
...
test lazy_sbrk: OK
test partial_write: OK
test unlinkcwd: OK
ALL TESTS PASSED

(tracetest run again after usertests passed too, with pids 6653 and 6656.) No process in usertests sets a mask, so the only cost to an untraced process is a few instructions per system call in syscall (load the mask, shift, test).

How many system calls does ls make? trace 2147483647 ls in / (26 entries) printed 808 lines: 1 trace (by the trace program, before the exec), 1 exec, then ls itself made 65 reads (64 directory entries of 16 bytes and one end-of-file read), 27 opens, 27 fstats, 27 closes (one set for . and one per entry, through stat) and 660 writes of 1 byte each. User-space printf calls write once per character (user/printf.c:12), so more than four out of five of the system calls ls makes print one character, each paying for a full trap, two satp switches and a return.

What does the shell do for one command? trace 2147483647 sh, then echo hi, then Ctrl-D: 21 lines in all.

3: syscall trace -> 0
3: syscall exec -> 1
3: syscall open -> 3
3: syscall close -> 0
$ 3: syscall write -> 2
echo hi
3: syscall read -> 1          (eight of these: "echo hi\n", one byte per read)
3: syscall fork -> 4
4: syscall sbrk -> 20480
4: syscall exec -> 2
hi4: syscall write -> 2

4: syscall write -> 1
3: syscall wait -> 4
$ 3: syscall write -> 2
3: syscall read -> 0

For one command, the shell makes 11 calls (8 one-byte reads in gets, fork, wait, and the prompt’s write), and the child makes 4 visible ones: sbrk, because the child parses the command and malloc grows the heap; exec; and echo’s two writes. The open/close pair at the start is the shell making sure descriptors 0 to 2 are open (user/sh.c:152). Neither exit prints, and neither does the child’s return from fork.

Interleaving on three harts. cat README &; trace 65536 echo hello printed, in the middle of the README, Marcelo helloArroyo, Hirb3: syscall od Bwehritena -> m, S5il and, a line later, carlc / 3: syscall write ->lo 1 / ne: the kernel’s trace lines and cat’s text alternated byte by byte, because printk and uartwrite do not share a lock.

7. Go further