必须以PTRACE_TRACEME开始:子进程在fork后、execve前调用ptrace(PTRACE_TRACEME,0,NULL,NULL),向内核声明允许父进程调试,否则waitpid会失败或阻塞,且后续ptrace操作因权限不足而无效。

ptrace 调试器必须以 PTRACE_TRACEME 开始,否则子进程无法被父进程控制
Linux 下用 ptrace 写调试器,第一步不是 attach,而是让被调试进程主动请求被跟踪。子进程在 fork() 后、execve() 前必须调用 ptrace(PTRACE_TRACEME, 0, nullptr, nullptr),否则后续 waitpid() 会永远阻塞或直接失败(返回 -1,errno == ESRCH)。这个调用本质是向内核声明:“我愿意被父进程调试”。没有它,父进程调用 PTRACE_ATTACH 会因权限不足失败(除非有 CAP_SYS_PTRACE)。
常见错误现象:waitpid() 返回 -1 且 errno == ECHILD 或 ESRCH;或者子进程直接运行完退出,父进程完全收不到信号。
- 必须在
execve()之前调用PTRACE_TRACEME - 调用后需立即
raise(SIGSTOP)或依赖execve自动触发SIGTRAP,确保停在入口点 - 父进程要
waitpid(pid, &status, 0)等待子进程首次停止,之后才能读寄存器或内存
读写寄存器要用 PTRACE_GETREGS / PTRACE_SETREGS,别直接用 PTRACE_PEEKUSER
PTRACE_PEEKUSER 和 PTRACE_POKEUSER 是历史遗留接口,依赖架构特定的寄存器偏移(比如 x86_64 上 RIP 在 offset 0x70),可移植性差、易出错。现代写法应统一使用 PTRACE_GETREGS 和 PTRACE_SETREGS,它们操作标准结构体 user_regs_struct,字段名清晰(如 regs.rip、regs.rax),编译器能做类型检查。
示例:获取当前指令地址
立即学习“C++免费学习笔记(深入)”;
struct user_regs_struct regs;
if (ptrace(PTRACE_GETREGS, pid, nullptr, ®s) == -1) {
perror("PTRACE_GETREGS");
return -1;
}
printf("RIP = 0x%lx\n", regs.rip);
-
PTRACE_GETREGS需在进程处于 stopped 状态下调用,否则返回 -1(errno == ESRCH) - 修改寄存器后必须配对调用
PTRACE_SETREGS,再用PTRACE_SINGLESTEP或PTRACE_CONT继续执行 - x86_64 下
user_regs_struct定义在<sys/user.h>,无需手动计算偏移
修改内存得用 PTRACE_PEEKTEXT / PTRACE_POKETEXT,且地址必须按页对齐并可写
读写目标进程内存,必须用 PTRACE_PEEKTEXT 和 PTRACE_POKETEXT(两者在 Linux 上实际等价于 PTRACE_PEEKDATA/PTRACE_POKEDATA)。不能用 read()/write() 直接操作 /proc/PID/mem —— 那需要额外打开权限,且不兼容所有内核配置(ptrace_scope 限制下常被拒)。
关键限制:
- 目标地址必须是合法的用户空间虚拟地址,且所在内存页已映射(可用
mincore()或解析/proc/PID/maps验证) - 目标页必须可写(
maps中含w标志),否则PTRACE_POKETEXT返回 -1(errno == EIO) - 一次只能读写一个
long(8 字节),跨字节需循环调用;若要改字符串或大块数据,得拆成多次PTRACE_POKETEXT
示例:将地址 0x401000 处的 4 字节改为 0x90909090(nop 指令)
long data = 0x90909090; // 注意:只覆盖低 4 字节,高位保持原值(需先 peek 原值再 mask) long orig = ptrace(PTRACE_PEEKTEXT, pid, (void*)0x401000, nullptr); data = (orig & ~0xffffffffL) | data; ptrace(PTRACE_POKETEXT, pid, (void*)0x401000, (void*)data);
单步执行必须用 PTRACE_SINGLESTEP,但要注意信号透传和 syscall 陷阱
让被调试进程执行一条指令,应该用 PTRACE_SINGLESTEP,而不是 PTRACE_CONT 加断点模拟。前者由内核硬件支持(x86 的 TF 标志),可靠且无需手动插桩。但有两个关键细节:
- 如果子进程在
waitpid()返回时带WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP,说明刚触发断点或单步完成;此时若想继续单步,直接发PTRACE_SINGLESTEP即可 - 若子进程因
execve或clone进入新上下文,内核可能发送SIGTRAP | 0x80(即WSTOPSIG(status) | 0x80),表示 syscall 入口/出口——这是PTRACE_O_TRACEEXEC或PTRACE_O_TRACESYSGOOD的副作用,需显式处理,否则容易误判为普通断点 - 调用
PTRACE_SINGLESTEP后,务必再次waitpid(),否则进程会脱离控制变成僵尸或继续运行
容易被忽略的是:某些 libc 函数(如 malloc)内部频繁调用 brk、mmap,若开启了 PTRACE_O_TRACE_SYSGOOD,每次 syscall 都会中断,导致调试器卡死。生产级工具通常只在需要时临时开启 syscall trace,用完立刻关闭。


















