Build a Debugger (1) : ptrace
Linux的ptrace
系统调用是一个强大的工具,允许一个进程(跟踪者)监视和控制另一个进程(被跟踪者)的执行。它广泛应用于调试器、动态分析工具和沙盒环境。以下详细解释其原理和使用方法:
一、ptrace 的原理
1. 核心机制
- 进程跟踪:跟踪进程(如调试器)通过
ptrace
附加到目标进程,成为其“父进程”,从而控制其执行流程。 - 信号拦截:被跟踪进程在接收到信号时会被暂停,等待跟踪进程处理。跟踪进程可以决定是否将信号传递给目标进程。
- 权限控制:默认情况下,非特权进程只能附加到同一用户的进程,但可通过
/proc/sys/kernel/yama/ptrace_scope
调整权限。
2. 关键操作
- 暂停与恢复:被跟踪进程在每次信号、系统调用或单步执行后暂停,跟踪进程通过
waitpid
获取其状态,并通过ptrace
请求恢复其运行。 - 读写资源:跟踪进程可以读写被跟踪进程的寄存器、内存、文件描述符等资源。
- 事件捕获:捕获进程的
exec
、fork
、exit
等事件,并拦截系统调用。
3. 工作流程
- 附加进程:通过
PTRACE_TRACEME
(子进程自我跟踪)或PTRACE_ATTACH
(附加到运行中进程)。 - 事件循环:跟踪进程循环调用
waitpid
等待被跟踪进程停止事件。 - 处理事件:根据事件类型(信号、断点、系统调用等),使用
ptrace
请求操作目标进程。 - 恢复执行:使用
PTRACE_CONT
或PTRACE_SYSCALL
等恢复目标进程执行。
二、ptrace 的使用方法
1. 基本函数
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
request
:指定操作类型(如读内存、写寄存器等)。pid
:目标进程ID。addr
/data
:操作的地址和数据指针。
2. 常用请求类型
请求类型 | 功能描述 |
---|---|
PTRACE_TRACEME | 子进程请求被父进程跟踪 |
PTRACE_ATTACH | 附加到运行中的进程 |
PTRACE_DETACH | 分离并恢复目标进程 |
PTRACE_PEEKTEXT | 读取目标进程内存 |
PTRACE_POKETEXT | 写入目标进程内存 |
PTRACE_GETREGS | 获取寄存器值 |
PTRACE_SETREGS | 设置寄存器值 |
PTRACE_SINGLESTEP | 单步执行一条指令 |
PTRACE_CONT | 恢复目标进程执行 |
PTRACE_SYSCALL | 在下次系统调用入口/出口时暂停 |
3. 典型使用场景
-
启动并跟踪子进程
pid_t child = fork(); if (child == 0) { ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); } else { waitpid(child, &status, 0); // 使用ptrace操作子进程 }
-
附加到运行中的进程
ptrace(PTRACE_ATTACH, target_pid, NULL, NULL); waitpid(target_pid, &status, 0); // 等待目标进程暂停
-
读写内存和寄存器
long data = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); ptrace(PTRACE_POKETEXT, pid, addr, new_data); struct user_regs_struct regs; ptrace(PTRACE_GETREGS, pid, NULL, ®s); regs.rip = new_address; // 修改指令指针 ptrace(PTRACE_SETREGS, pid, NULL, ®s);
-
设置断点
// 备份原指令 long orig = ptrace(PTRACE_PEEKTEXT, pid, addr, NULL); // 写入断点指令(x86的int3) ptrace(PTRACE_POKETEXT, pid, addr, (orig & ~0xFF) | 0xCC); // 恢复执行 ptrace(PTRACE_CONT, pid, NULL, NULL); waitpid(pid, &status, 0); // 命中断点后恢复原指令
三、示例代码
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>
int main() {
pid_t child = fork();
if (child == 0) {
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
execl("/bin/ls", "ls", NULL);
} else {
int status;
waitpid(child, &status, 0); // 等待子进程暂停
// 获取寄存器
struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, child, NULL, ®s);
printf("EIP: %llx\n", regs.rip);
// 恢复执行
ptrace(PTRACE_CONT, child, NULL, NULL);
}
return 0;
}
四、注意事项
- 权限与安全:非root用户可能受限于
ptrace_scope
设置(见/proc/sys/kernel/yama/ptrace_scope
)。 - 错误处理:每次
ptrace
调用后检查返回值,处理errno
。 - 跨平台差异:寄存器名称和断点指令因架构(x86、ARM等)而异。
- 性能影响:频繁的
ptrace
操作可能导致显著性能下降。 - 信号处理:跟踪进程需正确处理信号,避免目标进程意外终止。
通过合理利用ptrace
,开发者可以实现调试器、动态二进制插桩等高级工具,但其复杂性要求深入理解进程控制和信号处理机制。