大家好,这里是物联网心球。 gdb是Linux开发者经常会用到的调试工具,相信大部分读者都会使用gdb命令来调试程序。那么大家是否好奇:gdb为什么能够调试我们的程序呢?本文我们来一探究竟。
1.gdb是什么?
gdb是用于调试C、C++等语言编写的程序。它可以控制程序的执行,设置断点,查看和修改变量,查看堆栈信息,以及进行反汇编等操作。它还支持多线程和远程调试,帮助开发者快速定位和修复程序中的错误。
2.gdb工作原理
gdb是基于ptrace系统调用实现的调试器,通过ptrace系统调用来监控和控制目标进程的执行流程。我们把gdb程序称为调试器,把待调试的程序称为目标程序。gdb和目标程序为父子关系,即gdb进程为目标进程的父进程。我们通过一张图来直观了解,如图1所示。

图1 gdb工作原理
当用户执行gdb <可执行文件>命令时,gdb程序会调用fork和execl系统调用创建目标进程(子进程)。目标进程创建成功后,目标进程需要调用ptrace函数请求父进程(gdb程序)接管,如下:
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
gdb程序接管目标进程后,gdb就能够通过ptrace系统调用来控制目标进程。所以理解gdb工作原理的关键在于理解ptrace系统调用。 ptrace是Linux系统中一个功能强大的系统调用,主要用于进程跟踪和调试。它允许一个进程(称为跟踪者或调试器)监视和控制另一个进程(称为被跟踪者或目标进程)的执行,读取和修改其寄存器和内存,以及拦截系统调用。 ptrace函数原型如下:
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request op, pid_t pid,
void *addr, void *data);
参数说明:
- request:指定要执行的操作,常见操作见表1:

表1 request参数
- pid:目标进程的进程 ID。
- addr:目标进程的内存地址,用于读取或写入操作。
- data:根据不同的request操作,data的用途不同。
返回值:long类型,具体含义根据不同的request 操作而变化:
PTRACE_PEEKTEXT和PTRACE_PEEKDATA:返回值是读取到的内存内容。
其他操作:0 表示成功,-1表示失败。
关于ptrace的使用,我们会通过实际案例来讲解,这里先跳过,我们先来讲解另外一个比较重要的知识点:SIGTRAP信号。
3.SIGTRAP信号
SIGTRAP信号编号为5。当程序执行到调试器设置的断点时,操作系统会向程序发送SIGTRAP信号。调试器通过捕获这个信号来暂停程序的执行,程序暂停执行后,开发者就能够查看程序的状态,如变量值、调用堆栈等。
当目标进程没有被调试器追踪,内核对SIGTAP信号的默认处理方式为程序退出。反之,内核会暂停目标进程,调试器可通过waitpid系统调用捕捉目标进程的当前状态。

图2 SIGTRAP信号处理过程
如图2所示,目标进程执行至断点指令或目标进程单步执行时,Linux内核都会发送一个SIGTRAP信号至目标进程。SIGTRAP信号会插入目标进程的未决信号队列。SIGTRAP信号的处理方式和其他信号一样,当目标程序从内核态切换至用户态时,返回的过程中会调用do_signal信号处理函数,该函数将会轮询目标进程未决信号队列,从队列中取出未决信号并执行信号处理函数。SIGTRAP信号处理函数会暂停目标进程,调试器通过waitpid函数能够捕捉到目标进程停止运行。 SIGTRAP信号的作用是暂停目标进程运行,并通知调试器。这样调试器就能够基于SIGTRAP信号实现断点和单步执行等功能。
4.给目标程序打断点
打断点和单步执行是调试器的两个核心功能。为了让大家进一步了解gdb调试器,我们基于ptrace系统调用来实现这两个功能。简单来说,打断点就是将断点指令(0xCC)插入目标进程代码段指定位置。当目标进程执行至断点指令时,内核会发送SIGTRAP信号至目标进程未决信号队列。SIGTRAP信号会使目标进程暂停运行。此时,用户会观察到程序运行至断点处就停止运行。打断点实现原理如图3所示。

