gdb断点是调试的重要手段,常用的断点有一段断点(break),观察断点(watch)、捕捉断点(catch)和条件断点(condition)。除了这些断点之外,还有单步调试等手段常用来做调试。
gdb设置断点-高级应用
在上一节中有提到简单的断点设置方法,即直接通过break number
设置断点! 本节将在此基础上深入研究。
同样的还是基于一段简单的程序进行学习!
#include<stdio.h>
int main(int argc,char* argv[])
{
int num = 1;
while(num<100)
{
num *= 2;
}
printf("num=%d",num);
return 0;
}
并编译成可调试的可执行程序:gcc -g main.c -o main.out
通过main.out 进入gdb调试器
break常用命令
(gdb) break location // 在location位置暂停运行
(gdb) break location if <condition> // 如果条件成立, 在location位置暂停运行
上述location参数列表如下:
location 的值 |
含 义 |
linenum |
linenum 是一个整数,表示要打断点处代码的行号。要知道,程序中各行代码都有对应的行号,可通过执行 l(小写的 L)命令看到。 |
filename:linenum |
filename 表示源程序文件名;linenum 为整数,表示具体行数。整体的意思是在指令文件 filename 中的第 linenum 行打断点。 |
+ offset - offset |
offset 为整数(假设值为 2),+offset 表示以当前程序暂停位置(例如第 4 行)为准,向后数 offset 行处(第 6 行)打断点;-offset 表示以当前程序暂停位置为准,向前数 offset 行处(第 2 行)打断点。 |
function |
function 表示程序中包含的函数的函数名,即 break 命令会在该函数内部的开头位置打断点,程序会执行到该函数第一行代码处暂停。 |
filename:function |
filename 表示远程文件名;function 表示程序中函数的函数名。整体的意思是在指定文件 filename 中 function 函数的开头位置打断点。 |
接下来看看实际的使用:
调试的时候首先用list看看程序的源代码,一来可以判断file有没有加载进来,而来可以了解整体程序结构:
(gdb) list
1 #include<stdio.h>
2 int main(int argc,char* argv[])
3 {
4 int num = 1;
5 while(num<100)
6 {
7 num *= 2;
8 }
9 printf("num=%d",num);
10 return 0;
(gdb)
然后通过打断点查看程序运行状态
(gdb) break 4 <---- 在第四行设置断点
Breakpoint 1 at 0x1144: file main.c, line 4.
(gdb) run <---- 开始运行,在第一个断点处停止
Starting program: /root/c/c.biancheng.com/gdb_demo/break_demo/main.out
Breakpoint 1, main (argc=1, argv=0x7fffffffeb68) at main.c:4
4 int num = 1;
(gdb) break +1 <---- 在当前断点的基础上+1 设置断点(4+1)
Breakpoint 2 at 0x55555555514b: file main.c, line 5.
(gdb) continue <---- 继续运行,下一个断点停止
Continuing.
Breakpoint 2, main (argc=1, argv=0x7fffffffeb68) at main.c:5
5 while(num<100)
(gdb) break 7 if num > 10 <---- 当num大于10 的时候在第7行设置断点
Breakpoint 3 at 0x55555555514d: file main.c, line 7.
(gdb) continue <---- 继续运行,下一个断点停止
Continuing.
Breakpoint 3, main (argc=1, argv=0x7fffffffeb68) at main.c:7
7 num *= 2;
(gdb) print num <---- 打印此时num的值,确实大于10了
$1 = 16
(gdb)
gdb tbreak命令
tbreak命令是对break命令的封装,与break命令不同的是,tbreak打出的断点是一次性的,也就是暂停运行一次后,此断点消失,这个断点常用在循环中。来看看使用:
基于上面break 7 if num > 10
的断点继续看,我们先在此基础上continue,继续运行看看:
(gdb) break 7 if num > 10
Breakpoint 3 at 0x55555555514d: file main.c, line 7.
(gdb) continue
Continuing.
Breakpoint 3, main (argc=1, argv=0x7fffffffeb68) at main.c:7
7 num *= 2;
(gdb) print num
$3 = 16
(gdb) continue <---- 在上面实验的基础上continue,继续运行
Continuing.
Breakpoint 3, main (argc=1, argv=0x7fffffffeb68) at main.c:7
7 num *= 2; <---- 断点停止
(gdb) print num <---- 打印num的值
$4 = 32
(gdb)
然后我们同样的情况使用tbreak 打个断点看看
(gdb) tbreak 7 if num > 10 <---- 使用tbreak
Temporary breakpoint 1 at 0x114d: file main.c, line 7.
(gdb) run
Starting program: /root/c/c.biancheng.com/gdb_demo/break_demo/main.out
Temporary breakpoint 1, main (argc=1, argv=0x7fffffffeb68) at main.c:7
7 num *= 2; <---- 断点停止
(gdb) print num <---- 首次打印num的值
$1 = 16
(gdb) continue <---- 继续运行
Continuing.
num=128[Inferior 1 (process 37219) exited normally] <---- 程序直接结束了!num的值是128
(gdb)
通过上面的实验发现,tbreak打的断点只生效了一次。
gdb rbreak命令
rbreak命令也是基于break的封装,其作用是,只会在指定的函数处打断点,并且断点一直生效,语法格式为:
rbreak regex
regex表示正则表达式,也就是说,我们不需要精准的输入完整的函数名,只要正则能匹配就可以打断点。
很好理解,懒得写函数了!
有时候需要持续监控某一个变量的变化,这个时候打break这种断点很显然已经不合适了,所以gdb还提供了其他的方式。gdb共有三种断点方式:break(普通断点)、观察断点和捕捉断点。接下来我们看看观察断点:
gdb设置观察断点-watch
watch命令的功能是:只有当被监控变量发生变化的时候,程序才会暂停。和 watch 命令功能相似的,还有 rwatch 和 awatch 命令
- rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
- awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。
还是以之前break的程序作为例子来学习watch
(gdb) break 5
Breakpoint 1 at 0x114b: file main.c, line 5.
(gdb) run
Starting program: /root/c/c.biancheng.com/gdb_demo/break_demo/main.out
Breakpoint 1, main (argc=1, argv=0x7fffffffeb68) at main.c:5
5 while(num<100)
(gdb) print num
$1 = 1 <---- 此时num的值是1
(gdb) watch num <---- 使用watch为num打观察断点
Hardware watchpoint 2: num
(gdb) continue <---- 继续运行
Continuing.
Hardware watchpoint 2: num
Old value = 1
New value = 2 <---- num 值发生了改变
main (argc=1, argv=0x7fffffffeb68) at main.c:5
5 while(num<100) <---- 暂停了
(gdb) continue <---- 继续运行
Continuing.
Hardware watchpoint 2: num
Old value = 2
New value = 4 <---- num 值再次发生了改变
main (argc=1, argv=0x7fffffffeb68) at main.c:5
5 while(num<100) <---- 暂停了
(gdb)
上面的实验说明,watch监控的变量值一旦发生变化,程序立刻暂停。
值得一提的是,对于使用 watch(rwatch、awatch)命令监控 C、C++ 程序中变量或者表达式的值,有以下几点需要注意:
- 当监控的变量(表达式)为局部变量(表达式)时,一旦局部变量(表达式)失效,则监控操作也随即失效;
- 如果监控的是一个指针变量(例如 p),则 watch p 和 watch p 是有区别的,前者监控的是 p 所指数据的变化情况,而后者监控的是 p 指针本身有没有改变指向;
- 这 3 个监控命令还可以用于监控数组中元素值的变化情况,例如对于 a[10] 这个数组,watch a 表示只要 a 数组中存储的数据发生改变,程序就会停止执行。
gdb设置捕捉断点-catch
捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行,其语法为:
catch event
event 事件 |
含 义 |
throw [exception] |
当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
catch [exception] |
当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
load [regexp] unload [regexp] |
其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。 |
注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:
- 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
- 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
- catch 无法捕获以交互方式引发的异常。
如同 break 命令和 tbreak 命令的关系一样(前者的断点是永久的,后者是一次性的),catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。
使用下面这个程序看看catch的使用:
#include <iostream>
using namespace std;
int main(){
int num = 1;
while(num <= 5){
try{
throw 100;
}catch(int e){
num++;
cout << "next" << endl;
}
}
cout << "over" << endl;
return 0;
}
// 编译
g++ -g main.cpp -o main.out
root@klelee:~/c/c.biancheng.com/gdb_demo/catch_demo# gdb main.out -q
Reading symbols from main.out...
(gdb) catch throw int <---- 指定捕获“throw int”事件
Catchpoint 1 (throw)
(gdb) run <---- 开始运行
Starting program: /root/c/c.biancheng.com/gdb_demo/catch_demo/main.out
Catchpoint 1 (exception thrown), 0x00007ffff7e98c2e in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6 <---- 异常停止
(gdb) up <---- 查看上一条执行的代码
#1 0x00005555555551ef in main () at main.cpp:7
7 throw 100;
(gdb) continue <---- 继续执行
Continuing.
next
Catchpoint 1 (exception thrown), 0x00007ffff7e98c2e in __cxa_throw () from /lib/x86_64-linux-gnu/libstdc++.so.6 <---- 异常停止
(gdb) up <---- 查看上一条执行的代码
#1 0x00005555555551ef in main () at main.cpp:7
7 throw 100;
(gdb)
如上所示,借助 catch 命令设置了一个捕获断点,该断点用于监控 throw int 事件,只要发生程序就会暂停执行。由此当程序执行时,其会暂停至 libstdc++ 库中的某个位置,借助 up 指令我们可以得知该异常发生在源代码文件中的位置。
同理,我们也可以监控 main.cpp 程序中发生的 catch event 事件:
(gdb) catch catch int
Catchpoint 1 (catch)
(gdb) r
Starting program: ~/demo/main.exe
Catchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
\#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb) c
Continuing.
next
Catchpoint 1 (exception caught), 0x00007ffff7e804d3 in __cxa_begin_catch ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
\#1 0x00005555555552d0 in main () at main.cpp:9
9 }catch(int e){
(gdb)
<留坑,准备写load选项>
gdb设置条件断点
条件断点在之前的break 断点中已经接触过了,就是当条件触发的时候在指定位置设置断点。现在要讲的是,给指定的断点设置条件。语法如下:
(gdb) condition bnum expression
(gdb) condition bnum
参数 bnum 用于代指目标断点的编号;参数 expression 表示为断点添加或修改的条件表达式。
以下列程序为例来看看具体的使用方式:
#include <stdio.h>
int main ()
{
unsigned long long int n, sum;
n = 1;
sum = 0;
while (n <= 100)
{
sum = sum + n;
n = n + 1;
}
return 0;
}
对上面的程序进行编译调试,如下:
(gdb) list
4 unsigned long long int n, sum;
5 n = 1;
6 sum = 0;
7 while (n <= 100)
8 {
9 sum = sum + n;
10 n = n + 1;
11 }
12 return 0;
13 }
(gdb) break 9 if sum=10 <---- 如果sum等于10,在第9行暂停运行
Note: breakpoint 1 also set at pc 0x55555555513b.
Breakpoint 2 at 0x55555555513b: file main.c, line 9.
(gdb) run <---- 开始运行,第一个断点处暂停
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/klelee/github/c/c.biancheng.com/gdb_demo/main.out
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
Breakpoint 1, main () at main.c:9
9 sum = sum + n; <---- 暂停了
(gdb) print sum <---- 打印sum的值,确认是我们设置的
$2 = 10
(gdb) info break <---- 查看当前存在的断点
Num Type Disp Enb Address What
1 breakpoint keep y 0x000055555555513b in main at main.c:9
stop only if sum = 1
breakpoint already hit 1 time
2 breakpoint keep y 0x000055555555513b in main at main.c:9
stop only if sum=10
breakpoint already hit 1 time
(gdb) condition 2 sum=21 <---- 我们将第二个断点的条件改成sum=21
(gdb) continue <---- 继续运行
Continuing.
Breakpoint 1, main () at main.c:9
9 sum = sum + n; <---- 暂停了
(gdb) print sum <---- 打印sum的值,确认是我们设置的
$3 = 21
(gdb)
以上就是condition命令的一个使用方法。
gdb单步调试程序
GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB 调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。
用下面的例子来学习单步调试:
#include <stdio.h>
int print(int num){
int ret = num * num;
return ret;
}
int myfunc(int num){
int i = 1;
int sum = 0;
while(i <= num){
sum += print(i);
i++;
}
return sum;
}
int main(){
int num =0;
scanf("%d", &num);
int result = myfunc(num);
printf("%d", result);
return 0;
}
next命令
特点是对于函数,next只是会把它当成一行指令。来看看具体使用吧:
(gdb) list
1 #include <stdio.h>
2 int print(int num){
3 int ret = num * num;
4 return ret;
5 }
6 int myfunc(int num){
7 int i = 1;
8 int sum = 0;
9 while(i <= num){
10 sum += print(i);
(gdb)
11 i++;
12 }
13 return sum;
14 }
15 int main(){
16 int num =0;
17 scanf("%d", &num);
18 int result = myfunc(num);
19 printf("%d", result);
20 return 0;
(gdb)
21 }
(gdb)
Line number 22 out of range; main.c has 21 lines.
(gdb) break 16 <---- 在16行打一个普通断点
Breakpoint 1 at 0x11be: file main.c, line 16.
(gdb) run
Starting program: /home/klelee/github/c/c.biancheng.com/gdb_demo/step_demo/main.out
...
Breakpoint 1, main () at main.c:16
16 int num =0; <---- 在此处停止
(gdb) next <---- 下一步是一个scanf语句,我们看一下next之后的效果
17 scanf("%d", &num); <---- next之后并没有让我们输入,也就是说下一步,但不执行
(gdb) next <---- 继续next
3 <---- 这里响应scanf了
18 int result = myfunc(num);
(gdb) next
19 printf("%d", result);
(gdb) next <---- 这个next直接到return语句,表明next不会进入到print函数内部去单步调试
20 return 0;
(gdb)
从上面的例子可以看出来next语句,会把函数当成一个语句,一次性执行!
step命令
step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。
until命令
until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:
(gdb) until
(gdb) until location
其中,参数 location 为某一行代码的行号。
不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。
断点禁用和删除
在程序调试过程中使用break打出来的断点,我们之前说到是永久有效的。假如说在某个程序的循环结构中打的断点,我们不需要了,那就需要考虑进行删除操作,或者禁用掉。
在删除或禁用之前需要先查看当前程序中有的断点。
info命令查看断点
(gdb) list
2 int print(int num){
3 int ret = num * num;
4 return ret;
5 }
6 int myfunc(int num){
7 int i = 1;
8 int sum = 0;
9 while(i <= num){
10 sum += print(i);
11 i++;
(gdb)
12 }
13 return sum;
14 }
15 int main(){
16 int num =0;
17 scanf("%d", &num);
18 int result = myfunc(num);
19 printf("%d", result);
20 return 0;
21 }
(gdb) break 11 <---- 打第一个断点
Breakpoint 1 at 0x1182: file main.c, line 11. <---- 断点类型为:breakpoint 编号为1
(gdb) break 16
Breakpoint 2 at 0x119b: file main.c, line 16. <---- 断点类型为:breakpoint 编号为2
(gdb) run
Starting program: /root/c/c.biancheng.com/gdb_demo/main.out
Breakpoint 2, main () at main.c:16
16 int num =0;
(gdb) continue
Continuing.
3
Breakpoint 1, myfunc (num=3) at main.c:11
11 i++;
(gdb) continue
Continuing.
Breakpoint 1, myfunc (num=3) at main.c:11
11 i++;
(gdb) info break <---- 查看现在存在的断点
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555182 in myfunc at main.c:11
breakpoint already hit 2 times
2 breakpoint keep y 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
(gdb)
可能当我们了解当前阶段的运行状态之后需要删除某些断点,或者断点行数设置错误,就需要删除一些断点了。删除断点通常有两种方法,分别是clear命令和delete命令。
删除断点命令——clear和delete
- clear:删除指定点的所有断点
- delete:删除所有断点,或指定点的断点
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555182 in myfunc at main.c:11
breakpoint already hit 3 times
2 breakpoint keep y 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
(gdb) delete 2 <---- 删除断点编号为2的断点
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555182 in myfunc at main.c:11
breakpoint already hit 3 times
(gdb) clear 11 <---- 删除在第11行的所有断点
Deleted breakpoint 1
(gdb)
禁用断点命令——disable
禁用和删除的区别就在于断点还在不在。其常用语法为:
(gdb) disable # 禁用所有断点
(gdb) disable num1 num2 ... # 禁用指定的断点
如下:
(gdb) list
14 }
15 int main(){
16 int num =0;
17 scanf("%d", &num);
18 int result = myfunc(num);
19 printf("%d", result);
20 return 0;
21 }
(gdb)
Line number 22 out of range; main.c has 21 lines.
(gdb) break 16 <---- 设置一个普通断点
Breakpoint 3 at 0x55555555519b: file main.c, line 16.
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /root/c/c.biancheng.com/gdb_demo/main.out
Breakpoint 3, main () at main.c:16
16 int num =0;
(gdb) watch num <---- 设置一个观察断点
Hardware watchpoint 4: num
(gdb) info break <---- 查看所有断点
Num Type Disp Enb Address What
3 breakpoint keep y 0x000055555555519b in main at main.c:16 <---- 此时断点状态为y
breakpoint already hit 1 time
4 hw watchpoint keep y num
(gdb) disable 3 <---- 禁用第三个断点
(gdb) info break
Num Type Disp Enb Address What
3 breakpoint keep n 0x000055555555519b in main at main.c:16 <---- 断点还在,只是状态为n
breakpoint already hit 1 time
4 hw watchpoint keep y num
(gdb)
打开已禁用的断点——enable
既然禁用断点让其保存,那就要有重新使用的方法,那就是enable,其语法也和disable相对应
(gdb) disable
(gdb) info break
Num Type Disp Enb Address What
3 breakpoint keep n 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
4 hw watchpoint keep n num
(gdb) enable
(gdb) info break
Num Type Disp Enb Address What
3 breakpoint keep y 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
4 hw watchpoint keep y num
(gdb) disable 4
(gdb) info break
Num Type Disp Enb Address What
3 breakpoint keep y 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
4 hw watchpoint keep n num
(gdb) enable 4
(gdb) info break
Num Type Disp Enb Address What
3 breakpoint keep y 0x000055555555519b in main at main.c:16
breakpoint already hit 1 time
4 hw watchpoint keep y num
(gdb)