You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何正确使用ptrace()实现追踪函数/系统调用的调试器?

用ptrace实现函数与系统调用追踪的思路与修复建议

嘿,我懂你现在卡在用ptrace写调试器的这个节点上——靠寄存器抓操作码来识别函数/系统调用确实容易踩坑,毕竟不同架构的指令差异、调用的进出逻辑都挺绕的。我给你梳理几个关键思路和实用技巧,帮你跳出困境:

一、别自己识别syscall指令了,用ptrace自带的系统调用追踪

ptrace提供了PTRACE_SYSCALL这个超级实用的选项,完全不用你手动解析操作码:

  • 当你给目标进程发送PTRACE_SYSCALL后,进程会在进入系统调用前和系统调用完成后各触发一次SIGTRAP信号
  • 第一次触发时,寄存器里的rax(x86_64)或eax(x86)就是系统调用号,这时候你可以记录「系统调用进入」事件;第二次触发时,寄存器里已经有了系统调用的返回值,对应「系统调用退出」事件
  • 给你一段核心代码片段参考:
// 父进程中,子进程启动/attach后执行
ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
wait(&status);

// 循环处理停止事件
while (WIFSTOPPED(status)) {
    if (WSTOPSIG(status) == SIGTRAP) {
        struct user_regs_struct regs;
        ptrace(PTRACE_GETREGS, child_pid, NULL, &regs);
        
        // 用标记位区分进入/退出系统调用
        static int in_syscall = 0;
        if (!in_syscall) {
            printf("[SYSCALL ENTER] 调用号: %ld\n", regs.rax);
            in_syscall = 1;
        } else {
            printf("[SYSCALL EXIT] 返回值: %ld\n", regs.rax);
            in_syscall = 0;
        }
    }
    // 继续触发下一次syscall断点
    ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
    wait(&status);
}

二、函数调用的识别与进出追踪

普通函数调用(非系统调用)需要识别call和ret指令,这里要注意x86/x86_64的差异:

1. 读取目标进程的指令

用PTRACE_PEEKTEXT读取当前rip(x86_64)指向的内存,获取操作码:

struct user_regs_struct regs;
ptrace(PTRACE_GETREGS, child_pid, NULL, &regs);
// 读取当前rip指向的4字节(x86_64下指令最长15字节,这里先读基础部分)
long opcode = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, NULL);

// 识别相对call指令(操作码0xe8,占5字节)
if ((opcode & 0xff) == 0xe8) {
    // 计算返回地址:当前rip + 指令长度(5字节)
    unsigned long return_addr = regs.rip + 5;
    // 把返回地址存到栈结构里,后续监控rip是否等于这个地址
    printf("[FUNCTION ENTER] 调用地址: 0x%lx, 返回地址: 0x%lx\n", regs.rip, return_addr);
}
// 识别间接call指令(比如0xff 0xd0:call %rax)
else if (((opcode >> 8) & 0xff) == 0xff && (opcode & 0xff) == 0xd0) {
    // 间接调用的返回地址会被压入栈中,需要读取rsp指向的内存
    unsigned long return_addr = ptrace(PTRACE_PEEKDATA, child_pid, regs.rsp, NULL);
    printf("[FUNCTION ENTER] 间接调用, 返回地址: 0x%lx\n", return_addr);
}

2. 追踪函数退出

  • 维护一个栈结构,记录所有未返回的函数的返回地址
  • 每次进程停下时,检查当前rip是否在返回地址栈中:如果匹配,说明函数执行完毕,弹出该地址并记录「函数退出」事件
  • 注意:如果用单步执行(PTRACE_SINGLESTEP)效率会很低,更高效的方式是用硬件断点:通过PTRACE_SETREGS设置调试寄存器(dr0-dr7),在返回地址上设断点,只有当进程跳到返回地址时才会触发中断

