Top Header

How strace Works: A Deep Dive Using attace

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:


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:

Conceptually, strace works like this:

  1. Create a child process
  2. Ask the kernel to allow tracing
  3. Execute the target program
  4. Stop execution on each syscall
  5. 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?

  1. fork() – creates two processes: child (to be traced) and parent (tracer).
  2. PTRACE_TRACEME – child requests to be traced by the parent.
  3. 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:

These steps form the real core of strace and will be explored in more detail in future posts.


Conclusion

In summary:

If you want to try attace yourself, you can clone it from my GitHub repository.

Further reading: