GDB断点管理

GDB断点管理

一、断点管理

1.1 添加断点

在GDB中,添加断点使用break命令,break可以简写为b


1、按照文件名与行号添加断点

(gdb) b src.cpp:10

上面命令表示在src.cpp文件的第10行添加断点。

如果当前GDB已经定位或者运行到某个源文件,想要直接在该源文件中添加断点,可以直接使用b+行号的方式:

(gdb) b 10

这种方式表示在当前源文件的第 10 行添加断点。


2、按照函数名添加断点

如果希望程序进入某个函数时暂停,可以直接使用函数名添加断点:

(gdb) b function_name

表示在function_name函数入口处添加断点。当程序调用function_name函数时,会在函数开始处暂停。对于C++成员函数,也可以使用类名加函数名的方式添加断点:

(gdb) b ClassName::functionName

当使用函数名进行添加断点的时候,需要注意在cpp中函数重载的情况,此时会在多个相同函数名的地方都设置上断点。

1.2 正则表达式断点

除了使用函数名或文件行号添加断点外,GDB还支持使用正则表达式批量添加断点,对应命令是rbreak,可以简写为rb

rb [正则表达式]

例如代码中有三个类,包含person、student、teacher,三个类中都有含有work名称成员函数,想要对这些work函数都打上断点

例如,有如下 C++ 代码:

#include <iostream>
using namespace std;class Person
{
public:void work(){cout << "Person work" << endl;}
};class Student
{
public:void work(){cout << "Student work" << endl;}
};class Teacher
{
public:void work(){cout << "Teacher work" << endl;}
};int main()
{Person p;Student s;Teacher t;p.work();s.work();t.work();return 0;
}

使用命令:

rb work

可以直接在所有包含work名字的函数上打上断点。

1.3 条件断点

条件断点是普通断点的增强形式。普通断点只要程序运行到指定位置就会暂停,而条件断点只有在满足指定条件时才会暂停。条件断点常用于循环调试场景。例如,程序循环执行很多次,但我们只关心某个变量等于某个特定值时的状态。如果每次循环都停下来,会非常麻烦,这时就可以使用条件断点。

条件断点的基本格式如下:

b src.cpp:num if [condition]

例如下面是一个测试程序:

#include <iostream>
using namespace std;int main()
{for (int i = 0; i < 10; i++){int value = i * 2;cout << "i = " << i << ", value = " << value << endl;}return 0;
}

如果想在 i == 5 时停下来,可以在循环体内部设置条件断点:

(gdb) b main.cpp:8 if i == 5

然后运行程序:

(gdb) r

程序不会在每一次循环都暂停,而是等到i的值为5时才暂停。暂停后可以查看变量值:

(gdb) p i
(gdb) p value

输出可能类似:

$1 = 5
$2 = 10

条件表达式中也可以使用其他比较关系,例如:

(gdb) b main.cpp:8 if i > 5

表示当 i > 5 时暂停。

(gdb) b main.cpp:8 if value == 12

表示当 value 的值等于 12 时暂停。

对于指针变量,也可以判断是否为空:

(gdb) b main.cpp:20 if ptr == nullptr

或者:

(gdb) b main.cpp:20 if ptr == 0

条件断点也可以作用在函数上:

(gdb) b add if a == 10

表示只有调用 add 函数,并且参数 a 的值等于 10 时,才会在 add 函数入口处暂停。

如果已经设置了普通断点,也可以后续给断点添加条件。首先查看断点编号:

(gdb) i b

假设断点编号为1,可以使用condition命令添加条件:

(gdb) condition 1 i == 5

如果想取消该断点的条件,可以执行:

(gdb) condition 1

这样断点 1 就会恢复为普通断点。

1.4 临时断点

临时断点是一种只生效一次的断点。当程序第一次运行到该断点位置并暂停后,这个断点就会自动删除,后续程序再次运行到同一位置时不会再暂停。

临时断点和普通断点的格式完全相同,唯一不同的就是临时断点只作用一次。

tb src.cpp:num

1.5 查看断点信息

在GDB中,可以使用info breakpoints(i b)命令查看当前已经设置的断点信息

该命令会列出当前所有断点的信息,包括断点编号、断点类型、是否启用、断点地址以及断点所在位置。其中各字段含义如下:

字段 含义
Num 断点编号,后续删除、禁用、启用断点时会用到
Type 断点类型,例如 breakpoint 表示普通断点
Disp 断点命中后的处理方式
Enb 断点是否启用,y 表示启用,n 表示禁用
Address 断点对应的程序地址
What 断点所在的函数、文件和行号

其中,Disp 字段比较常见的值有:

Disp 含义
keep 断点命中后继续保留,普通断点通常是这个值
del 断点命中后自动删除,临时断点通常是这个值
dis 断点命中后自动禁用

如果只想查看某一个断点的信息,可以在命令后面加上断点编号:

i b [id]

1.6 删除断点/禁用断点

在调试过程中,随着断点数量增多,有些断点可能不再需要。此时可以选择删除断点,也可以选择临时禁用断点。

删除一个断点使用delete(d)命令进行删除,只使用delete命令,不跟断点号,是删除所有断点

d [id]

禁用某一个断点是用disable命令进行禁用

disable [id]
enable [id]

二、断点处预设置命令