三、你现有代码可能踩的坑

  • 架构兼容性:x86和x86_64的寄存器、指令长度差异很大,比如x86用eip而x86_64用rip,指令长度从1到15字节不等,读取操作码时要考虑前缀指令的影响
  • 权限问题:调试进程需要足够权限,比如不能调试root进程除非你是root,或者目标进程和调试进程的UID相同
  • 信号处理:目标进程收到的所有信号都会被ptrace拦截,你需要正确处理WIFSTOPPED(status)的情况,处理完成后用PTRACE_SYSCALL或PTRACE_CONT让进程继续执行

补充一个简化的完整框架代码

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/user.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_RETURN_ADDRS 1024

int main(int argc, char *argv[]) {
    if (argc != 2) {
        fprintf(stderr, "用法: %s <目标进程PID或程序路径>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    pid_t child_pid;
    // 判断是attach到现有进程还是启动新进程
    if (strspn(argv[1], "0123456789") == strlen(argv[1])) {
        child_pid = atoi(argv[1]);
        if (ptrace(PTRACE_ATTACH, child_pid, NULL, NULL) == -1) {
            perror("ptrace attach失败");
            exit(EXIT_FAILURE);
        }
    } else {
        child_pid = fork();
        if (child_pid == 0) {
            // 子进程启动目标程序并开启追踪
            ptrace(PTRACE_TRACEME, 0, NULL, NULL);
            execl(argv[1], argv[1], NULL);
            perror("execl启动目标程序失败");
            exit(EXIT_FAILURE);
        }
    }

    int status;
    wait(&status);
    struct user_regs_struct regs;
    int in_syscall = 0;
    unsigned long return_addrs[MAX_RETURN_ADDRS];
    int stack_ptr = 0;

    while (WIFSTOPPED(status)) {
        ptrace(PTRACE_GETREGS, child_pid, NULL, &regs);

        if (WSTOPSIG(status) == SIGTRAP) {
            // 处理系统调用进出
            if (!in_syscall) {
                printf("[SYSCALL 进入] 调用号: %ld\n", regs.rax);
                in_syscall = 1;
            } else {
                printf("[SYSCALL 退出] 返回值: %ld\n", regs.rax);
                in_syscall = 0;
            }

            // 识别call指令
            long opcode = ptrace(PTRACE_PEEKTEXT, child_pid, regs.rip, NULL);
            if ((opcode & 0xff) == 0xe8) {
                // 相对call指令
                unsigned long return_addr = regs.rip + 5;
                if (stack_ptr < MAX_RETURN_ADDRS) {
                    return_addrs[stack_ptr++] = return_addr;
                    printf("[FUNCTION 进入] 调用地址: 0x%lx, 返回地址: 0x%lx\n", regs.rip, return_addr);
                }
            } else if (((opcode >> 8) & 0xff) == 0xff && (opcode & 0xff) == 0xd0) {
                // 间接call %rax
                unsigned long return_addr = ptrace(PTRACE_PEEKDATA, child_pid, regs.rsp, NULL);
                if (stack_ptr < MAX_RETURN_ADDRS) {
                    return_addrs[stack_ptr++] = return_addr;
                    printf("[FUNCTION 进入] 间接调用, 返回地址: 0x%lx\n", return_addr);
                }
            }

            // 检查函数退出
            for (int i = 0; i < stack_ptr; i++) {
                if (regs.rip == return_addrs[i]) {
                    printf("[FUNCTION 退出] 返回至地址: 0x%lx\n", regs.rip);
                    // 移除该返回地址
                    memmove(&return_addrs[i], &return_addrs[i+1], sizeof(unsigned long) * (stack_ptr - i - 1));
                    stack_ptr--;
                    break;
                }
            }
        }

        // 继续执行,触发下一次syscall断点
        ptrace(PTRACE_SYSCALL, child_pid, NULL, NULL);
        wait(&status);
    }

    // 解除追踪
    ptrace(PTRACE_DETACH, child_pid, NULL, NULL);
    return EXIT_SUCCESS;
}

这个框架只是基础版本,实际使用时还需要处理更多边缘情况,比如不同长度的指令、嵌套函数调用的栈管理、信号的正确转发等。

内容的提问来源于stack exchange,提问作者SamuelRousseaux

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:27:12