ptrace系统调用及示例

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_PEEKDATAPTRACE_PEEKTEXT: 由跟踪者调用。读取被跟踪者内存中的数据或代码。
      • PTRACE_POKEDATAPTRACE_POKETEXT: 由跟踪者调用。修改被跟踪者内存中的数据或代码。
      • PTRACE_GETREGSPTRACE_SETREGS: 由跟踪者调用。获取或设置被跟踪者的 CPU 寄存器值。
      • PTRACE_GETSIGINFOPTRACE_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 指定的进程。
  • EINVALrequest 参数无效,或者在当前状态下不允许该操作。
  • EIO: I/O 错误,或在某些非法状态下尝试操作(例如,对一个正在运行的进程执行 PTRACE_PEEKDATA)。
  • EFAULTaddr 或 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, &regs) == -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 非常复杂,涉及信号处理、寄存器操作、架构相关细节等。实际工具(如 gdbstrace)对其进行了大量封装。

对于 Linux 编程新手来说,理解 ptrace 的基本概念和它在 gdb/strace 等工具中的作用是非常有价值的,它揭示了操作系统底层强大的进程控制能力。

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

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注