ptrace系统调用及示例

ptrace系统调用及示例

data-ad-format="fluid" data-ad-layout-key="-7k+ex-4a-9w+4a">

我们来深入学习 ptrace 系统调用

1. 函数介绍

在 Linux 系统中,进程通常是独立运行的,它们有自己的内存空间和执行状态。但是,有时候我们需要一个进程能够观察甚至控制另一个进程的运行。这在很多场景下都非常有用:

  • 调试器 (Debugger):像 gdb 这样的调试器,可以让你暂停一个正在运行的程序(被调试者),查看它的内存、寄存器状态,单步执行代码,设置断点等。gdb 就是通过 ptrace 来实现这些强大功能的。

  • 系统调用跟踪 (Strace):strace 命令可以显示一个程序执行了哪些系统调用,传入了什么参数,返回了什么结果。它也是利用 ptrace 来实现的。

  • 进程监控和分析:安全软件或系统管理员工具可能需要监控某个进程的行为。

  • 沙箱 (Sandboxing):某些安全机制会使用 ptrace 来限制或监视程序可以执行的操作。

ptrace (Process Trace) 系统调用就是实现这些功能的核心工具。它允许一个进程(我们称它为跟踪者 Tracer,通常是 gdb 或 strace)对另一个进程(我们称它为被跟踪者 Tracee,是你想调试或监控的程序)进行各种操作。

简单来说,ptrace 就像是一个功能强大的“钩子”或“后门”,允许一个进程(跟踪者)介入另一个进程(被跟踪者)的执行过程,查看它的状态,甚至暂停、修改它的执行。

2. 函数原型

1
2
3
4
#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 的简化版)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#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&#91;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, &regs) == -1) {
perror("ptrace GETREGS");
break;
}

// 在 x86_64 上,系统调用号在 orig_rax 寄存器中
long syscall_num = regs.orig_rax;

if (entering_syscall) {
printf("Parent: &#91;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: &#91;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. 编译和运行

1
2
3
4
5
6
7
8
# 假设代码保存在 ptrace_example.c 中
# 注意:此代码是 x86_64 架构特定的 (因为使用了 regs.orig_rax, regs.rax 等)
# 在其他架构上需要修改寄存器名称
gcc -o ptrace_example ptrace_example.c

# 运行程序
./ptrace_example

10. 预期输出 (x86_64)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--- 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: &#91;Entering] Syscall: write (1)
Parent: &#91;Exiting] Syscall: write (1), Return: 35
Parent: &#91;Entering] Syscall: open (2)
Parent: &#91;Exiting] Syscall: open (2), Return: 3
Parent: &#91;Entering] Syscall: write (1)
Parent: &#91;Exiting] Syscall: write (1), Return: 9
Parent: &#91;Entering] Syscall: close (3)
Parent: &#91;Exiting] Syscall: close (3), Return: 0
Child: Goodbye from traced process!
Parent: &#91;Entering] Syscall: write (1)
Parent: &#91;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 等工具中的作用是非常有价值的,它揭示了操作系统底层强大的进程控制能力。

ptrace系统调用及示例-CSDN博客

data-ad-format="auto" data-full-width-responsive="true">