Fundamentals 15 min read

How Does strace Peek Inside Other Processes? A Hands‑On Implementation Walkthrough

This article explains the inner workings of the classic strace command by building a minimal tracer in C, detailing how ptrace attaches to a target process, sets up syscall tracing, waits for signals, reads the ORIG_RAX register, and prints system call names, while also exposing the relevant Linux kernel source.

Liangxu Linux
Liangxu Linux
Liangxu Linux
How Does strace Peek Inside Other Processes? A Hands‑On Implementation Walkthrough

Overview

The strace utility observes system calls made by a target process by using the Linux ptrace API. It attaches to the target, requests notification on each system call, waits for the kernel‑generated SIGTRAP, reads the syscall number from the ORIG_RAX register, translates it to a name, and repeats.

Minimal strace implementation

A compact C program demonstrates the essential workflow. The full source is available at

https://github.com/yanfeizhang/coder-kung-fu/blob/main/tests/cpu/test11/main.c

:

int main(int argc, char *argv[]) {
    pid_t pid = /* target pid */;
    int status;
    // 1. Attach to the target
    ptrace(PTRACE_ATTACH, pid, NULL, NULL);
    waitpid(pid, &status, 0);
    while (1) {
        // 2. Request syscall stop
        ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
        // 3. Wait for SIGTRAP generated at syscall entry
        waitpid(pid, &status, 0);
        // 4. Read syscall number from ORIG_RAX
        long sc = ptrace(PTRACE_PEEKUSER, pid, 8 * ORIG_RAX, NULL);
        const char *name;
        switch (sc) {
            case 5:  name = "read";  break;
            case 6:  name = "write"; break;
            case 10: name = "open"; break;
            case 11: name = "close"; break;
            default: name = "unknown"; break;
        }
        printf("Syscall: %s (number: %ld)
", name, sc);
    }
}

Attaching to the target process

The attachment is performed with ptrace(PTRACE_ATTACH, pid, …). Inside the kernel the ptrace system call resolves the task_struct for the given PID and calls ptrace_attach:

// kernel/ptrace.c (simplified)
SYSCALL_DEFINE4(ptrace, long request, long pid, unsigned long addr, ...){
    struct task_struct *child = find_get_task_by_vpid(pid);
    if (request == PTRACE_ATTACH || request == PTRACE_SEIZE)
        ret = ptrace_attach(child, request, addr, data);
    ...
}

static int ptrace_attach(struct task_struct *task, long request, ...){
    if (unlikely(task->flags & PF_KTHREAD))
        return -EPERM;               // kernel threads cannot be traced
    ptrace_link(task, current);      // link tracer and tracee
    ...
}
ptrace_link

inserts the child into the tracer’s ptraced list and sets child->parent = current, enabling the tracer to receive signals from the tracee via waitpid.

Enabling syscall tracing

After attachment the tracer issues ptrace(PTRACE_SYSCALL, pid, …). The kernel marks the task with SYSCALL_TRACE:

// kernel/ptrace.c (excerpt)
case PTRACE_SYSCALL:
    set_task_syscall_work(child, SYSCALL_TRACE);
    break;

// include/linux/thread_info.h
#define set_task_syscall_work(t, fl) \
    set_bit(SYSCALL_WORK_BIT_##fl, &task_thread_info(t)->syscall_work)

When the tracee executes a system call, the entry point checks the flag and, if set, calls ptrace_report_syscall_entry, which ultimately triggers a SIGTRAP to the tracer.

Waiting for the signal

The tracer blocks with waitpid(pid, &status, 0). The kernel implementation adds the tracer to a wait queue and puts it to TASK_INTERRUPTIBLE until the tracee generates SIGTRAP:

// kernel/exit.c (simplified)
SYSCALL_DEFINE3(waitpid, pid_t pid, int __user *stat_addr, int options){
    return kernel_wait4(pid, stat_addr, options, NULL);
}

static long do_wait(struct wait_opts *wo){
    init_waitqueue_func_entry(&wo->child_wait, child_wait_callback);
    wo->child_wait.private = current;
    add_wait_queue(¤t->signal->wait_chldexit, &wo->child_wait);
    ...
}

Reading the syscall number

When SIGTRAP is received, the tracer reads the ORIG_RAX register (the saved syscall number) via PTRACE_PEEKUSER:

// arch/x86/kernel/ptrace.c (excerpt)
case PTRACE_PEEKUSR:
    if (addr < sizeof(struct user_regs_struct))
        tmp = getreg(child, addr);
    ret = put_user(tmp, datap);
    break;

The value is then mapped to a human‑readable name (e.g., 5 → read) and printed.

Loop and continuation

After printing, the tracer repeats the PTRACE_SYSCALL → waitpid → read cycle, allowing continuous monitoring of the target’s system calls.

Performance considerations

Because the tracee is stopped at every syscall entry (and exit, if PTRACE_SYSCALL is used for both), strace introduces additional context switches and can noticeably increase the execution time of the traced program. It is therefore suited for debugging and short‑term analysis, but not for long‑running production monitoring.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

CSystem Callstraceptrace
Liangxu Linux
Written by

Liangxu Linux

Liangxu, a self‑taught IT professional now working as a Linux development engineer at a Fortune 500 multinational, shares extensive Linux knowledge—fundamentals, applications, tools, plus Git, databases, Raspberry Pi, etc. (Reply “Linux” to receive essential resources.)

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.