intentrace底层实现(上):ptrace是如何拦截Linux进程每一次系统调用的
【免费下载链接】intentraceintentrace is strace with intent, it goes all the way for you instead of half the way. intentrace is currently in beta项目地址: https://gitcode.com/gh_mirrors/in/intentrace
intentrace 是一个"带意图"的 strace 替代工具:它基于 Linux 的 ptrace 机制拦截并记录进程中每一次系统调用,再通过推理启发式告诉你每次调用"到底想干什么"。本文带你深入 intentrace 的底层实现(上篇),搞懂 ptrace 是如何拦截 Linux 进程的系统调用的。
运行效果:一眼看懂系统调用追踪 🖥️
先看看它追踪google-chrome启动过程时的输出——每一行都是一次被 ptrace 拦截的系统调用,右侧=>之后是 intentrace 对结果的"意图化"解读:
传统 strace 只会打印read(3, 0x7ffd..., 4096) = 832这样的原始信息,你还需要自己查手册理解每个参数的含义。而 intentrace 会直接解释为"从文件描述符 3 读取 832 字节"、"映射内存失败:权限被修改"等自然语言结论。
双进程架构:fork + PTRACE_TRACEME 埋下伏笔
intentrace 追踪一个新程序时,并没有直接运行它,而是在 main.rs 中采用经典的fork 双进程结构(参考parent()与child_trace_me()函数):
- 父进程:调用
fork()创建子进程,自己进入主循环负责"拦截"; - 子进程:先调用
ptrace::traceme()(即PTRACE_TRACEME)声明"我被追踪",然后exec目标程序。
PTRACE_TRACEME的作用是让子进程此后每次进入/退出系统调用时都会自动停下来,并把控制权交还父进程。这是 ptrace 拦截体系的第一块基石。
💡 如果目标是已经在运行的进程,intentrace 则用
-p <pid>配合ptrace::attach动态附加,原理完全相同。
ptrace 的核心机制:一次系统调用 = 两次停顿 🔍
父进程的主循环(src/main.rs中parent()函数)围绕一个简单动作展开:反复执行
ptrace::syscall(pid) → 等待子进程停下来 → 读取寄存器 → 放行PTRACE_SYSCALL让被追踪进程在每次系统调用入口和出口各停顿一次,于是父进程拿到两帧"快照":
| 停顿时机 | 关键寄存器 | 拿到什么 |
|---|---|---|
| 入口(entering) | orig_rax | 系统调用号(如openat、mmap) |
| 入口 | rdi rsi rdx r10 r8 r9 | 最多 6 个调用参数 |
| 出口(exiting) | rax | 返回值或错误码 |
为什么必须读orig_rax而不是rax?因为系统调用返回时rax会被返回值覆盖,而orig_rax是内核原样保留的"原始调用号",是区分进入还是返回的唯一可靠依据。
代码中用一个syscall_entering布尔量在两次停顿之间切换状态:入口时构造SyscallObject并解析参数,出口时读取rax判定成败,再PTRACE_SYSCALL放行进程,直到进程退出(ESRCH)结束整个循环。
偷看别人内存:peek & poke 阶段 👀
光有寄存器里的参数还不够——比如openat的参数只是一个指向路径字符串的指针。要解读出真实路径,必须读取被追踪进程的内存。
intentrace 把这一步封装在 peeker_poker.rs(名字致敬 ptrace 文档里"读写被追踪进程内存"的术语 peek/poke),核心是process_vm_readv系统调用:
read_bytes:按定长读取远程内存;read_bytes_variable_length:读取变长数据(如 C 字符串),并小心处理跨 4KiB 页边界的半读问题;read_bytes_as_struct:把读到的字节块直接转成内存结构体。
这样,像截图中readlink /usr/bin/google-chrome => /etc/alternatives/google-chrome这类"解析出符号链接真实目标"的能力,就是靠远程读内存实现的。
从原始参数到"意图":SyscallObject 与骨架映射 ⚙️
拦截到一次调用后,intentrace 还需要回答两个问题:参数是什么类型?返回值该怎么解读?
- 骨架映射(syscall_skeleton_map.rs):为每个系统调用登记一份"形状"描述,例如
read的返回是字节数或Errno,mmap的返回是地址或MAP_FAILED。目前覆盖了 380+ 个 Linux 系统调用中最常用的约 166 个,遵循"少数调用覆盖绝大多数软件"的策略(类似 UniKraft 内核的思路); - 调用对象(syscall_object.rs):
SyscallObject把调用号、被追踪 PID、进入/退出状态和结果聚合在一起,fill_buffer()负责渲染成终端上那行带颜色的人话输出; - 返回值解析(
src/return_resolvers.rs、src/utilities.rs):把rax解释为"成功 + N 字节"或"失败 + errno 含义",比如EPERM会被翻译成具体的权限说明。
这就是 intentrace 与 strace 的本质区别:拦截靠 ptrace,理解靠推理——它不止展示"发生了什么",还解释"这意味着什么"。
快速上手:构建并运行 intentrace 🚀
git clone https://gitcode.com/gh_mirrors/in/intentrace cd intentrace cargo build --release几个常用命令:
intentrace -q ls # -q 屏蔽被追踪程序自身的输出 intentrace -f docker run alpine # -f 追踪 fork 出的子进程 intentrace -Z ls # -Z 只看失败的系统调用 intentrace -C ls # -C 额外打印统计汇总表注意:当前 beta 版本仅支持 x86-64 架构,多线程程序支持仍在完善中。
小结
intentrace 的拦截链路可以浓缩为一句话:子进程PTRACE_TRACEME自曝 → 父进程PTRACE_SYSCALL双停顿 →getregs读orig_rax和参数寄存器 →process_vm_readv偷读内存还原指针内容 → 骨架映射 + 返回值解析输出"意图"。下一篇我们将深入参数解析与"意图推理"的启发式细节。
【免费下载链接】intentraceintentrace is strace with intent, it goes all the way for you instead of half the way. intentrace is currently in beta项目地址: https://gitcode.com/gh_mirrors/in/intentrace
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考