图3 ptrace给目标程序打断点
打断点的函数调用方式如下:
ptrace(PTRACE_POKETEXT, pid, /*代码地址*/, 0xCC)
request请求为PTRACE_POKETEXT,pid设置目标进程pid,代码地址为目标进程代码段指定地址,断点指令为0xCC。代码地址不能随便填写,需要指定一个有效的地址,通常可以直接使用函数名,或者通过PTRACE_PEEKTEXT先获取代码地址,再在当前位置插入断点指令。调试器调用ptrace函数打断点,内核会先获取目标进程task_struct结构,然后通过tast_struct结构找到目标进行虚拟地址空间代码段,再将断点指令写入代码段指定位置。打断点示例代码如下:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/ptrace.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<sys/user.h>
#include<elf.h>
#define INT3 0xCC // 断点指令
void breakpoint_func(){ //函数地址为断点地址
printf("Child continue\n");
}
int breakpoint_test(){
pid_t child = fork();
if (child == 0) { //子进程(目标进程)
//目标进程被调试器跟踪
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
//目标进程停止运行
raise(SIGSTOP);
printf("before breakpoint\n");
//执行至断点指令
breakpoint_func();
printf("after breakpoint\n");
return 0;
} else {
wait(NULL); //等待目标进程暂停
//获取断点地址
void *breakpoint_addr = (void *)breakpoint_func;
printf("sizeof(long)=%lu\n", sizeof(long));
//获取目标进程断点地址代码指令,64位系统一次获取8个字节
long orig = ptrace(PTRACE_PEEKTEXT, child, breakpoint_addr, NULL);
if (orig == -1) {
perror("ptrace PEEKTEXT failed");
return -1;
}
//采用或运算方式插入断点指令(0xCC)
if (ptrace(PTRACE_POKETEXT, child, breakpoint_addr, (orig & ~0xFF) | INT3) == -1) {
perror("ptrace POKETEXT failed");
return -1;
}
//目标进程继续运行
ptrace(PTRACE_CONT, child, NULL, NULL);
//处理SIGTRAP信号,等待断点指令执行
int status;
pid_t ret = waitpid(child, &status, WUNTRACED | WCONTINUED);
if (ret == -1) {
perror("waitpid failed");
return -1;
}
if (WIFSTOPPED(status)) {
printf("Child stopped by signal %d\n", WSTOPSIG(status));
}
//休眠2秒,便于观察断点是否生效
sleep(2);
//目标进程继续运行
ptrace(PTRACE_CONT, child, NULL, NULL);
wait(NULL);
printf("Child process exited\n");
}
return 0;
}
int main(int argc, char **argv){
breakpoint_test();
return 0;
}
5.目标程序单步执行
所谓单步执行,就是目标程序每执行一条指令就暂停运行,内核会发送SIGTRAP信号至目标进程未决信号队列。SIGTRAP信号的处理方式和打断点是一样的。单步执行实现原理如图4所示。

图4 ptrace单步执行
单步执行的函数调用方式如下:
ptrace(PTRACE_SINGLESTEP, child, 0, 0);
request请求为PTRACE_SINGLESTEP,pid设置目标进程pid。单步执行需要硬件支持。调试器调用ptrace函数控制目标进程单步执行,内核会设置目标进程对应的寄存器单步执行标志,如X86设置的是EFLAGS寄存器的X86_EFLAGS_TF标志。单步执行标志设置完后,目标进程执行一条指令就会暂停运行,内核会发送SIGTRAP信号至目标进程未决信号队列。单步执行示例代码如下:
int singlestep_test(){
pid_t child = fork();
if (child == 0) { //子进程(目标进程)
printf("Child process, PID: %d\n", getpid());
ptrace(PTRACE_TRACEME, 0, NULL, NULL);
raise(SIGSTOP);
printf("1111111111111111111\n");
printf("2222222222222222222\n");
printf("3333333333333333333\n");
printf("4444444444444444444\n");
} else {
while (1) { //循环控制目标进程单步执行
//每次单步执行间隔100毫秒,便于观察
usleep(100 * 1000);
//目标进程单步执行
ptrace(PTRACE_SINGLESTEP, child, 0, 0);
int status;
//处理SIGTRAP信号
waitpid(child, &status, WUNTRACED);
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
break;
}
}
}
}
int main(int argc, char **argv){
singlestep_test();
return 0;
}
