ptrace系统调用及示例
我们来深入学习 ptrace
系统调用
1. 函数介绍
在 Linux 系统中,进程通常是独立运行的,它们有自己的内存空间和执行状态。但是,有时候我们需要一个进程能够观察甚至控制另一个进程的运行。这在很多场景下都非常有用:
- 调试器 (Debugger):像
gdb
这样的调试器,可以让你暂停一个正在运行的程序(被调试者),查看它的内存、寄存器状态,单步执行代码,设置断点等。gdb
就是通过ptrace
来实现这些强大功能的。 - 系统调用跟踪 (Strace):
strace
命令可以显示一个程序执行了哪些系统调用,传入了什么参数,返回了什么结果。它也是利用ptrace
来实现的。 - 进程监控和分析:安全软件或系统管理员工具可能需要监控某个进程的行为。
- 沙箱 (Sandboxing):某些安全机制会使用
ptrace
来限制或监视程序可以执行的操作。
ptrace
(Process Trace) 系统调用就是实现这些功能的核心工具。它允许一个进程(我们称它为跟踪者 Tracer,通常是 gdb
或 strace
)对另一个进程(我们称它为被跟踪者 Tracee,是你想调试或监控的程序)进行各种操作。
简单来说,ptrace
就像是一个功能强大的“钩子”或“后门”,允许一个进程(跟踪者)介入另一个进程(被跟踪者)的执行过程,查看它的状态,甚至暂停、修改它的执行。
2. 函数原型
#include <sys/ptrace.h> // 包含 ptrace 函数声明和相关常量
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
3. 功能
根据 request
参数指定的操作类型,对进程 ID 为 pid
的目标进程执行相应的跟踪操作。
4. 参数详解
request
:enum __ptrace_request
类型。- 这是最重要的参数,它指定了你想要执行的具体操作。常见的操作有:
PTRACE_TRACEME
: 由被跟踪者调用。意思是“请允许我的父进程跟踪我”。这是子进程请求被其父进程跟踪的标准方式。PTRACE_ATTACH
: 由跟踪者调用。意思是“我要开始跟踪进程pid
”。跟踪者可以是任何有权限的进程,不一定是父进程。PTRACE_DETACH
: 由跟踪者调用。意思是“我要停止跟踪进程pid
”,并让它继续独立运行。PTRACE_SYSCALL
(PTRACE_SYSEMU
): 由跟踪者调用。让被跟踪者继续运行,但在它即将进入或离开一个系统调用时暂停。PTRACE_SINGLESTEP
: 由跟踪者调用。让被跟踪者执行一条机器指令,然后暂停。这是实现“单步执行”的基础。PTRACE_CONT
: 由跟踪者调用。让被跟踪者从当前暂停状态继续运行。PTRACE_PEEKDATA
,PTRACE_PEEKTEXT
: 由跟踪者调用。读取被跟踪者内存中的数据或代码。PTRACE_POKEDATA
,PTRACE_POKETEXT
: 由跟踪者调用。修改被跟踪者内存中的数据或代码。PTRACE_GETREGS
,PTRACE_SETREGS
: 由跟踪者调用。获取或设置被跟踪者的 CPU 寄存器值。PTRACE_GETSIGINFO
,PTRACE_SETSIGINFO
: 获取或设置导致进程停止的信号信息。PTRACE_SETOPTIONS
: 设置跟踪选项,例如是否在系统调用入口/出口时暂停 (PTRACE_O_TRACESYSGOOD
),是否自动跟踪子进程 (PTRACE_O_TRACECLONE
等)。- … 还有很多其他选项。
pid
:pid_t
类型。- 指定要操作的被跟踪者进程的进程 ID (PID)。
- 对于
PTRACE_TRACEME
,这个参数被忽略。
addr
:void *
类型。- 一个内存地址。其具体含义取决于
request
的类型。- 对于
PTRACE_PEEK*
/PTRACE_POKE*
,它指定要读取/修改的被跟踪者内存地址。 - 对于其他操作,通常被忽略或有特殊含义。
- 对于
data
:void *
类型。- 一个指向数据的指针。其具体含义也取决于
request
的类型。- 对于
PTRACE_POKEDATA
/PTRACE_POKETEXT
,它指向要写入被跟踪者内存的数据。 - 对于
PTRACE_SET*
操作,它指向包含新值的结构体。 - 对于
PTRACE_PEEK*
操作,结果通常通过ptrace
的返回值给出,而不是通过data
参数。 - 对于其他操作,通常被忽略或有特殊含义。
- 对于
5. 返回值
- 成功:
- 对于大多数
PTRACE_PEEK*
操作,返回值是从被跟踪者内存中读取的数据。 - 对于其他操作,通常返回 0。
- 对于大多数
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因。
6. 错误码 (errno
)
ptrace
可能返回多种错误码,常见的有:
EPERM
: 权限不足。例如:- 尝试跟踪一个你不拥有的进程。
- 目标进程已经在被其他进程跟踪。
- 目标进程是
init
进程 (PID 1)。 - 受到
Yama
安全模块 (ptrace_scope
) 的限制。
ESRCH
: 找不到pid
指定的进程。EINVAL
:request
参数无效,或者在当前状态下不允许该操作。EIO
: I/O 错误,或在某些非法状态下尝试操作(例如,对一个正在运行的进程执行PTRACE_PEEKDATA
)。EFAULT
:addr
或data
指向了调用进程无法访问的内存地址。
7. 被跟踪者状态的变化
当一个被跟踪的进程即将收到一个信号或即将执行系统调用/从系统调用返回时,内核会暂停该进程的执行,并发送一个 SIGCHLD
信号给它的跟踪者。此时,跟踪者可以调用 waitpid()
或 wait()
来等待并获取被跟踪者暂停的通知。
跟踪者在检查被跟踪者的状态(读取寄存器、内存等)并决定如何处理后,可以调用 ptrace(PTRACE_CONT, ...)
或 ptrace(PTRACE_SYSCALL, ...)
等操作让被跟踪者继续运行。
8. 示例代码
下面是一个简单的示例,演示了如何使用 ptrace
来跟踪一个子进程的系统调用(类似 strace
的简化版)。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h> // 包含 struct user_regs_struct (不同架构可能不同)
#include <sys/syscall.h> // 包含系统调用号常量
#include <errno.h>
#include <string.h>
// 一个简单的子进程函数,用于被跟踪
void traced_process() {
// 1. 请求被父进程跟踪
if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
perror("ptrace TRACEME");
exit(EXIT_FAILURE);
}
// 2. 停止自己,让父进程有机会在我们真正开始执行前进行设置
// 这会向父进程发送 SIGSTOP 信号
kill(getpid(), SIGSTOP);
// 3. 执行一些操作
printf("Child: Hello from traced process!\n");
int fd = open("/dev/null", O_WRONLY);
if (fd != -1) {
write(fd, "Test data", 9);
close(fd);
}
printf("Child: Goodbye from traced process!\n");
// 子进程结束
}
// 将系统调用号转换为名称的简单函数 (只列举几个)
const char* get_syscall_name(long syscall_num) {
switch(syscall_num) {
case SYS_write: return "write";
case SYS_open: return "open";
case SYS_close: return "close";
case SYS_read: return "read";
case SYS_mmap: return "mmap";
case SYS_mprotect: return "mprotect";
default: {
static char buf[32];
snprintf(buf, sizeof(buf), "syscall_%ld", syscall_num);
return buf;
}
}
}
int main() {
pid_t child_pid;
int status;
struct user_regs_struct regs; // 用于存储寄存器值
printf("--- Demonstrating ptrace (syscall tracing) ---\n");
// 1. Fork 创建子进程
child_pid = fork();
if (child_pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (child_pid == 0) {
// --- 子进程 ---
traced_process();
} else {
// --- 父进程 (跟踪者) ---
printf("Parent: Started tracing child (PID: %d)\n", child_pid);
// 2. 等待子进程因 SIGSTOP 而暂停
// 当子进程调用 kill(getpid(), SIGSTOP) 时,它会暂停并通知父进程
if (waitpid(child_pid, &status, 0) == -1) {
perror("waitpid (initial stop)");
// 杀死子进程
kill(child_pid, SIGKILL);
exit(EXIT_FAILURE);
}
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGSTOP) {
printf("Parent: Child has stopped itself with SIGSTOP. Ready to trace.\n");
} else {
fprintf(stderr, "Parent: Child did not stop with SIGSTOP as expected.\n");
kill(child_pid, SIGKILL);
exit(EXIT_FAILURE);
}
// 3. 让子进程继续运行,但在每次系统调用时暂停
// PTRACE_SYSCALL 会让子进程在进入和退出系统调用时都暂停
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1) {
perror("ptrace SYSCALL (first)");
kill(child_pid, SIGKILL);
exit(EXIT_FAILURE);
}
// 4. 主循环:等待子进程暂停,打印系统调用信息,然后让它继续
int entering_syscall = 1; // 标志位:1表示即将进入,0表示即将退出
while (1) {
// 等待子进程暂停
if (waitpid(child_pid, &status, 0) == -1) {
if (errno == ECHILD) {
// 子进程已退出
printf("Parent: Child process has exited.\n");
break;
} else {
perror("waitpid");
kill(child_pid, SIGKILL);
break;
}
}
// 检查子进程暂停的原因
if (WIFSTOPPED(status)) {
int sig = WSTOPSIG(status);
if (sig == (SIGTRAP | 0x80)) { // 这是 PTRACE_O_TRACESYSGOOD 的效果
sig = SIGTRAP;
}
if (sig == SIGTRAP) {
// 由于 PTRACE_SYSCALL 而暂停,表示系统调用事件
// 获取寄存器值
if (ptrace(PTRACE_GETREGS, child_pid, NULL, ®s) == -1) {
perror("ptrace GETREGS");
break;
}
// 在 x86_64 上,系统调用号在 orig_rax 寄存器中
long syscall_num = regs.orig_rax;
if (entering_syscall) {
printf("Parent: [Entering] Syscall: %s (%ld)\n", get_syscall_name(syscall_num), syscall_num);
// 可以在这里打印参数 regs.rdi, regs.rsi, regs.rdx 等
entering_syscall = 0;
} else {
// 在 x86_64 上,系统调用返回值在 rax 寄存器中
long retval = regs.rax;
printf("Parent: [Exiting] Syscall: %s (%ld), Return: %ld\n", get_syscall_name(syscall_num), syscall_num, retval);
entering_syscall = 1;
}
// 让子进程继续,直到下一个系统调用事件
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1) {
perror("ptrace SYSCALL (loop)");
break;
}
} else if (sig == SIGSTOP) {
// 可能是初始的 SIGSTOP,或者由其他地方发出的 SIGSTOP
printf("Parent: Child received SIGSTOP.\n");
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1) {
perror("ptrace SYSCALL after SIGSTOP");
break;
}
} else {
// 子进程因其他信号暂停
printf("Parent: Child stopped by signal %d. Forwarding signal.\n", sig);
// 将信号传递给子进程
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, (void*)(unsigned long)sig) == -1) {
perror("ptrace SYSCALL (forward signal)");
break;
}
}
} else if (WIFEXITED(status)) {
// 子进程正常退出
printf("Parent: Child exited normally with status %d.\n", WEXITSTATUS(status));
break;
} else if (WIFSIGNALED(status)) {
// 子进程被信号杀死
printf("Parent: Child was killed by signal %d.\n", WTERMSIG(status));
break;
} else {
printf("Parent: Child stopped with unexpected status: %d\n", status);
if (ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL) == -1) {
perror("ptrace SYSCALL (unexpected)");
break;
}
}
}
printf("Parent: Tracing finished.\n");
}
return 0;
}
9. 编译和运行
# 假设代码保存在 ptrace_example.c 中
# 注意:此代码是 x86_64 架构特定的 (因为使用了 regs.orig_rax, regs.rax 等)
# 在其他架构上需要修改寄存器名称
gcc -o ptrace_example ptrace_example.c
# 运行程序
./ptrace_example
10. 预期输出 (x86_64)
--- Demonstrating ptrace (syscall tracing) ---
Parent: Started tracing child (PID: 12345)
Parent: Child has stopped itself with SIGSTOP. Ready to trace.
Child: Hello from traced process!
Parent: [Entering] Syscall: write (1)
Parent: [Exiting] Syscall: write (1), Return: 35
Parent: [Entering] Syscall: open (2)
Parent: [Exiting] Syscall: open (2), Return: 3
Parent: [Entering] Syscall: write (1)
Parent: [Exiting] Syscall: write (1), Return: 9
Parent: [Entering] Syscall: close (3)
Parent: [Exiting] Syscall: close (3), Return: 0
Child: Goodbye from traced process!
Parent: [Entering] Syscall: write (1)
Parent: [Exiting] Syscall: write (1), Return: 37
Parent: Child process has exited.
Parent: Tracing finished.
11. 总结
ptrace
是一个功能极其强大但也相当复杂的系统调用,是 Linux 系统调试和监控能力的基石。
- 核心作用:允许一个进程(跟踪者)观察和控制另一个进程(被跟踪者)的执行。
- 主要操作 (
request
):PTRACE_TRACEME
: 子进程请求被父进程跟踪。PTRACE_ATTACH
/PTRACE_DETACH
: 开始/停止跟踪一个任意进程。PTRACE_SYSCALL
/PTRACE_SINGLESTEP
: 控制被跟踪者的执行(系统调用步进/单步执行)。PTRACE_CONT
: 让被跟踪者继续运行。PTRACE_PEEK*/PTRACE_POKE*
: 读写被跟踪者的内存。PTRACE_GETREGS/PTRACE_SETREGS
: 读写被跟踪者的寄存器。
- 工作机制:被跟踪者在特定事件(如信号、系统调用)发生时暂停,内核通知跟踪者。跟踪者通过
wait
获取通知,进行检查/修改,然后通过ptrace
命令让其继续。 - 典型应用:
- 调试器 (
gdb
): 设置断点、单步执行、查看变量。 - 系统调用跟踪器 (
strace
): 记录程序执行的所有系统调用。 - 沙箱/安全监控: 限制或记录程序的行为。
- 调试器 (
- 重要限制:
- 权限:通常需要是被跟踪者的父进程,或者具有
CAP_SYS_PTRACE
能力。 - 安全:受
Yama
LSM (ptrace_scope
) 限制,防止恶意跟踪。 - 一对一:一个进程同时只能被一个进程跟踪。
- 权限:通常需要是被跟踪者的父进程,或者具有
- 复杂性:直接使用
ptrace
非常复杂,涉及信号处理、寄存器操作、架构相关细节等。实际工具(如gdb
,strace
)对其进行了大量封装。
对于 Linux 编程新手来说,理解 ptrace
的基本概念和它在 gdb
/strace
等工具中的作用是非常有价值的,它揭示了操作系统底层强大的进程控制能力。