一、观察点观察点watchpoint是GDB中一种特殊的断点也可以理解为“数据断点”。普通断点通常是在程序执行到某一行代码或某个函数时停下来而观察点关注的是某个表达式的值是否发生变化。当被观察的表达式发生变化或者被读写访问时程序就会中断下来。观察点常用于排查“某个变量不知道在哪里被修改了”的问题。例如一个全局变量、结构体成员、指针指向的内存、数组元素等在程序运行过程中被意外修改这时就可以使用观察点让GDB在变量发生变化的那一刻停下来从而定位是哪一行代码修改了它。1.1 观察点类型GDB 中的观察点分为两类1、硬件观察点hardware watchpoint硬件观察点依赖CPU的调试寄存器来监控内存访问。当变量被修改时CPU会自动触发调试异常GDB就能在修改变量的位置停下来。硬件观察点的优点是效率高对程序运行性能影响很小而且通常能准确停在真正修改变量的那条指令附近。例如设置观察点后GDB 可能会提示(gdb) watch gdata Hardware watchpoint 2: gdata这表示当前观察点是硬件观察点。2、软件观察点software watchpoint软件观察点则是GDB通过单步执行程序并在每一步之后检查表达式的值是否发生变化来实现的。因此软件观察点会明显降低程序运行速度尤其是在循环、递归、多线程程序中会更加明显。软件观察点是一种类似轮询的方式他不是CPU自动通知GDB变量变了而是GDB反复检查变量有没有变。大多数情况下GDB默认使用的都是硬件观察点不过需要注意虽然硬件观察点效率高但它并不是无限制使用的。硬件观察点依赖CPU的调试寄存器而调试寄存器的数量是有限的所以同时能够设置的硬件观察点数量也有限。不同CPU架构支持的数量可能不同通常只能设置少量几个硬件观察点。GDB默认会尽量使用硬件观察点。如果想查看当前是否允许使用硬件观察点可以使用(gdb) show can-use-hw-watchpoints如果想强制关闭硬件观察点让 GDB 使用软件观察点可以使用(gdb) set can-use-hw-watchpoints 0如果想重新开启硬件观察点可以使用(gdb) set can-use-hw-watchpoints 1一般情况下不需要手动修改这个选项保持默认即可。只有在研究软件观察点行为或者某些调试环境下硬件观察点异常时才需要手动设置。1.2 观察点常用命令命令作用watch expr写观察点当表达式的值发生变化时中断rwatch expr读观察点当表达式被读取时中断awatch expr访问观察点当表达式被读取或写入时中断i watchpoints查看当前观察点信息也可以使用i b命令观察点和断点显示在同一个列表当中delete num删除指定编号的观察点disable num禁用指定编号的观察点enable num启用指定编号的观察点其中watch最常用通常用来定位变量被修改的位置。rwatch和awatch使用频率相对较低主要用于排查某块数据在哪里被读取或访问。在实际使用中观察点常见于以下几类场景1、定位全局变量被谁修改当某个全局变量的值异常但是不清楚是哪段代码修改的可以使用(gdb) watch gdata程序运行过程中只要gdata的值发生变化GDB就会停下来并显示旧值和新值。2、定位结构体成员被谁修改例如有如下结构体struct Node { int id; int value; }; struct Node node;如果想观察node.value是否被修改可以使用(gdb) watch node.value如果是结构体指针struct Node *pnode;则可以使用(gdb) watch pnode-value3、定位数组元素被谁修改如果只关心数组中的某一个元素可以直接观察指定下标(gdb) watch arr[3]这样只有arr[3]的值发生变化时才会中断修改数组中的其他元素不会触发这个观察点。4、定位多线程中的共享变量修改在多线程程序中多个线程可能都会修改同一个共享变量。如果想知道到底是哪个线程修改了变量可以设置观察点(gdb) watch gdata当gdata被修改时程序会停下来。此时可以使用(gdb) info threads (gdb) bt查看当前是哪个线程触发了观察点以及对应的函数调用栈。如果只想观察某一个线程对变量的修改可以使用(gdb) watch gdata thread 3这表示只有GDB中编号为3的线程修改gdata时才会触发观察点。线程编号可以通过下面命令查看(gdb) info threads5、观察变量计算式观察点不仅可以观察单个变量也可以观察由多个变量组成的计算表达式。当表达式的计算结果发生变化时程序就会中断下来。例如(gdb) watch a b表示观察表达式a b的值。当a或b的值发生变化并且导致a b的计算结果发生变化时GDB就会停下来。示例代码#include stdio.h int main() { int a 1; int b 2; a 3; // a b 从 3 变成 5会触发观察点 b 4; // a b 从 5 变成 7会触发观察点 printf(a b %d\n, a b); return 0; }调试时可以这样设置(gdb) r (gdb) watch a b (gdb) continue当a b的结果发生变化时GDB会中断下来并显示表达式的旧值和新值。除了普通计算表达式也可以观察条件表达式(gdb) watch a b 10这表示观察表达式a b 10的结果是否发生变化。由于这是一个布尔表达式所以结果只有两种真或假。需要注意watch a b 10并不是表示“只要a b 10就停下来”而是表示“当a b 10这个表达式的结果发生变化时停下来”。二、捕获点捕获点catchpoint也是一种特殊的断点。普通断点通常是在程序执行到某一行代码或某个函数时中断观察点是在某个表达式的值发生变化时中断而捕获点关注的是某类事件是否发生。捕获点的命令语法为catch event含义是当程序运行过程中捕获到指定的event事件时程序就会中断下来。捕获点和普通断点、观察点一样也会被 GDB 分配编号因此可以使用info breakpoints、delete、disable、enable等命令进行管理。2.1 常用捕获点命令常见的捕获点事件如下命令作用catch throw当C 程序抛出异常时中断catch catch当C 程序捕获异常时中断catch rethrow当C 程序重新抛出异常时中断catch syscall当程序执行系统调用时中断catch fork当程序调用fork创建子进程时中断catch vfork当程序调用vfork创建子进程时中断catch exec当程序调用exec执行新程序时中断catch load当程序加载动态库时中断catch unload当程序卸载动态库时中断其中C 异常调试中比较常用的是catch throw catch catch catch rethrow进程调试中比较常用的是catch fork catch exec系统调用调试中比较常用的是catch syscall2.2 捕获点示例在这一小节中将介绍几种捕获点的使用场景包括捕获C异常、捕获系统调用、捕获进程创建、捕获程序替换以及捕获动态库加载和卸载等。1、捕获C异常在C程序中如果程序抛出了异常但是不清楚异常是在哪里抛出的可以使用catch throw捕获异常抛出事件或者捕获对应的异常被处理的位置catch catch。示例代码如下#include iostream #include stdexcept void func() { throw std::runtime_error(error happened); } int main() { try { func(); } catch (const std::exception e) { std::cout catch exception: e.what() std::endl; } return 0; }在对应系统中编译出对应的可执行文件并进行GDB调试可以看见执行了catch throw命令之后让程序继续执行当func函数中执行完throw命令之后GDB会对其进行捕获在该捕获处执行bt命令查看对应的函数栈调用情况切换到1号栈帧中就可以看见抛出异常的具体代码在什么地方2、捕获系统调用系统调用是用户程序请求内核服务的接口例如文件读写、进程创建、网络通信等操作最终都会通过系统调用完成。如果想捕获程序中发生的系统调用可以使用(gdb) catch syscall这会捕获所有系统调用。不过程序运行过程中系统调用非常频繁如果捕获所有系统调用程序可能会频繁中断因此实际调试中通常会指定具体的系统调用。例如捕获文件打开相关的系统调用(gdb) catch syscall openat捕获读文件操作(gdb) catch syscall read捕获写文件操作(gdb) catch syscall write小技巧在捕获linux系统调度的read、write、open、close这些系统调度函数的时候运行程序之后在linux的库中也会有一些调度函数这时候如果我们想跳过main函数之后前的系统调度可以先在main函数加上断点r命令执行到main函数之后再使用catch命令示例代码如下#include stdio.h int main() { FILE *fp fopen(test.txt, r); if (fp NULL) { perror(fopen); return 1; } fclose(fp); return 0; }对上述程序进行系统调试在对应的系统调用处中断下来之后查看对应的帧栈情况。因为这里的程序中使用的是fopen标准IO函数非系统IO函数open所以使用的捕获接口是openat也可以使用openat2新标准。如果是使用的系统IO中的open函数打开对应的文件描述符这种情况下需要使用syscall open。3、捕获进程创建在多进程程序中如果想知道程序什么时候创建了子进程可以使用catch fork或catch vfork。示例代码#include stdio.h #include unistd.h int main() { pid_t pid fork(); if (pid 0) { printf(child process\n); } else { printf(parent process\n); } return 0; }当程序执行到fork()创建子进程时GDB会中断下来。如果程序使用的是vfork()可以使用(gdb) catch vfork捕获进程创建事件后可以使用(gdb) bt查看是哪个函数调用了fork()或vfork()。4、捕获程序替换在 Linux 中exec系列函数用于将当前进程替换成另一个程序。例如当前程序调用execl()执行/bin/ls那么当前进程的代码和数据会被新的程序替换。如果想捕获这种程序替换事件可以使用(gdb) catch exec示例代码#include unistd.h int main() { execl(/bin/ls, ls, NULL); return 0; }当程序执行到execl()并准备替换为/bin/ls时GDB会中断下来。这个命令适合用来调试启动脚本、父子进程、程序跳转执行其他可执行文件等场景。5、捕获动态库加载和卸载如果程序使用了动态库或者程序运行过程中会动态加载插件可以使用catch load和catch unload捕获动态库的加载和卸载事件。捕获任意动态库加载(gdb) catch load捕获指定动态库加载(gdb) catch load libm.so捕获任意动态库卸载(gdb) catch unload捕获指定动态库卸载(gdb) catch unload libm.so这类捕获点常用于调试动态库初始化、插件加载、共享库符号解析等问题。例如一个程序通过dlopen()动态加载某个动态库.so文件如果想知道动态库什么时候被加载可以这样设置(gdb) catch load (gdb) run