How strace Traces System Calls in Linux

The strace utility is a diagnostic and debugging tool in Linux used to monitor and capture the interactions between user-space applications and the Linux kernel. It operates primarily by leveraging the kernel's ptrace (process trace) system call, which allows strace to pause an application, inspect its CPU registers and memory, decode the arguments and return values of executed system calls, and then resume execution. This article explains the internal mechanisms strace uses to attach to processes, intercept kernel transitions, decode syscall data, and display runtime execution.

Initialization and Attachment via ptrace

When you launch a program using strace or attach it to a running process (using the -p flag), strace establishes a parent-child or tracer-tracee relationship using the ptrace system call.

The Interception Cycle

Once tracing is established, strace configures the kernel to stop the tracee every time a system call is made. This is accomplished using the PTRACE_SYSCALL request within an event loop managed via the waitpid system call.

Tracing a single system call involves two interception points:

  1. Syscall-Enter-Stop: When the target process triggers a software interrupt, a syscall instruction (on x86_64), or a sysenter instruction to transition from user mode to kernel mode, the kernel intercepts the action. Instead of immediately running the system call, the kernel pauses the thread and sends a SIGTRAP signal (specifically with PTRACE_O_TRACESYSGOOD set) to notify strace.
  2. Syscall-Exit-Stop: After strace inspects the initial call, it allows the kernel to execute the system call. Once the kernel finishes servicing the request and before returning control to user space, it pauses the process again. This second stop enables strace to capture the return value or error code.

Reading System Call Arguments

When the tracee is paused at the syscall-enter stage, strace retrieves the state of the target CPU registers using ptrace(PTRACE_GETREGSET, ...) or ptrace(PTRACE_GETREGS, ...).

The registers hold the system call number and its arguments according to the architecture's Application Binary Interface (ABI). On an x86_64 architecture:

strace maintains an internal lookup table that maps system call numbers to human-readable names and signatures. If an argument is a primitive value (like a file descriptor or flag mask), strace reads it directly from the register.

If an argument is a memory pointer (such as a string pointer for a file path or a buffer pointer), strace must read the tracee's virtual memory space. It does this either through the process_vm_readv system call or via repeated ptrace(PTRACE_PEEKDATA, ...) operations, decoding structures and null-terminated strings into readable text.

Capturing Return Values

After reading the arguments, strace issues ptrace(PTRACE_SYSCALL, ...) to resume execution. The kernel performs the actual operation and places the result into the designated return register (rax on x86_64).

At the syscall-exit-stop, strace reads the register state once more:

Performance Overhead

Because strace requires two context switches between user space and kernel space—and between the tracee and tracer—for every individual system call, running a program under strace introduces substantial execution overhead. This overhead makes strace well suited for debugging and troubleshooting, but typically unsuitable for high-throughput production environments.