Reputation: 110462
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
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:
fork()
.ptrace(PTRACE_TRACEME)
followed by kill(SIGSTOP)
. After this, exec*()
whatever program you want to trace.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.ptrace(PTRACE_SYSCALL)
+ wait()
, one will stop when the child is entering execve
and one will stop right after execve
is completed.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, ®s);
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
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