ptrace and Debugging Internals

2026-04-24

Every time you set a breakpoint in GDB, you're relying on ptrace — the single system call that makes userspace debugging possible on Linux. Understanding how it works reveals the machinery beneath every debugger, tracer, and sandbox you use.

The ptrace system call has one signature: long ptrace(enum request, pid_t pid, void *addr, void *data). The request parameter selects the operation. The critical ones:

How software breakpoints actually work: GDB doesn't use magic. On x86, it reads the instruction at the target address with PTRACE_PEEKTEXT, saves the original byte, then overwrites the first byte with 0xCC (the INT 3 instruction) using PTRACE_POKETEXT. When the CPU hits 0xCC, it raises a SIGTRAP. The kernel stops the tracee and notifies the tracer via waitpid(). To resume, GDB restores the original byte, rewinds RIP by one (since INT 3 is a single byte), single-steps past the original instruction, re-inserts 0xCC, and continues. This is why breakpoints on x86 cost essentially nothing until hit — the CPU runs at full speed between them.

Real-world example: strace uses PTRACE_SYSCALL to intercept every system call. Each syscall causes two stops — entry and exit — so strace can capture both arguments and return values. This means a traced process stops twice per syscall. If your program makes 50,000 syscalls per second, that's 100,000 context switches added by strace. Rule of thumb: strace slows a syscall-heavy process by roughly 100-300x. Use it for debugging, never in production.

Security implications: The kernel enforces ptrace permissions through the ptrace_access_check function. By default, a process can only trace its own children. The /proc/sys/kernel/yama/ptrace_scope sysctl controls this: 0 means any process can trace any other (classic behavior), 1 restricts to parent-child (Ubuntu default), 2 limits to CAP_SYS_PTRACE holders, and 3 disables ptrace entirely. This is why on a hardened system, attaching GDB to an already-running process requires sudo.

On ARM (AArch64): The breakpoint mechanism differs. Instead of patching instructions, you can use hardware breakpoint registers (DBGBCR/DBGBVR). ARM provides up to 16 hardware breakpoints. ptrace exposes these via PTRACE_SETHBPREGS (ARM32) or the NT_ARM_HW_BREAK regset. Hardware breakpoints don't modify code, making them essential for debugging read-only memory or flash.

See it in action: Check out EnthusiastiCon 2019 – ptrace: The Sherlock Holmes of syscalls! by EnthusiastiCon to see this theory applied.
Key Takeaway: ptrace is the universal mechanism behind Linux debuggers — it works by stopping a tracee on events (breakpoints, syscalls, signals), letting the tracer inspect and modify registers and memory one word at a time, with software breakpoints implemented by patching a single INT 3 byte into the instruction stream.

All newsletters