December 20, 2025
This article explores how strace works on Linux by diving into the mechanisms it relies on inside the kernel. Instead of explaining strace only theoretically, we’ll also introduce a minimal strace-like tool called attace, written by me, to demonstrate the core ideas in a simpler and practical way.
What is strace?
According to the man page:
strace is a diagnostic, debugging, and instructional userspace utility for Linux that intercepts and records system calls and signals made by a process.
In simpler terms, strace allows you to observe the boundary between user space and kernel space by showing which system calls a process invokes and how the kernel responds to them.
Example:
mateo@truthTerminal:~$ strace ls
execve("/usr/bin/ls", ["ls"], 0x7ffe58d65ec0 /* 62 vars */) = 0
brk(NULL) = 0x55ccc2b50000
mmap(NULL, 8192, PROT_READ|PROT_WRITE,
MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2d82bee000
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\0...", 832) = 832
close(3) = 0
This information is extremely useful for:
- Debugging
- Reverse engineering
- Understanding program behavior
- Learning how user-space programs interact with the kernel
How is this possible?
A normal user-space process cannot see inside another process. The answer is: ptrace.
ptrace: the foundation of strace
On Linux, syscall tracing is made possible by the ptrace system call:
long ptrace(enum __ptrace_request request,
pid_t pid,
void *addr,
void *data);
ptrace allows one process (the tracer) to:
- Observe another process (the tracee)
- Stop it at specific execution points
- Read or modify its registers and memory
- Intercept system calls before and after they are executed
Conceptually, strace works like this:
- Create a child process
- Ask the kernel to allow tracing
- Execute the target program
- Stop execution on each syscall
- Inspect registers to decode syscall number and arguments
A minimal strace-like example (attace)
/* Simple ptrace example */
#include <stdio.h>
#include <unistd.h>
#include <sys/ptrace.h>
int main(int argc, char *argv[])
{
if (argc < 2)
{
perror("usage: ./attace <program>");
_exit(1);
}
int pid = fork();
if (pid == 0)
{
/* Child process: ask to be traced */
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
/* Replace child with target program */
execvp(argv[1], &argv[1]);
}
else
{
/* Parent process: tracer */
/* Tracing logic will be implemented here */
}
return 0;
}
What is happening here?
- fork() – creates two processes: child (to be traced) and parent (tracer).
- PTRACE_TRACEME – child requests to be traced by the parent.
- execvp() – replaces the child process with the target program, which is still traceable.
What’s missing?
To build a real strace-like tool, the parent process must:
- Wait for the child using
waitpid() - Use
PTRACE_SYSCALLto stop execution before and after each syscall - Read CPU registers to identify syscall number, arguments, and return value
These steps form the real core of strace and will be explored in more detail in future posts.
Conclusion
In summary:
- strace works by intercepting syscalls using ptrace
- ptrace provides fine-grained control over process execution
- A minimal tracer can be implemented with surprisingly little code
- The complexity of strace lies in decoding syscalls, handling different architectures, and formatting output
If you want to try attace yourself, you can clone it from my GitHub repository.
Further reading: