Appearance
ptrace 原理 —— 一个进程如何"偷看并控制"另一个进程
strace 教你用
strace追踪系统调用,gdb 教你用断点调试程序。但它们都建立在同一个系统调用之上——ptrace(process trace,进程追踪)。本篇回答核心问题:ptrace到底是怎么让一个进程(tracer)能够暂停、检查、修改另一个进程(tracee)的?内核为这件事设计了怎样的机制?为什么strace和gdb跟踪的进程会明显变慢?
更新时间:2026-08-10
一、ptrace 是什么——一句话定义
ptrace 是一个系统调用,它允许一个进程(称为 tracer,追踪者)观察并控制另一个进程(称为 tracee,被追踪者)的执行,包括读取/修改 tracee 的内存和寄存器、拦截系统调用、单步执行等。这是 Linux 上 strace、gdb 等调试工具的共同基石。
一句话定位:如果说普通系统调用是"用户态请内核代劳做一件事",
ptrace就是"一个用户态进程请内核替它管住另一个进程"。它是进程间干预能力的内核级暴露。
二、tracer / tracee 模型
2.1 两种建立追踪关系的方式
ptrace 的追踪关系必须在内核中建立,有两种方式:
| 方式 | 谁发起 | 关键步骤 | 典型场景 |
|---|---|---|---|
PTRACE_TRACEME | tracee 自己 | tracee 调用 ptrace(PTRACE_TRACEME, 0, 0, 0),然后 execve 启动目标程序 | strace ls、gdb ./a.out |
PTRACE_ATTACH | tracer | tracer 调用 ptrace(PTRACE_ATTACH, pid, 0, 0) 附加到已在运行的进程 | strace -p <pid>、gdb attach <pid> |
两种方式的最终效果相同:内核在 tracee 的 task_struct 中设置 ptrace 标志位,并为两者建立关联。

Fig 2.1:PTRACE_ATTACH 建立追踪的简化流程。tracer 通过
waitpid()获知 tracee 已停止。
2.2 ptrace-stop 状态——tracee 什么时候"停下来"
tracee 并非一直处于被追踪状态,而是在特定事件发生时进入一种特殊的暂停状态,称为 ptrace-stop:
- 信号递送停止:tracee 收到任何信号(包括
SIGTRAP)时,会先停下来通知 tracer,由 tracer 决定是否把信号转发给 tracee - 系统调用停止:如果 tracer 设置了
PTRACE_SYSCALL,tracee 在每次进入和退出系统调用时各停一次 - 单步停止:
PTRACE_SINGLESTEP让 tracee 每执行一条指令就停一次 - execve 停止:
PTRACE_O_TRACEEXEC选项让 tracee 在execve调用成功后、新程序的第一条指令执行前停住
当一个 tracee 进入 ptrace-stop 时,内核将其状态设为 TASK_TRACED(可通过 /proc/<pid>/status 中的 State: T 观察到)。此时 tracer 调用 waitpid() 会立即返回。
2.3 ptrace 整体原理序列图
下面的序列图展示了 tracer、内核、tracee 三者之间从建立追踪到解除追踪的完整生命周期。ptrace 的核心设计思想是:内核充当中介,tracer 永远不直接操作 tracee,所有读写和控制都通过 ptrace() 系统调用由内核代理执行。

