如何正确使用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, ®s); // 用标记位区分进入/退出系统调用 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, ®s); // 读取当前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, ®s); 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
相关产品推荐
相关产品推荐

