求助:基于PTrace实现C++包装器替代Strace追踪进程打开文件
Got it, let's tackle this problem head-on. You’re right that strace can be heavy because it logs every syscall and generates verbose output, but building a custom ptrace-based tool to only track file opens will cut down that overhead significantly. Here’s a practical, step-by-step guide to make this happen:
Core Idea
Strace’s performance hit comes from intercepting all syscalls and generating verbose output. We’ll focus only on syscalls that open files (open, openat, creat, etc.), which means we can skip most of the overhead strace incurs.
Step-by-Step Implementation
1. Attach to the Target Process
First, we need to attach to the process we want to monitor. This pauses the process, letting us take control via ptrace.
2. Intercept Syscall Entries
On x86_64 systems, when a process executes a syscall, it triggers a stop that we can catch with waitpid. We’ll check the syscall number (stored in the orig_rax register) to see if it’s one of the file-opening syscalls we care about.
3. Extract the File Path
For syscalls like open or openat, the filename is passed as a pointer in the target process’s user space. We use ptrace(PTRACE_PEEKDATA) to read this string from the target’s memory.
4. Resume the Process
After logging the file path, we let the syscall complete and wait for the next stop. We repeat this loop until the target process exits.
Full C++ Example Code
#include <iostream> #include <sys/ptrace.h> #include <sys/wait.h> #include <sys/user.h> #include <cstring> #include <unistd.h> #include <stdexcept> void read_filename(pid_t pid, unsigned long addr, char* buf, size_t buf_size) { memset(buf, 0, buf_size); size_t bytes_read = 0; // Read 8 bytes at a time (x86_64 long) to minimize ptrace calls while (bytes_read < buf_size - 1) { long data = ptrace(PTRACE_PEEKDATA, pid, addr + bytes_read, nullptr); if (data == -1) { perror("Failed to read memory"); break; } memcpy(buf + bytes_read, &data, sizeof(long)); // Stop early if we hit a null terminator if (memchr(buf + bytes_read, '\0', sizeof(long)) != nullptr) { break; } bytes_read += sizeof(long); } } int main(int argc, char* argv[]) { if (argc != 2) { std::cerr << "Usage: " << argv[0] << " <target-pid>\n"; return EXIT_FAILURE; } pid_t target_pid = std::stoi(argv[1]); // Attach to target process if (ptrace(PTRACE_ATTACH, target_pid, nullptr, nullptr) == -1) { throw std::runtime_error("Failed to attach to process: " + std::string(strerror(errno))); } int status; waitpid(target_pid, &status, 0); if (!WIFSTOPPED(status)) { std::cerr << "Process didn't stop after attachment\n"; ptrace(PTRACE_DETACH, target_pid, nullptr, nullptr); return EXIT_FAILURE; } struct user_regs_struct regs; while (true) { // Wait for syscall entry if (ptrace(PTRACE_SYSCALL, target_pid, nullptr, nullptr) == -1) { perror("ptrace syscall entry failed"); break; } waitpid(target_pid, &status, 0); if (WIFEXITED(status) || WIFSIGNALED(status)) { std::cout << "Target process exited with status: " << status << "\n"; break; } // Get register state at syscall entry if (ptrace(PTRACE_GETREGS, target_pid, nullptr, ®s) == -1) { perror("Failed to get registers"); break; } // Check for file-opening syscalls (x86_64 syscall numbers) long syscall_num = regs.orig_rax; if (syscall_num == 2 || syscall_num == 257 || syscall_num == 85) { // syscall 2 = open, 257 = openat, 85 = creat char filename[512]; unsigned long filename_addr; if (syscall_num == 257) { // openat uses rdi as dirfd, rsi as filename filename_addr = regs.rsi; } else { // open/creat use rdi as filename filename_addr = regs.rdi; } read_filename(target_pid, filename_addr, filename, sizeof(filename)); std::cout << "[PID " << target_pid << "] Opened file: " << filename << "\n"; } // Wait for syscall exit if (ptrace(PTRACE_SYSCALL, target_pid, nullptr, nullptr) == -1) { perror("ptrace syscall exit failed"); break; } waitpid(target_pid, &status, 0); if (WIFEXITED(status) || WIFSIGNALED(status)) { std::cout << "Target process exited with status: " << status << "\n"; break; } } // Clean up: detach from the process ptrace(PTRACE_DETACH, target_pid, nullptr, nullptr); return EXIT_SUCCESS; }
Key Notes & Optimizations
- Architecture Compatibility: This example is for x86_64. For 32-bit x86, use
orig_eaxfor syscall numbers, andebx/ecxfor parameters instead ofrdi/rsi. - Performance: We read memory in 8-byte chunks instead of single bytes to reduce the number of
ptracecalls (each call has overhead). This is way faster than strace’s default mode. - Permissions: You’ll need root privileges or the same UID as the target process to attach.
- Tracking Child Processes: If your target forks children, add
ptrace(PTRACE_SETOPTIONS, pid, nullptr, PTRACE_O_TRACEFORK | PTRACE_O_TRACECLONE)after attaching to track child processes. - Error Handling: The example has basic error handling; in production code, you’ll want to handle signals like
SIGINTto cleanly detach from the target process.
Why Your Previous Guides Didn’t Help
Most general ptrace guides focus on attaching, modifying registers, or debugging execution—but your use case requires syscall interception and user-space memory reading, which are more specialized tasks. This guide zeroes in on exactly that workflow.
内容的提问来源于stack exchange,提问作者Nisan Bahar

