---
title: ptrace and Debugging Interfaces
url: https://doc.liz6.com/en/systems-programming/04-signals-and-exceptions/02-ptrace-and-debugging-interfaces
locale: en
area: systems-programming
tags:
- systems-programming
- signals-and-exceptions
date: 2026-06-30
modified: 2026-07-16
description: 'Coverage: ptrace(PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA/PTRACE_GETREGS) → strace/gdb implementation principles → /proc/pid/mem → seccomp user notify Applicable to: Linux user space, x86-64/ARM64'
---

# ptrace and Debugging Interfaces

> Coverage: ptrace(PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA/PTRACE_GETREGS) → strace/gdb implementation principles → /proc/pid/mem → seccomp user notify
> Applicable to: Linux user space, x86-64/ARM64

## Overview

ptrace is the oldest debugging interface in Unix—it allows a tracer process to fully control a tracee process. gdb (breakpoints, single-stepping, register read/write) and strace (system call tracing) are both built on ptrace. In recent years, more efficient alternatives have emerged (/proc/pid/mem, seccomp user notify), but ptrace remains the most universal.

## Core ptrace Operations

```c
#include <sys/ptrace.h>

// Attach (self or others):
ptrace(PTRACE_TRACEME, 0, 0, 0);            // tracee declares "can be traced"
// or
ptrace(PTRACE_ATTACH, target_pid, 0, 0);    // tracer attaches to tracee
ptrace(PTRACE_SEIZE, target_pid, 0, opts);  // attach but do not stop (SEIZE, 3.4+)

// Control:
ptrace(PTRACE_CONT, pid, 0, sig);           // continue execution (optional injected signal)
ptrace(PTRACE_SYSCALL, pid, 0, 0);          // stop at syscall entry + exit
ptrace(PTRACE_SINGLESTEP, pid, 0, 0);       // single-step one instruction

// Read/Write:
ptrace(PTRACE_PEEKDATA, pid, addr, 0);      // read tracee memory (word at a time)
ptrace(PTRACE_POKEDATA, pid, addr, data);   // write tracee memory
ptrace(PTRACE_GETREGS, pid, 0, &regs);      // read registers
ptrace(PTRACE_SETREGS, pid, 0, &regs);      // write registers

// Detach:
ptrace(PTRACE_DETACH, pid, 0, 0);
```

## strace Principles

```c
// Core logic of strace (simplified):

// 1. Fork child process
pid = fork();
if (pid == 0) {
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    execve(argv[1], &argv[1], envp);
}

// 2. Parent process (tracer):
waitpid(pid, &status, 0);  // wait for first stop (SIGTRAP after execve)

while (1) {
    ptrace(PTRACE_SYSCALL, pid, 0, 0);   // tell kernel: stop on syscall
    waitpid(pid, &status, 0);             // wait for stop

    if (WIFEXITED(status)) break;

    // tracee stopped at syscall entry → read arguments
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    // x86: regs.orig_rax = syscall number, regs.rdi/rsi/rdx = args 0-2

    // Continue to syscall exit:
    ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, &status, 0);
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    // regs.rax = return value (possibly -errno)
}
```

Every syscall entry/exit goes through: tracer → PTRACE_SYSCALL → waitpid → GETREGS → PTRACE_SYSCALL → waitpid → GETREGS. This is the root cause of strace's high overhead—each traced system call requires 4 context switches (tracee→tracer→tracee→tracer).

## gdb Principles

```
Breakpoints:
  gdb uses PTRACE_PEEKDATA to read the instruction at the breakpoint location → saves it
  Uses PTRACE_POKEDATA to replace that location with INT3 (0xCC)
  Execution reaches INT3 → SIGTRAP → tracer is notified (waitpid)
  Tracer restores original instruction → single-step or continue

Single-stepping:
  PTRACE_SINGLESTEP → CPU executes 1 instruction → SIGTRAP → tracer

Register read/write:
  PTRACE_GETREGS / PTRACE_SETREGS

Memory read/write:
  PTRACE_PEEKDATA / PTRACE_POKEDATA (slow, per-word)
  /proc/pid/mem: more efficient (pread/pwrite, arbitrary size)
```

## /proc/pid/mem: An Alternative to ptrace

```c
// No need for ptrace ATTACH → read/write tracee's virtual memory
// But ptrace ATTACH is required to write to non-rw regions (before 5.x)
// 5.x+: process_vm_readv/writev is more efficient (no need to open /proc/pid/mem)

int memfd = open("/proc/<pid>/mem", O_RDWR);
pread(memfd, buf, size, addr);  // read tracee address addr
pwrite(memfd, buf, size, addr); // write tracee address addr
```

## seccomp user notify: "ptrace for the New Era"

```c
// SECCOMP_RET_USER_NOTIF: seccomp filter notifies the supervisor process of syscalls
// Supervisor can check/modify/approve/reject → no need for ptrace (much lower overhead)

// Problems with ptrace:
//   All syscalls stop → tracer → tracee (even for syscalls not of interest)
// seccomp user notify:
//   Only intercepts syscalls matching the seccomp filter → others are unaffected
//   → ptrace overhead is O(all syscalls), seccomp is O(interested ones)
```

## Debugger Limitations

```
1. A process can only have 1 tracer (multiple straces are not allowed)
2. Processes after PTRACE_TRACEME cannot setuid (security restriction)
3. Cannot trace init (PID 1)
4. Yama LSM: /proc/sys/kernel/yama/ptrace_scope restricts who can trace
   0: Anyone can trace anyone (permissive)
   1: Can only trace self or descendants (default)
   2: Can only trace descendants (admin override)
   3: Completely disable ptrace
```

## References

- **man**: ptrace(2)
- **Source code**: `kernel/ptrace.c`, `kernel/seccomp.c`
- **LWN**: "Better ptrace", "Seccomp user-space notification"

*Keywords: ptrace, PTRACE_ATTACH, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_PEEKDATA, strace, gdb, /proc/pid/mem, seccomp user notify, Yama*
