编写最简单的汇编调试器
Writing the simplest assembly debugger
假设我有以下要单步执行的汇编代码:
.globl _start
_start:
nop
mov , %eax
syscall
我可以通过单步将 ptrace
附加到 运行 的最简单方法是什么?我通常在 gdb
中执行此操作,但很好奇如何以最粗暴的方式手动执行此操作(没有错误处理或除上述情况外的任何内容)以查看幕后发生的情况。任何语言都可以(汇编可能是最好的)。
为了简单起见,我添加了一个 int3
来触发断点陷阱。在实际使用中,您需要跟踪 exec
调用并在您从 ELF header 中解析出的入口地址放置一个软件或硬件断点。我把目标程序组装成了a.out
,看起来像:
00000000004000d4 <_start>:
4000d4: cc int3
4000d5: 90 nop
4000d6: b8 3c 00 00 00 mov [=10=]x3c,%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, 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);
}
}
示例输出:
$ ./singlestep
child: 31254
RIP: 00000000004000d5 opcode: 90
RIP: 00000000004000d6 opcode: b8
RIP: 00000000004000db opcode: 0f
如果您不想在目标程序中手动插入调试器中断 (int3
),这里有一个更简洁的解决方案。
您想做的是:
- 首先
fork()
.
- CHILD:执行
ptrace(PTRACE_TRACEME)
,然后执行 kill(SIGSTOP)
。在此之后,exec*()
您想跟踪的任何程序。
- PARENT:
wait()
为child,然后继续ptrace(PTRACE_SYSCALL)
+wait()
。当 kill
系统调用结束时,child 的执行将恢复并立即再次停止。
- PARENT:再做两个
ptrace(PTRACE_SYSCALL)
+ wait()
,一个会在child进入execve
时停止,一个会在[后立即停止=24=]完成。
- PARENT: 继续
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, 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;
}
测试程序(只是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 位的示例)。
假设我有以下要单步执行的汇编代码:
.globl _start
_start:
nop
mov , %eax
syscall
我可以通过单步将 ptrace
附加到 运行 的最简单方法是什么?我通常在 gdb
中执行此操作,但很好奇如何以最粗暴的方式手动执行此操作(没有错误处理或除上述情况外的任何内容)以查看幕后发生的情况。任何语言都可以(汇编可能是最好的)。
为了简单起见,我添加了一个 int3
来触发断点陷阱。在实际使用中,您需要跟踪 exec
调用并在您从 ELF header 中解析出的入口地址放置一个软件或硬件断点。我把目标程序组装成了a.out
,看起来像:
00000000004000d4 <_start>:
4000d4: cc int3
4000d5: 90 nop
4000d6: b8 3c 00 00 00 mov [=10=]x3c,%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, 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);
}
}
示例输出:
$ ./singlestep
child: 31254
RIP: 00000000004000d5 opcode: 90
RIP: 00000000004000d6 opcode: b8
RIP: 00000000004000db opcode: 0f
如果您不想在目标程序中手动插入调试器中断 (int3
),这里有一个更简洁的解决方案。
您想做的是:
- 首先
fork()
. - CHILD:执行
ptrace(PTRACE_TRACEME)
,然后执行kill(SIGSTOP)
。在此之后,exec*()
您想跟踪的任何程序。 - PARENT:
wait()
为child,然后继续ptrace(PTRACE_SYSCALL)
+wait()
。当kill
系统调用结束时,child 的执行将恢复并立即再次停止。 - PARENT:再做两个
ptrace(PTRACE_SYSCALL)
+wait()
,一个会在child进入execve
时停止,一个会在[后立即停止=24=]完成。 - PARENT: 继续
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, 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;
}
测试程序(只是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 位的示例)。