Build Your Own Strace
This challenge is to build your own version of Linux tool strace. It’s a useful tool to debug programs that you don’t have the source code for.
It can be used to monitor interactions between processes and the Linux kernel, which include system calls, signal deliveries, and changes of process state.
So What Are System Calls?
A system call (often abbreviated to syscall) is a call made by a program to request a service from the operating system. Such request might be for hardware related access, i.e. to a camera, network or audio device; creation of new processes; file management; or allocation, access and release of memory.
The Challenge - Building Your Own Strace
Strace was originally written for SunOS before being ported to Linux.
Step Zero
Step Zero of every coding challenge is setting up your development and test environment. As strace is a Linux tool that relies on functionality in the Linux kernel, much like the build your own Docker coding challenge we’re going to need to run a Linux VM if you’re not working on a Linux machine.
For Windows and Intel based Apple hardware VirtualBox is a great option. For Apple M based hardware VMware Fusion is free for personal use, QEMU is another option, though I’ve not tried it.
In case you’re wondering why you need a Linux virtual machine on Mac and Windows when Docker and Kubernetes don’t, well, it’s because they do. It’s just packaged up in the application so you don’t have to worry about it.
Step 1
In this step your goal is to run strace against an existing command, here’s an abbreviated output from strace ls
. The full output is quite long, so I’ve snipped it!
$ strace ls
execve("/usr/bin/ls", ["ls"], 0xffffe735d390 /* 25 vars */) = 0
brk(NULL) = 0xaaaac8a4b000
faccessat(AT_FDCWD, "/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=28262, ...}) = 0
mmap(NULL, 28262, PROT_READ, MAP_PRIVATE, 3, 0) = 0xffff8e296000
close(3) = 0
openat(AT_FDCWD, "/lib/aarch64-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0\0\0\0\0\0\0\0\0\3\0\267\0\1\0\0\0 e\0\0\0\0\0\0"..., 832) = 832
fstat(3, {st_mode=S_IFREG|0644, st_size=154872, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xffff8e294000
mmap(NULL, 227800, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xffff8e236000
<snipped by John> = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
write(1, "README.md Vagrantfile\tgo.mod\tma"..., 38README.md Vagrantfile go.mod main.go
) = 38
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
Here we can see that after a number of operations it made a system call to a function write:
write(1, "README.md Vagrantfile\tgo.mod\tma"..., 38README.md Vagrantfile go.mod main.go
) = 38
Which is writing to the console the actual output of ls
:
README.md Vagrantfile go.mod main.go
Which you can see if I run ls
on it’s own:
$ ls
README.md Vagrantfile go.mod main.go
If you want to dig into the rest of the system calls you can run the command: man syscalls
or refer to the online list of system calls to see what the other syscalls in the strace
output do. I’d urge you do so, you’ll gain a greater understanding of how the software you use is working under the hood.
Step 2
In this step your goal is to create your own strace program that is able to launch another executable. You will be doing this using the fork and exec calls. Depending on the programming language you use there might be some abstraction over this, for example Python provides abstractions in the subprocess module, Go in os/exec, and Rust has std::process.
Step 3
In this step your goal is to attach ptrace
to the executable you’ve launched.
The ptrace() system call provides a means by which one process
(the "tracer") may observe and control the execution of another
process (the "tracee"), and examine and change the tracee's memory
and registers. It is primarily used to implement breakpoint
debugging and system call tracing.
You’ll then need to look into using PTRACE_GETREGS
and PTRACE_SYSCALL
to get the details of registers at each system call. You’ll need to read about wait too.
Step 4
In this step your goal is to make sense of the data and translate it into the name of the system call, and details of the arguments passed to it. The Linux kernel syscall table will be a useful resource for this.
Step 5
In this step your goal is to support the -c
flag so your strace
is able to create an output like this:
$ strace -c ls
README.md Vagrantfile go.mod main.go
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
25.39 0.002233 93 24 openat
20.30 0.001786 1786 1 execve
13.52 0.001189 132 9 read
12.28 0.001080 36 30 mmap
10.89 0.000958 479 2 2 statfs
10.58 0.000931 77 12 mprotect
2.46 0.000216 8 25 fstat
2.15 0.000189 7 26 close
0.68 0.000060 30 2 getdents64
0.43 0.000038 19 2 2 faccessat
0.28 0.000025 8 3 brk
0.28 0.000025 25 1 munmap
0.16 0.000014 7 2 rt_sigaction
0.13 0.000011 11 1 write
0.13 0.000011 11 1 futex
0.11 0.000010 10 1 set_tid_address
0.10 0.000009 9 1 rt_sigprocmask
0.09 0.000008 8 1 set_robust_list
0.03 0.000003 3 1 prlimit64
0.00 0.000000 0 2 ioctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.008796 147 4 total
Help Others by Sharing Your Solutions!
If you think your solution is an example other developers can learn from please share it, put it on GitHub, GitLab or elsewhere. Then let me know - ping me a message on the Discord Server, via Twitter or LinkedIn or just post about it there and tag me. Alternately please add a link to it in the Coding Challenges Shared Solutions Github repo.
Get The Challenges By Email
If you would like to receive the coding challenges by email, you can subscribe to the weekly newsletter on SubStack here: