编写最简单的程序集调试器

问题描述

假设我有以下汇编代码,我想单步执行:

.globl _start
_start:
    nop
    mov $60,%eax
    syscall

ptrace附加到此以单步运行的最简单方法是什么?我通常在gdb中进行此操作,但很好奇如何以最粗略的方式手动执行此操作(除了上述情况外,没有错误处理或任何其他措施),以了解幕后情况。任何语言都可以(尽管汇编可能是最好的)。

解决方法

为简单起见,我添加了一个int3来触发断点陷阱。在实际使用中,您希望跟踪exec调用,并将软件或硬件断点放在从ELF标头解析出的入口地址处。我已经将目标程序组装到a.out中,它看起来像:

00000000004000d4 <_start>:
  4000d4:   cc                      int3   
  4000d5:   90                      nop
  4000d6:   b8 3c 00 00 00          mov    $0x3c,%eax
  4000db:   0f 05                   syscall 

一个演示单步执行的简单程序:

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

int main() {
    int pid;
    int status;
    if ((pid = fork()) == 0) {
        ptrace(PTRACE_TRACEME,NULL,NULL);
        execl("./a.out","a.out",NULL);
    }
    printf("child: %d\n",pid);
    waitpid(pid,&status,__WALL);
    ptrace(PTRACE_CONT,pid,NULL);
    while(1) {
        unsigned long rip;
        waitpid(pid,__WALL);
        if (WIFEXITED(status)) return 0;
        rip = ptrace(PTRACE_PEEKUSER,16*8,0);    // RIP is the 16th register in the PEEKUSER layout
        printf("RIP: %016lx opcode: %02x\n",rip,(unsigned char)ptrace(PTRACE_PEEKTEXT,NULL));
        ptrace(PTRACE_SINGLESTEP,NULL);
    }
}

样本输出:

$ ./singlestep 
child: 31254
RIP: 00000000004000d5 opcode: 90
RIP: 00000000004000d6 opcode: b8
RIP: 00000000004000db opcode: 0f
,

如果您不想在目标程序中手动插入调试器中断(int3),这是一个更干净的解决方案。

您想要做的是:

  1. 首先fork()
  2. 孩子:先做ptrace(PTRACE_TRACEME),然后再做kill(SIGSTOP)。之后,exec*()您要跟踪的任何程序。
  3. 父母:wait()代表孩子,然后继续ptrace(PTRACE_SYSCALL) + wait()kill系统调用结束时,将继续执行子进程并立即停止执行。
  4. 父母:再做两个ptrace(PTRACE_SYSCALL) + wait(),一个会在孩子进入execve时停止,另一个会在execve完成后停止。 >
  5. 父母:继续使用ptrace(PTRACE_SINGLESTEP)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>

void hexdump_long(unsigned long long addr,long data) {
        printf("[parent] 0x%016llx: ",addr);

        for (unsigned i = 0; i < 64; i += 8)
                printf("%02x ",((unsigned long)data >> i) & 0xff);
        putchar('\n');
}

int main(int argc,char **argv) {
        int status;
        pid_t pid;

        if ((pid = fork()) == 0) {
                char *child_argv[] = {"./prog",NULL};
                char *child_envp[] = {NULL};

                ptrace(PTRACE_TRACEME,0);
                kill(getpid(),SIGSTOP); // Don't use libc `raise` because it does more syscalls.

                execve(child_argv[0],child_argv,child_envp);
                perror("[child ] execve failed");
                return 1;
        }

        // Wait for child to stop
        wait(&status);

        // Exit kill syscall
        ptrace(PTRACE_SYSCALL,0);
        wait(&status);

        // Enter execve syscall
        ptrace(PTRACE_SYSCALL,0);
        wait(&status);

        // Exit execve syscall
        ptrace(PTRACE_SYSCALL,0);
        wait(&status);

        // Child is now running the new program,trace one step at a time.
        // Trace up to 1000 steps or until the program exits/receives a signal.
        unsigned steps = 1000;

        while(WIFSTOPPED(status)) {
                struct user_regs_struct regs;
                long code;

                steps--;
                if (steps == 0) {
                        ptrace(PTRACE_CONT,0);
                        break;
                }

                ptrace(PTRACE_GETREGS,&regs);
                code = ptrace(PTRACE_PEEKTEXT,regs.rip,0);

                hexdump_long(regs.rip,code);

                ptrace(PTRACE_SINGLESTEP,0);
                wait(&status);
        }

        if (steps == 0)
                wait(&status);

        if (WIFEXITED(status))
                printf("[parent] Child exited with status %d.\n",WEXITSTATUS(status));
        else
                puts("[parent] Child didn't exit,something else happened.");

        return 0;
}

测试程序(仅exit(0)):

_start:
    mov rdi,0x0
    mov rax,0x3c
    syscall

结果:

$ ./trace
[parent] 0x0000000000400080: bf 00 00 00 00 b8 3c 00
[parent] 0x0000000000400085: b8 3c 00 00 00 0f 05 00
[parent] 0x000000000040008a: 0f 05 00 00 00 00 00 00
[parent] Child exited with status 0.

注意hexdump_long()函数仅转储long,但是x86指令可以更长或更短。这只是一个例子。为了计算x86指令的实际大小,您需要一个指令解码器(here是x86 32位的示例)。