Fig 2.2:ptrace 整体原理——从建立追踪到解除追踪的五个阶段。整个生命周期的核心是第②→④步的循环:tracee 因事件停止 → 内核通知 tracer → tracer 检查/修改 tracee 状态 → tracer 恢复 tracee 执行,如此往复。
下面按三层递进展开 ptrace 的工作原理:先看谁控制谁(控制面),再看数据怎么流动(数据面),最后把二者串成完整的运行模型(循环模型)。
第一层:控制面——谁在控制谁,怎么控制
ptrace 建立了一种单向的、内核中介的控制关系。
建立:tracee 通过 PTRACE_TRACEME 声明"我愿意被追踪"(或 tracer 通过 PTRACE_ATTACH 主动附加,参见 Fig 2.1)。无论哪种方式,最终都是内核在 tracee 的 task_struct 中设置 ptrace 标志位,并将 tracer 设为 tracee 的"临时父进程"。这个临时父子关系至关重要——后续所有 wait/signal 通信都依赖它。
拦截:tracee 正常运行时,一旦命中追踪事件——系统调用入口/出口、收到信号、单步完成一条指令、execve 调用——内核强制拦截 tracee 的执行流,将其置入 ptrace-stop 状态(TASK_TRACED),然后向 tracer 发送 SIGCHLD。关键点:tracee 不是"主动停下来通知",而是被内核剥夺了 CPU——tracee 在被追踪时完全无法"抵抗"或"隐藏"自己的行为。
恢复与解除:tracer 完成检查/修改后,调用 PTRACE_CONT 或 PTRACE_SYSCALL 恢复 tracee 继续执行;追踪结束时调用 PTRACE_DETACH 通知内核清除追踪标志,tracee 恢复为普通进程独立运行。整个过程中,控制权始终在 tracer 一侧——tracee 只是一个被动的执行单元。
一句话:控制面 = tracer 通过内核剥夺 tracee 的 CPU 并在需要时归还。
第二层:数据面——tracer 怎么"看到"和"修改" tracee
在控制面确保 tracee 处于停止状态后,数据面提供了两个方向的能力:
读方向(tracer ← tracee):tracer 通过 PTRACE_PEEKDATA / PTRACE_PEEKTEXT 读取 tracee 内存中的一个 word,通过 PTRACE_GETREGS / PTRACE_GETFPREGS 读取通用/浮点寄存器快照,通过 PTRACE_GETSIGINFO 获取导致停止的信号详情。所有读取都是单向的——tracer 发出 ptrace() 请求,内核通过 access_process_vm() 从 tracee 地址空间中提取数据并返回给 tracer,tracee 对此毫不知情。这是 strace 抓 syscall 参数和返回值的基础,也是 gdb 查看变量值的底层机制。
写方向(tracer → tracee):tracer 通过 PTRACE_POKEDATA / PTRACE_POKETEXT 向 tracee 内存写入数据,通过 PTRACE_SETREGS 修改 tracee 寄存器。这是 gdb 实现 set variable x=3 和软件断点(将目标地址指令替换为 int3 / 0xCC)的底层能力。写入发生在 tracee 停止期间,恢复执行后 tracee 看到的已经是修改后的状态——对 tracee 完全透明。
为什么"一个字一个字"地读:PEEKDATA / POKEDATA 每次操作一个 long,这是历史接口设计的产物。现代内核也提供了 process_vm_readv() / process_vm_writev() 做批量读写,但不依赖 ptrace 追踪关系。
一句话:数据面 = 内核充当中介,在 tracee 停止时替 tracer 完成地址空间的读写。
第三层:循环模型——停、看、改、走
控制面和数据面不是独立运作的,而是交织成一个固定的循环——这是 ptrace 运行时的核心节奏:
tracee 运行 → 事件触发 → 内核拦截(TASK_TRACED)→ SIGCHLD → tracer waitpid 返回
→ tracer 读取(数据面:读)→ tracer 可选修改(数据面:写)
→ tracer 恢复(控制面:CONT/SYSCALL)→ tracee 继续运行 → 下一次事件...每一次循环包含 4 次上下文切换(tracee→内核→tracer 的通知 + tracer→内核→tracee 的恢复),如果使用 PTRACE_SYSCALL 追踪系统调用,一个 syscall 会在入口和出口各触发一次循环——共 8 次上下文切换。这就是 §4.3 中分析的性能开销根源。
strace 的本质:就是把上述循环重复执行 N 次——每次在 syscall 入口停一次(读 orig_rax 获得系统调用号,读参数寄存器),在 syscall 出口再停一次(读 rax 获得返回值),格式化输出后继续。
gdb 的本质:同样是这个循环,但使用的"武器"更丰富——用 POKETEXT 注入 int3 实现断点(命中断点时触发 SIGTRAP → 进入循环),用 SINGLESTEP 实现逐指令执行,用 PEEKDATA / POKEDATA 实现变量读写。
一句话:循环模型 = 停-看-改-走的无限循环,strace 和 gdb 只是在这个循环中使用了不同的 ptrace 操作组合。
三、核心 ptrace 操作——你需要哪一把"手术刀"
ptrace 的操作通过 request 参数区分,按功能可分为四组:
3.1 控制 tracee 的执行
| 操作 | 含义 | 备注 |
|---|---|---|
PTRACE_CONT | 让 tracee 继续运行,直到下一个事件 | 可附带一个信号值(如 SIGTERM)注入给 tracee |
PTRACE_SYSCALL | 继续运行,但在每次 syscall 入口和出口停止 | strace 的核心依赖——两次停止分别对应进入和退出 syscall |
PTRACE_SINGLESTEP | 执行一条 CPU 指令后停止 | 利用 CPU 的 TF(Trap Flag)实现;gdb 的 stepi 用这个 |
PTRACE_DETACH | 解除追踪关系,tracee 恢复正常运行 | 与 PTRACE_ATTACH 对应 |
PTRACE_KILL | 向 tracee 发送 SIGKILL | 等同于 kill(tracee, SIGKILL) |
3.2 读写 tracee 的内存和寄存器
| 操作 | 含义 | 限制 |
|---|---|---|
PTRACE_PEEKDATA / PTRACE_PEEKTEXT | 读取 tracee 内存中的一个 word(与机器字长同) | 一次只读一个 word,多字节需循环 |
PTRACE_POKEDATA / PTRACE_POKETEXT | 向 tracee 内存写入一个 word | gdb 的 set variable x=3 用这个 |
PTRACE_GETREGS | 读取 tracee 的通用寄存器快照 | 返回 user_regs_struct,包含 rax/rbx/rcx 等 |
PTRACE_SETREGS | 修改 tracee 的通用寄存器 | gdb 的 set $rax=0 用这个 |
PTRACE_GETFPREGS / PTRACE_SETFPREGS | 读写浮点寄存器 | 对应 x87 FPU/SSE 寄存器 |
为什么"一个字一个字"地读?
PEEKDATA/POKEDATA通过内核函数access_process_vm()实现,每次操作一个long,这是历史接口设计的产物。现代内核也提供了process_vm_readv()/process_vm_writev()做批量读写,但不依赖 ptrace 追踪关系。
3.3 获取事件详情
| 操作 | 含义 |
|---|---|
PTRACE_GETSIGINFO | 获取导致 tracee 停止的信号的详细信息(siginfo_t) |
PTRACE_GETEVENTMSG | 获取 ptrace 事件相关的附加消息(如 PTRACE_EVENT_FORK 时返回子进程 PID) |
3.4 设置追踪选项
PTRACE_SETOPTIONS 可以启用更细粒度的事件通知:
| 选项 | 触发条件 |
|---|---|
PTRACE_O_TRACESYSGOOD | 区分"真正的 SIGTRAP"和"系统调用停止产生的 SIGTRAP"(第 7 bit 设为 1) |
PTRACE_O_TRACEFORK / PTRACE_O_TRACEVFORK / PTRACE_O_TRACECLONE | tracee 调用 fork/vfork/clone 时通知 tracer |
PTRACE_O_TRACEEXEC | tracee 调用 execve 时通知 tracer |
PTRACE_O_TRACEEXIT | tracee 退出时通知 tracer |

