必须先attach并waitpid等待目标进程进入stopped状态,否则后续ptrace操作会失败;attach后需处理SIGSTOP、对齐内存访问、修改代码段前需mprotect改权限,并在detach前恢复寄存器和信号状态。

ptrace 跟踪目标进程前必须先 attach 并处理 SIGSTOP
Linux 下 ptrace 不能直接“热插拔”进一个正在运行的进程;必须让目标进程处于可被控制的状态。常见错误是调用 ptrace(PTRACE_ATTACH, pid, nullptr, nullptr) 后立刻读内存,结果失败或返回 -1 —— 因为内核此时已向目标进程发送 SIGSTOP,但你还没等它真正停止就去操作了。
正确做法是:waitpid(pid, &status, 0) 阻塞等待目标进入 stopped 状态(WIFSTOPPED(status) == true)。否则后续所有 ptrace(PTRACE_PEEKDATA) 或 PTRACE_POKEDATA 都会返回 -1 并设 errno = ESRCH(进程不存在或不可访问)或 EIO(I/O 错误,实际多因未停稳)。
- 目标进程若本身忽略
SIGSTOP(极少见),attach会失败,errno = EPERM - 普通用户只能 trace 自己启动的子进程,或拥有
CAP_SYS_PTRACE能力(如 root)才能 attach 其他用户进程 -
ptrace是单次生效的:每次peek/poke后,目标仍保持 stopped 状态,直到你显式ptrace(PTRACE_CONT, ...)
用 PTRACE_PEEKDATA / PTRACE_POKEDATA 读写内存要对齐且注意字节序
ptrace 的内存操作接口只支持按 long(即寄存器宽度)读写:在 x86_64 上是 8 字节,在 i386 上是 4 字节。直接传入一个 char* 地址去改单个字节?不行 —— 你得先 PEEKDATA 拿到整个 word,修改对应字节,再 POKEDATA 写回去。
示例:想把地址 0x7fffe1234567 处的第 3 个字节(从 0 开始)改成 0x99(x86_64):
long word = ptrace(PTRACE_PEEKDATA, pid, (void*)0x7fffe1234560, nullptr); // 假设地址 0x7fffe1234567 落在该 word 的 offset=7 位置(因为 0x67 - 0x60 = 7) unsigned char* p = (unsigned char*)&word; p[7] = 0x99; ptrace(PTRACE_POKEDATA, pid, (void*)0x7fffe1234560, (void*)word);
- 地址必须按
sizeof(long)对齐,否则ptrace返回 -1,errno = EIO - 不要依赖目标进程的 endianness ——
ptrace总是按宿主机字节序读写,而用户空间内存布局就是宿主机序,所以无需额外转换 - 写入后不会自动刷 cache,但现代 x86/x86_64 的 store buffer 不会导致可见性问题;不过若目标进程正执行该地址附近指令,可能触发
SEGV_MMAP或执行旧指令缓存(需__builtin___clear_cache()或sys_icache_invalidate,但 glibc 不暴露,通常靠mprotect(..., PROT_READ|PROT_WRITE|PROT_EXEC)触发刷新)
修改代码段前必须先 mprotect 改写权限,否则 PTRACE_POKEDATA 失败
即使你成功 attach 并停稳了进程,尝试往 .text 段(比如函数入口)写数据时,ptrace(PTRACE_POKEDATA, ...) 仍大概率返回 -1,errno = EIO。这不是 ptrace 的限制,而是底层页表标记为只读(PROT_READ|PROT_EXEC),写操作被 MMU 拦截。
解决方法是:用 ptrace(PTRACE_GETREGS, ...) 拿到目标进程的 user_regs_struct,从中提取 rip 或查 /proc/pid/maps 确定目标地址所属 vma,再调用 ptrace(PTRACE_SYSCALL, ...) 让目标自己执行一次 mprotect 系统调用(需构造栈/寄存器上下文),或者——更简单但需 root —— 在 tracer 进程中用 process_vm_writev(绕过 ptrace)配合 mprotect 注入(但这就偏离 ptrace 主线了)。
- 普通用户无法让目标进程执行任意系统调用,除非它原本就在
syscall中断点上(比如刚execve完) - 更可行的做法是:仅修改堆或栈上的数据(如全局变量、局部变量地址),这些区域默认可写;若真要 patch 代码,优先考虑
LD_PRELOAD或gdb --pid交互式 patch -
ptrace修改内存后,目标恢复执行前,务必确认 CPU 指令缓存已同步(尤其 ARM64 需显式ic ivau,但用户态无权限;此时建议避免 patch 正在执行的代码页)
detach 前忘记恢复寄存器或信号状态会导致目标崩溃
很多人 ptrace(PTRACE_ATTACH) → PEEKDATA → POKEDATA → PTRACE_DETACH 就完事,结果目标进程 segfault 或卡死。根本原因是:DETACH 不会自动恢复被修改过的寄存器(比如你动过 rip 或 rax),也不会重置挂起的信号(如 attach 时收到的 SIGSTOP 可能还在 pending 状态)。
安全 detach 流程必须包含:
- 调用
ptrace(PTRACE_GETREGS, pid, nullptr, ®s)保存原始寄存器(最好在 attach 后立即做) - 如有修改,用
PTRACE_SETREGS恢复rip、rsp等关键寄存器 - 用
ptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_TRACEEXIT)等选项控制退出行为,或手动kill(pid, SIGCONT)清除 pending 的SIGSTOP - 最后才
ptrace(PTRACE_DETACH, pid, nullptr, nullptr)—— 此时内核会自动向目标发SIGCONT,但前提是它没有其他阻塞信号
最隐蔽的坑:目标进程若在 read() 等可中断 sleep 中被 attach,waitpid 返回后它的 task_struct->state 是 TASK_INTERRUPTIBLE,但 DETACH 不保证唤醒它;必须显式 kill(pid, SIGCONT) 才行。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











