基于ptrace函数从结构体提取数据的技术求助
嘿,作为ptrace新手,想抓fstat返回的stat结构体里的st_mode和st_size确实容易踩坑,我来帮你捋捋怎么搞定这个问题~
先说说你之前代码可能失败的原因
你的buffer函数大概率踩了这几个坑:
- ptrace读取内存是按**字(64位系统是8字节,32位是4字节)**来读的,不能直接按任意size字节读取,否则会出现内存对齐或者读取不完整的问题
- 没有正确处理ptrace返回-1的情况:如果读取的内存内容本身就是-1,errno不会被设置,这时候不能直接判定为读取失败
- 可能没等fstat系统调用完成就去读内存:进入系统调用时,内核还没把stat结构体的数据填充到用户空间,这时候读出来的是无效值
修正后的内存读取函数
先给你一个能可靠读取子进程内存的函数,解决上面的问题:
#include <sys/ptrace.h> #include <sys/wait.h> #include <errno.h> #include <stddef.h> #include <string.h> int read_child_memory(pid_t child, unsigned long long addr, void *buffer, size_t size) { size_t bytes_read = 0; char *buf = (char *)buffer; errno = 0; while (bytes_read < size) { // 每次读取一个字(适配64/32位系统) long data = ptrace(PTRACE_PEEKDATA, child, addr + bytes_read, NULL); if (data == -1 && errno != 0) { // 真的读取失败,返回错误 return -1; } // 计算本次要复制的字节数:取剩余字节和字长的最小值 size_t copy_size = (size - bytes_read) < sizeof(long) ? (size - bytes_read) : sizeof(long); memcpy(buf + bytes_read, &data, copy_size); bytes_read += copy_size; } return 0; }
这个函数的关键细节:
- 用
PTRACE_PEEKDATA读取子进程内存,循环处理所有字节 - 严格区分“读取到-1”和“读取失败”的情况,避免误判
- 自动处理剩余不足一个字的字节,保证数据完整
完整获取st_mode和st_size的步骤
接下来结合拦截fstat系统调用的逻辑,实现你要的功能:
#include <sys/stat.h> #include <sys/user.h> #include <sys/syscall.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> // 上面的read_child_memory函数放在这里 int main() { pid_t child = fork(); if (child == 0) { // 子进程:开启跟踪,然后执行ls ptrace(PTRACE_TRACEME, 0, NULL, NULL); execl("/bin/ls", "ls", NULL); perror("execl failed"); exit(EXIT_FAILURE); } else { int status; struct user_regs_struct regs; while (wait(&status) > 0) { if (WIFEXITED(status)) break; // 获取当前寄存器状态 if (ptrace(PTRACE_GETREGS, child, NULL, ®s) == -1) { perror("PTRACE_GETREGS failed"); continue; } // 检查是否是fstat系统调用(x86_64下的系统调用号是__NR_fstat) if (regs.orig_rax == __NR_fstat) { // 第一次触发是进入系统调用,我们要等系统调用完成后再读内存 ptrace(PTRACE_SYSCALL, child, NULL, NULL); wait(&status); // 再次获取寄存器,确认系统调用成功返回 ptrace(PTRACE_GETREGS, child, NULL, ®s); if (regs.rax == 0) { // stat结构体的用户空间地址存在rsi寄存器里(x86_64参数传递规则) unsigned long long stat_addr = regs.rsi; struct stat stat_buf; // 读取整个stat结构体到本地 if (read_child_memory(child, stat_addr, &stat_buf, sizeof(struct stat)) == 0) { printf("抓到fstat的字段:\n"); printf("st_mode: %o\n", stat_buf.st_mode); // 八进制输出权限更直观 printf("st_size: %lld bytes\n", stat_buf.st_size); } else { perror("读取stat结构体失败"); } } } // 继续跟踪下一个系统调用 ptrace(PTRACE_SYSCALL, child, NULL, NULL); } } return 0; }
关键注意点:
- 系统调用时机:必须等fstat系统调用完成(第二次触发
PTRACE_SYSCALL)后再读内存,这时候内核已经把数据写入到用户空间的stat结构体里了 - 寄存器架构适配:如果是32位系统,系统调用参数存在ebx、ecx等寄存器里,系统调用号也不一样,需要调整代码
- 结构体兼容性:直接用系统自带的
struct stat定义,不要自己手动写,避免因为系统差异导致字段偏移错误 - 权限问题:运行程序时需要足够的权限,比如不能跟踪root进程除非你也是root
额外优化:只读取需要的字段
如果不想读取整个结构体,可以直接读取目标字段的偏移地址,更高效:
// 读取st_mode(mode_t类型,通常2字节) mode_t st_mode; read_child_memory(child, stat_addr + offsetof(struct stat, st_mode), &st_mode, sizeof(st_mode)); // 读取st_size(off_t类型,64位下8字节) off_t st_size; read_child_memory(child, stat_addr + offsetof(struct stat, st_size), &st_size, sizeof(st_size));
用offsetof宏可以安全获取字段的偏移量,不用硬编码数值,适配不同系统的结构体定义。
内容的提问来源于stack exchange,提问作者g0blin
相关产品推荐
相关产品推荐