Fig 3.1:ptrace 核心操作的四组分类。
四、内核实现——ptrace 是怎么在 task_struct 里运作的
4.1 关键数据结构
追踪关系挂载在 tracee 的 task_struct 上,核心字段:
| 字段 | 含义 |
|---|---|
task_struct->ptrace | 标志位掩码(PT_PTRACED 等),标识当前进程是否被追踪 |
task_struct->parent | 当被追踪时,tracer 成为 tracee 的"临时父进程"——waitpid() 依赖此关系 |
task_struct->ptrace_entry | 链接到 tracer 的 ptraced 链表,tracer 用它遍历所有被追踪进程 |
task_struct->ptraced | tracer 端:被自己追踪的所有 tracee 组成的链表头 |
核心设计:ptrace 巧妙地复用了 Linux 的 signal / wait 机制。当 tracee 进入 ptrace-stop 时,内核让它以
TASK_TRACED状态睡眠,然后向 tracer 发送SIGCHLD。tracer 调用waitpid()就像等待子进程退出一样等待 tracee 停止——这是 ptrace 机制的灵魂。
4.2 一次 PTRACE_SYSCALL 的完整流程
以 strace ls 追踪 write(1, "foo", 3) 为例,展示 tracee 在系统调用入口和出口各停一次的完整过程:

Fig 4.1:
PTRACE_SYSCALL下一次write()的完整交互流程。tracee 在 syscall 入口和出口各停一次,每次停止需要 tracer 主动恢复(PTRACE_SYSCALL)。注意两次停止之间,tracer 通过PTRACE_GETREGS分别获取到 syscall 编号和参数(rax=1),以及 syscall 返回值(rax=3)。
4.3 为什么被追踪的进程会变慢——开销分析
每个 strace 用户都能直观感受到追踪后的性能下降,原因有三层:
| 层次 | 开销来源 | 量级 |
|---|---|---|
| 上下文切换 | 每停止一次:tracee → kernel → tracer(通过信号+wait),tracer 检查后再通过 PTRACE_SYSCALL 恢复 tracee。一次系统调用触发 2 次停止,每次 2 次上下文切换,共 4 次 | ~数微秒/次 |
| 信号处理开销 | 内核为每次停止生成 SIGTRAP,通过信号投递机制走完整路径 | ~微秒级 |
| 缓存污染 | 频繁的上下文切换导致 CPU 缓存(L1/L2/ITLB/DTLB/BPU)被反复冲刷 | 间接影响,但对高频 syscall 的应用是灾难性的 |
假设一个程序每毫秒调用一次
write():原本每次 syscall 只需要约 100-200ns(陷入+执行+返回),strace 追踪后变成了 4 次上下文切换 + 2 次信号投递 + tracer 用户态逻辑——轻松增加 4-5 个数量级的开销。
五、两个典型应用场景——strace 和 gdb 怎么各取所需
5.1 strace——只看 syscall,别的不管
strace 的核心循环极其简单:
- 等待 tracee 在 syscall-enter-stop 停下(
waitpid) - 读取
orig_rax获得系统调用号,读取参数寄存器 (rdi/rsi/rdx/r10/r8/r9) 获得参数 - 格式化输出一行 "write(1, "foo", 3)"
- 调用
ptrace(PTRACE_SYSCALL)让 tracee 继续运行(syscall 被执行) - 等待 tracee 在 syscall-exit-stop 停下
- 读取
rax获得返回值,格式化输出 "= 3" - 回到步骤 4,循环往复
strace 不需要 PEEKDATA / POKEDATA,也不需要 SINGLESTEP——它只关心系统和调用的进出边界。
5.2 gdb——需要单步、断点、变量读写全套能力
gdb 对 ptrace 的使用更丰富:
- 断点:gdb 用
PTRACE_PEEKTEXT读取目标地址的原始指令,用PTRACE_POKETEXT将其替换为int3(0xCC)指令。当 CPU 执行到int3时触发SIGTRAP,tracee 进入 ptrace-stop,gdb 再把原始指令恢复回去 - 单步:
PTRACE_SINGLESTEP设置 CPU 的 Trap Flag,执行一条指令后自动触发SIGTRAP - 变量读写:
PTRACE_PEEKDATA/PTRACE_POKEDATA直接读写 tracee 地址空间 - 寄存器操作:
PTRACE_GETREGS/PTRACE_SETREGS读写通用寄存器

