David542
David542

Reputation: 110462

Writing the simplest assembly debugger

Let's say I have the following assembly code which I'd like to single-step through:

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

What would be the simplest way I could attach a ptrace to this to run this with single-stepping? I usually do this in gdb but curious how to manually do this in the crudest way possible (with no error handling or anything except the above case) to see what occurs behind the scenes. Any language is fine (assembly might be the best though).

Upvotes: 1

Views: 820

Answers (2)

Marco Bonelli
Marco Bonelli

Reputation: 69387

Here's a cleaner solution if you don't want to manually insert a debugger interrupt (int3) in the target program.

What you want to do is:

  1. First of all fork().
  2. CHILD: do ptrace(PTRACE_TRACEME) followed by kill(SIGSTOP). After this, exec*() whatever program you want to trace.
  3. PARENT: wait() for the child, then proceed with ptrace(PTRACE_SYSCALL) + wait(). Execution of the child will resume and immediately stop again when the kill syscall ends.
  4. PARENT: do another two ptrace(PTRACE_SYSCALL) + wait(), one will stop when the child is entering execve and one will stop right after execve is completed.
  5. PARENT: continue with ptrace(PTRACE_SINGLESTEP) as much as you want.
#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, 0, 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, pid, 0, 0);
        wait(&status);

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

        // Exit execve syscall
        ptrace(PTRACE_SYSCALL, pid, 0, 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, pid, 0, 0);
                        break;
                }

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

                hexdump_long(regs.rip, code);

                ptrace(PTRACE_SINGLESTEP, pid, 0, 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;
}

Test program (just exit(0)):

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

Result:

$ ./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.

NOTE: the hexdump_long() function only dumps a long, but x86 instructions can be longer or shorter. This is just an example. In order to compute the real sizes of x86 instructions you would need an instruction decoder (here is an example for x86 32bit).

Upvotes: 4

Jester
Jester

Reputation: 58812

For simplicity, I added an int3 which triggers a breakpoint trap. In real usage, you'd want to trace the exec call and put a software or hardware breakpoint at the entry address you parsed out of the ELF header. I have assembled the target program into a.out and it looks like:

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

A simple program demonstrating single stepping:

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

Sample output:

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

Upvotes: 6

Related Questions