在使用GDB调试程序时,程序停在断点之后,调试者经常会重复执行一些命令,例如查看变量值、打印链表节点、查看调用栈等。如果每次命中断点后都手动输入这些命令,会比较麻烦。GDB提供了断点预设置命令功能,可以为某个断点提前绑定一组命令。当程序运行到该断点并暂停时,GDB会自动执行这些命令,从而避免重复操作。

断点预设置命令使用commands命令,基本格式如下:

(gdb) commands [断点编号]
> 命令1
> 命令2
> 命令3
> end

其中,end表示命令列表结束。例如:

(gdb) commands 1
> p *curr
> p prev
> end

表示当编号为1的断点被命中时,自动执行:

p *curr
p prev

下面举一个具体的示例:

#include <iostream>
using namespace std;int main()
{for (int i = 0; i < 5; i++){int value = i * 10;cout << "loop" << endl;}return 0;
}

在循环体中设置断点:

(gdb) b main.cpp:8

假设这个断点编号为1,可以为它设置预执行命令:

(gdb) commands 1
> p i
> p value
> end

然后运行程序:

(gdb) run

当程序每次运行到该断点时,GDB都会自动打印ivalue的值。

不过这种写法有一个特点:程序每次命中断点后仍然会停下来,需要手动执行continue才会继续运行。如果只是想把断点当作“打印日志”来使用,不希望程序每次都停下来,可以在命令列表最后添加continue

(gdb) commands 1
> p i
> p value
> continue
> end

这样每次命中断点后,GDB会自动打印变量,然后继续运行程序。

需要注意的是,默认情况下,GDB每次命中断点时都会显示类似下面的信息:

Breakpoint 1, main () at main.cpp:8

如果不想显示这些断点提示信息,可以在命令列表开头添加silent

(gdb) commands 1
> silent
> p i
> p value
> continue
> end

这样程序命中断点时,就不会输出默认的断点提示信息,只会执行我们自定义的命令。

这里有一个小技巧:

GDB 中也提供了printf命令,用于格式化打印信息。它的用法和C语言中的printf类似,但是格式上有一点不同:GDB中的printf不需要写括号。

C 语言中的写法是:

printf("prev node data is %d\n", prev_node->data);

GDB 中应该写成:

(gdb) printf "prev node data is %d\n", prev_node->data

注意,GDB 的 printf 格式如下:

(gdb) printf "格式字符串", 参数1, 参数2

例如:

(gdb) printf "i = %d, value = %d\n", i, value

在断点预设置命令中,printf很适合用于循环打印:

(gdb) commands 1
> silent
> printf "i = %d, value = %d\n", i, value
> continue
> end

这样程序运行时会自动输出:

i = 0, value = 0
i = 1, value = 10
i = 2, value = 20
i = 3, value = 30
i = 4, value = 40

这种方式相当于在不修改源代码的情况下,临时给程序添加了一些调试打印。

如果想清除某个断点已经设置好的命令,可以重新执行commands,然后直接用end结束,不添加任何命令。

例如,清除编号为1的断点命令:

(gdb) commands 1
> end

这样断点 1 仍然存在,但是它命中后不会再自动执行之前设置的命令。

三、保存断点信息文件

在使用GDB调试程序时,我们可能已经设置好了很多断点,例如普通断点、条件断点、临时断点,甚至还为某些断点设置了预定义命令。如果此时需要处理其他事情,必须先中断当前调试工作,那么直接退出GDB会导致这些断点信息丢失。

为了避免下次重新调试时再次手动添加断点,GDB提供了保存断点信息的功能,可以将当前断点配置保存到文件中。下次重新进入GDB后,再将该文件导入,就可以恢复之前设置好的断点。

3.1 保存断点信息

保存断点信息使用save breakpoints命令,基本格式如下:

(gdb) save breakpoints 文件名

例如:

(gdb) save breakpoints breakpoints.gdb

该命令会将当前已经设置的断点信息保存到breakpoints.gdb文件中。

保存的内容不仅包括普通断点,也包括条件断点以及断点处设置的预定义命令。例如,假设当前设置了如下断点:

(gdb) b main.cpp:10
(gdb) b main.cpp:20 if i == 5(gdb) commands 2
> silent
> printf "i = %d\n", i
> continue
> end

然后执行:

(gdb) save breakpoints breakpoints.gdb

GDB 就会把这些断点配置保存到breakpoints.gdb文件中。

3.2 查看导入保存的断点文件

save breakpoints 生成的文件本质上是一个GDB命令脚本,里面保存的是重新创建断点所需的GDB命令。

可以直接使用 cat 查看:

cat breakpoints.gdb

文件内容可能类似:

break main.cpp:10
break main.cpp:20 if i == 5
commandssilentprintf "i = %d\n", icontinue
end

因此,这个文件不仅可以用于恢复断点,也可以根据需要手动编辑。例如,可以删除不需要的断点,或者修改某个断点的位置。

下次重新进入 GDB 后,可以使用source命令导入之前保存的断点文件。

例如先启动GDB:

gdb ./main

然后在 GDB 中执行:

(gdb) source breakpoints.gdb

执行完成后,GDB会读取breakpoints.gdb文件中的命令,并重新添加之前保存的断点。

可以使用下面命令查看断点是否恢复成功:

(gdb) i b