Fig 5.1:strace 和 gdb 对 ptrace 操作的使用差异。strace 只需要 syscall 级追踪,gdb 需要指令级控制。
六、安全控制——ptrace_scope
ptrace 的能力太强——能读内存、能改寄存器、能注入信号——如果不加限制,任何进程都可以窥探密码、篡改关键逻辑。Linux 通过 /proc/sys/kernel/yama/ptrace_scope 控制追踪权限:
| 值 | 含义 |
|---|---|
| 0 | 经典模式:同一个 uid 的进程可以互相 ptrace(传统 UNIX 行为) |
| 1 | 受限模式(默认):只有父进程(或 PR_SET_PTRACER 指定的调试器)能追踪子进程 |
| 2 | 管理员模式:只有 CAP_SYS_PTRACE 权限的进程能追踪 |
| 3 | 完全禁止:任何进程都不能 ptrace,也不能在运行时修改 scope |
现代 Linux 发行版默认
ptrace_scope = 1。这就是为什么strace -p <pid>追踪一个非子进程时可能收到Operation not permitted——需要 root 权限或先建立父子关系。
七、ptrace 的局限与替代方案
ptrace 虽强大,但在高频追踪场景下有明显瓶颈:
| 局限 | 原因 | 替代方案 |
|---|---|---|
| 性能开销极大 | 每次 syscall 触发 4 次上下文切换(见 4.3 节) | 基于内核事件的追踪框架(BPF/eBPF、ftrace、kprobe) |
| 一次只能一个 tracer | 一个 tracee 只能被一个进程 ptrace,无法并发观察 | BPF 程序可以同时运行多个 |
| 用户态/内核态的反复切换 | tracer 是用户态进程,每次检查都需切换 | BPF 程序在内核中执行判断逻辑,只在需要时才传数据到用户态 |
| 不支持生产环境大规模部署 | 开销不可接受 | eBPF 追踪点(bpftrace、perf trace)接近零开销 |
| 粗糙的停止粒度 | 只能在 syscall/信号/单步等"粗"事件上停止 | kprobe/uprobe 可以在任意内核/用户函数入口追踪 |
一句话: ptrace 是调试和单进程分析的最佳选择(strace、gdb 的基石),但在生产环境做系统级观测时,BPF 是更合适的选择。
八、与相关文档的关系
| 维度 | 文档 | 与本篇的关系 |
|---|---|---|
| 工具层 | strace | strace = ptrace + syscall 号解析 + 格式化输出 |
| perf | perf 不依赖 ptrace,而是用 kernel events + sampling | |
| 原理对比 | syscall | syscall 篇讲"正常 syscall 怎么走的",ptrace 篇讲"怎么拦截 syscall" |
| signal-multithread | ptrace 复用 signal 机制作为停止通知 | |
| 注入技术 | library-injection | 动态库注入的 ptrace 方案(PTRACE_ATTACH + mmap + dlopen) |
一句话总结:
ptrace是通过内核在task_struct中建立 tracer/tracee 关联、复用 signal/wait 机制实现进程停止通知、利用access_process_vm实现跨进程内存读写的一套系统调用接口。它是 strace、gdb 等调试工具的基石,但每次停止带来的多次上下文切换使其不适合高频生产环境追踪——此时应转向 BPF。