通过 PTRACE_SINGLESTEP 实现单步调试

单步调试原理

单步调试可以让程序运行一条指令 / 语句后就停下。GDB 中常用的命令有 next, step, nexti, stepi。单步跟踪又常分为语句单步 (next, step) 和指令单步 (如 nexti, stepi)。

在 Linux 上,指令单步可以通过 ptrace 来实现。通过系统调用 ptrace (PTRACE_SINGLESTEP,pid,…) 可以使被调试的进程在每执行完一条指令后就触发一个 SIGTRAP 信号,让 GDB 运行。

测试用例

下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
int val;
int counter = 0;
pid_t pid = fork();
if (pid == 0) { // child process
execl("./a.out", "HelloWorld", NULL);
} else { // parent process
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
while (1) {
wait(&val);
if (WIFEXITED(val))
break;
counter++;
ptrace(PTRACE_SINGLESTEP, pid, NULL, NULL);
}
printf("Total Instruction number= %d\n", counter);
}
return 0;
}

运行结果:

1
2
3
lhx@ubuntu:~/test/test_ptrace/test_singlestep$ ./test-singlestep 
Hello World!
Total Instruction number= 180263

这段程序比较简单,子进程调用 execve 执行 HelloWorld, 而父进程则先调用 ptrace (PTRACE_ATTACH,pid,…) 建立与子进程的跟踪关系。然后调用 ptrace (PTRACE_SINGLESTEP, pid, …) 让子进程一步一停,以统计子进程一共执行了多少条指令 (你会发现一个简单的 HelloWorld 实际上也执行了好几万条指令才完成)。当然你也完全可以在这个时候查看 EIP 寄存器中存放的指令,或者某个变量的值,当然前提是你得知道这个变量在子进程内存镜像中的位置。
指令单步可以依靠硬件完成,如 x86 架构处理器支持单步模式 (通过设置 EFLAGS 寄存器的 TF 标志实现),每执行一条指令,就会产生一次异常 (在 Intel 80386 以上的处理器上还提供了 DRx 调试寄存器以用于软件调试)。也可以通过软件完成,即在每条指令后面都插入一条断点指令,这样每执行一条指令都会产生一次软中断。
语句单步基于指令单步实现,即 GDB 算好每条语句所对应的指令,从什么地方开始到什么地方结束。然后在结束的地方插入断点,或者指令单步一步一步的走到结束点,再进行处理。