本页目录

ptrace 与调试接口

覆盖: ptrace(PTRACE_ATTACH/PTRACE_SYSCALL/PTRACE_PEEKDATA/PTRACE_GETREGS) → strace/gdb 实现原理 → /proc/pid/mem → seccomp user notify 适用: Linux 用户态, x86-64/ARM64

概述

ptrace 是 Unix 最古老的调试接口——它允许一个 tracer 进程完全控制 tracee 进程。gdb(断点、单步、寄存器读写)、strace(系统调用追踪)都是基于 ptrace 构建的。近年来出现了更高效的替代方案(/proc/pid/mem、seccomp user notify),但 ptrace 仍然是最通用的。

ptrace 核心操作

#include <sys/ptrace.h>

// 附加 (自己或别人):
ptrace(PTRACE_TRACEME, 0, 0, 0);            // tracee 声明"可以被 trace"
//ptrace(PTRACE_ATTACH, target_pid, 0, 0);    // tracer 附加到 tracee
ptrace(PTRACE_SEIZE, target_pid, 0, opts);  // 附加但不停 (SEIZE, 3.4+)

// 控制:
ptrace(PTRACE_CONT, pid, 0, sig);           // 继续执行 (可选注信号)
ptrace(PTRACE_SYSCALL, pid, 0, 0);          // syscall entry + exit 都停
ptrace(PTRACE_SINGLESTEP, pid, 0, 0);       // 单步执行一条指令

// 读写:
ptrace(PTRACE_PEEKDATA, pid, addr, 0);      // 读 tracee 内存 (word at a time)
ptrace(PTRACE_POKEDATA, pid, addr, data);   // 写 tracee 内存
ptrace(PTRACE_GETREGS, pid, 0, &regs);      // 读寄存器
ptrace(PTRACE_SETREGS, pid, 0, &regs);      // 写寄存器

// 分离:
ptrace(PTRACE_DETACH, pid, 0, 0);

strace 原理

// strace 的核心逻辑 (简化):

// 1. fork 子进程
pid = fork();
if (pid == 0) {
    ptrace(PTRACE_TRACEME, 0, 0, 0);
    execve(argv[1], &argv[1], envp);
}

// 2. 父进程 (tracer):
waitpid(pid, &status, 0);  // 等第一个 stop (execve 之后的 SIGTRAP)

while (1) {
    ptrace(PTRACE_SYSCALL, pid, 0, 0);   // 告诉内核: syscall 时停
    waitpid(pid, &status, 0);             // 等 stop

    if (WIFEXITED(status)) break;

    // tracee 在 syscall entry 停了 → 读参数
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    // x86: regs.orig_rax = syscall nr, regs.rdi/rsi/rdx = args 0-2

    // 继续到 syscall exit:
    ptrace(PTRACE_SYSCALL, pid, 0, 0);
    waitpid(pid, &status, 0);
    ptrace(PTRACE_GETREGS, pid, 0, &regs);
    // regs.rax = 返回值 (可能是 -errno)
}

每次 syscall entry/exit 都经过: tracer → PTRACE_SYSCALL → waitpid → GETREGS → PTRACE_SYSCALL → waitpid → GETREGS。这就是 strace 开销大的根源——每个被追踪的系统调用要 4 次上下文切换(tracee→tracer→tracee→tracer)。

gdb 原理

断点:
  gdb 用 PTRACE_PEEKDATA 读断点位置的指令 → 保存
  用 PTRACE_POKEDATA 把该位置替换为 INT3 (0xCC)
  执行到 INT3 → SIGTRAP → tracer 被通知 (waitpid)
  tracer 恢复原始指令 → 单步或继续

单步:
  PTRACE_SINGLESTEP → CPU 执行 1 条指令 → SIGTRAP → tracer

寄存器读写:
  PTRACE_GETREGS / PTRACE_SETREGS

内存读写:
  PTRACE_PEEKDATA / PTRACE_POKEDATA (慢, per-word)
  /proc/pid/mem: 更高效 (pread/pwrite, 任意大小)

/proc/pid/mem: ptrace 的替代

// 不需要 ptrace ATTACH → 读/写 tracee 的虚拟内存
// 但需要 ptrace ATTACH 才能写非 rw 区域 (5.x+ 之前)
// 5.x+: process_vm_readv/writev 更高效 (不需要打开 /proc/pid/mem)

int memfd = open("/proc/<pid>/mem", O_RDWR);
pread(memfd, buf, size, addr);  // 读 tracee 地址 addr
pwrite(memfd, buf, size, addr); // 写 tracee 地址 addr

seccomp user notify: "新时代的 ptrace"

// SECCOMP_RET_USER_NOTIF: seccomp filter 把 syscall 通知给监督进程
// 监督进程可以检查/修改/批准/拒绝 → 不需要 ptrace (开销低很多)

// ptrace 的问题:
//   所有 syscall 都 stop → tracer → tracee (即使不关心的 syscall)
// seccomp user notify:
//   只拦截 seccomp filter 匹配的 syscall → 其他不受影响
//   → ptrace 的开销是 O(所有 syscall), seccomp 是 O(关心的)

调试器限制

1. 一个进程只能有 1 个 tracer (多个 strace 不行)
2. PTRACE_TRACEME 后的进程不能 setuid (安全限制)
3. 不能 trace init (PID 1)
4. Yama LSM: /proc/sys/kernel/yama/ptrace_scope 限制谁可以 trace
   0: 任何人可以 trace 任何人 (宽松)
   1: 只能 trace 自己或后代 (默认)
   2: 只能 trace 后代 (admin override)
   3: 完全禁止 ptrace

参考

  • man: ptrace(2)
  • 源码⁠: kernel/ptrace.c, kernel/seccomp.c
  • LWN: "Better ptrace", "Seccomp user-space notification"

关键词: ptrace, PTRACE_ATTACH, PTRACE_SYSCALL, PTRACE_SINGLESTEP, PTRACE_PEEKDATA, strace, gdb, /proc/pid/mem, seccomp user notify, Yama