使用 ptrace 动态获取以字节为单位的指令大小

Dynamically get instruction size in bytes using ptrace

我正在使用 ptrace 来跟踪流程并监控其行为。在某些时候,我想在点击下一条指令之前获得下一个 rip 地址。事实上,我想在 callq 指令之后调用获取指令的地址。有几种不同的此类指令(近、远、相对、绝对等),它们的长度并不都相同。

有没有一种方法可以在检索到指令后使用 ptrace 获取指令的字节大小。类似于以下内容:

int ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0);                    // some addr where ip points
long isntruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL);           // e8 ae 72 f8 ff (relative call)
printf("Instruction is %d bytes", get_instruction_size(instruction));  // Instrction is 5 bytes

我猜测实现 get_instruction_size 的一种方法是获取指令的操作码(前 1 或 2 个字节),然后根据 x86 architecture/manual 确定它应该有多长.但我觉得会有很多特殊情况需要考虑,并且需要大量阅读才能找到价值 + 这将从一种 CPU 架构变为另一种架构。另一方面,动态查找大小似乎更方便。我还没有找到答案。

------ 编辑 ------

尝试在调用后立即从 rsp 检索 return 值:

#define M_OFFSETOF(STRUCT, ELEMENT) \
        (unsigned long) &((STRUCT *)NULL)->ELEMENT;
...
ipoffs = M_OFFSETOF(struct user, regs.rip);
spoffs = M_OFFSETOF(struct user, regs.rsp);
...
while(1) {
    // exec one instruction
    if(ptrace(PTRACE_SINGLESTEP, t_pid, 0, signo) < 0){
        perror("ptrace single step error\n");
        exit(EXIT_FAILURE);
    }
    ip = ptrace(PTRACE_PEEKUSER, t_pid, ipoffs, 0);
    full_instruction = ptrace(PTRACE_PEEKTEXT, t_pid, ip, NULL);
    opcode = (unsigned)0xFF & full_instruction;
    if(opcode == ADDR32){
        opcode = ((unsigned)0xFF00 & full_instruction) >> 8;
    }
    if(call_found){
        sp = ptrace(PTRACE_PEEKUSER, t_pid, spoffs, 0);
        // print sp ...
        call_found = false;
    }
    if(opcode == CALL)
       call_found = true;
}

ptrace 在内核中没有反汇编程序1,硬件本身不会告诉你这一点,直到 call 指令执行后。

如果您可以等到指令执行之后,最好的选择可能是 PTRACE_SINGLESTEP 然后读取压入堆栈的 return 地址 call。 (ESP/RSP会指向它2)。


另一种选择当然是自己解码(包括任何可能用于填充的前缀,例如 ld 将 6 字节 call [got_entry] 放宽为 1+5 字节 addr32 call rel32).使用反汇编程序库,或者通过扫描前缀直到到达 call 操作码之一,然后您可以从它(E8 call rel32)或从解码 ModRM 字节获得间接长度FF /2 call [r/m32]。 (https://www.felixcloutier.com/x86/call).


如果你想移植到非 x86 ISA,许多人使用 link 寄存器而不是压入 return 地址,所以它不相同;您不能只是一般地取消引用具有 uintptr_t 宽度的堆栈指针。

并且其中许多 ISA 具有固定的指令宽度,因此您可以只向前执行一条指令,而不是单步执行和读取寄存器。 (尽管许多支持使用 2 或 4 字节指令的紧凑编码,例如 ARM Thumb、MIPS 和 RISC-V)。

一些 ISA 上还有其他问题,例如 MIPS 有一个分支延迟槽,所以 return 地址实际上在 next 指令之后 [=20] =].


脚注 1:(有趣的事实:ARM Linux 内核曾经有一个支持 某些 指令的反汇编程序,因此它可以为您模拟单步执行,但是 that hack was removed).

脚注 2:即使对于带有 far 调用的手写 asm,CS:[ER]IP return 地址的偏移部分也会位于最低地址,即指向ESP/RSP。当然,远调用使用不同的操作码,所以你可以单独对待它们或忽略它们。

我不确定前缀是否有可能以某种方式覆盖大小,从而使 call 推送不同大小的 return 地址。 (例如 32 位模式下的 16 位)。可能不是,即使是这样,它也只是恶意二进制文件试图故意欺骗您的跟踪器的一个问题。即使是 GNU/Linux 的手写 asm 也不太可能出于任何正常原因这样做。