C C++最新C C++调试总结:IDE(visual studio和Dev C+(1),C C++开发的基础知识

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

(gdb) b +1 <-- 在第 4 行的基础上,在第 5 行代码处打断点
Breakpoint 2 at 0x55555555513f: file main.c, line 5.
(gdb) c <-- 继续执行程序,至第 5 行暂停
Continuing.

Breakpoint 2, main (argc=1, argv=0x7fffffffe078) at main.c:5
5 while(num<100)
(gdb) b 7 if num>10 <-- 如果 num>10 在第 7 行打断点
Breakpoint 3 at 0x555555555141: file main.c, line 7.
(gdb) c <-- 继续执行
Continuing.

Breakpoint 3, main (argc=1, argv=0x7fffffffe078) at main.c:7
7 num *= 2; <-- 程序在第 7 行暂停
(gdb) p num <-- p 命令查看 num 当前的值
$1 = 16 <-- num=16



> 
> 有关 c 和 p 命令,后续章节会做详细讲解,这里会用即可,不必深究。
> 
> 
> 


### GDB tbreak命令


tbreak 命令可以看到是 break 命令的另一个版本,tbreak 和 break 命令的用法和功能都非常相似,唯一的不同在于,使用 tbreak 命令打的断点仅会作用 1 次,即使程序暂停之后,该断点就会自动消失。


tbreak 命令的使用格式和 break 完全相同,有以下 2 种:


1、(gdb) tbreak location  
 2、(gdb) tbreak … if cond


其中,location、… 和 cond 的含义都和 break 命令中的参数含义相同,即表 1 也同样适用于 tbreak 命令。


仍以 main.exe 为例,如下演示了 tbreak 命令的用法:


(gdb) tbreak 7 if num>10  
 Temporary breakpoint 1 at 0x1165: file main.c, line 7.  
 (gdb) r  
 Starting program: /home/ubuntu64/demo/main.exe


Temporary breakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:7  
 7 num \*= 2;  
 (gdb) p num  
 $1 = 16  
 (gdb) c <-- 继续执行程序,则原使用 tbreak 在第 7 行打的断点将不再起作用  
 Continuing.  
 num=128[Inferior 1 (process 6534) exited normally]  
 (gdb)


可以看到,自`num=16`开始,后续循环过程中 num 的值始终大于 10,则`num>10`表达式的值永远为 True,理应在第 7 行暂停多次。但由于打断点采用的是 tbreak 命令,因此断点的作用只起 1 次。


### GDB rbreak 命令


和 break 和 tbreak 命令不同,rbreak 命令的作用对象是 C、C++ 程序中的函数,它会在指定函数的开头位置打断点。


tbreak 命令的使用语法格式为:


(gdb) tbreak regex


其中 regex 为一个正则表达式,程序中函数的函数名只要满足 regex 条件,tbreak 命令就会其内部的开头位置打断点。值得一提的是,tbreak 命令打的断点和 break 命令打断点的效果是一样的,会一直存在,不会自动消失。


这里我们对 main.c 源文件的程序做如下修改:


(gdb) l <-- 显示源码  
 1 #include<stdio.h>  
 2 void rb\_one(){  
 3 printf(“rb\_one\n”);  
 4 }  
 5 void rb\_second(){  
 6 printf(“rb\_second”);  
 7 }  
 8 int main(int argc,char\* argv[])  
 9 {  
 10 rb\_one();  
 (gdb)  
 11 rb\_second();  
 12 return 0;  
 13 }  
 (gdb) rbreak rb\_\* <–匹配所有以 rb\_ 开头的函数  
 Breakpoint 1 at 0x1169: file main.c, line 2.  
 void rb\_one();  
 Breakpoint 2 at 0x1180: file main.c, line 5.  
 void rb\_second();  
 (gdb) r  
 Starting program: /home/ubuntu64/demo/main.exe


Breakpoint 1, rb\_one () at main.c:2  
 2 void rb\_one(){  
 (gdb) c  
 Continuing.  
 rb\_one


Breakpoint 2, rb\_second () at main.c:5  
 5 void rb\_second(){  
 (gdb) c  
 Continuing.  
 rb\_second[Inferior 1 (process 7882) exited normally]  
 (gdb)


可以看到,通过执行`rbreak rb_*`指令,找到了程序中所有以 tb\_\* 开头的函数,并在这些函数内部的开头位置打上了断点(如上所示,分别为第 2 行和第 5 行)。


### 总结


在 GDB 调试器中,为程序的适当位置打断点,是最常用的调试程序的方法。不过,本节仅介绍了如何使用 break(tbreak、rbreak)在程序中打断点,实际场景中还可以使用 catch 或者 watch 中断程序的运行,有关它们的功能和用法,会在后续章节中给大家做详细讲解。


## GDB watch命令:监控变量值的变化


《[GDB break命令]( )》一节,给大家介绍了使用 break 命令在程序某一行的位置打断点。但还有一些场景,我们需要监控某个变量或者表达式的值,通过值的变化情况判断程序的执行过程是否存在异常或者 Bug。这种情况下,break 命令显然不再适用,推荐大家使用 watch 命令。


要知道,GDB 调试器支持在程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点。其中 break 命令打的就是普通断点,而 watch 命令打的为观察断点,关于捕捉断点,后续章节会做详细讲解。


使用 GDB 调试程序的过程中,借助观察断点可以监控程序中某个变量或者表达式的值,只要发生改变,程序就会停止执行。相比普通断点,观察断点不需要我们预测变量(表达式)值发生改变的具体位置。



> 
> 所谓表达式,就是包含多个变量的式子,比如 a+b 就是一个表达式,其中 a、b 为变量。
> 
> 
> 


对于监控 C、C++ 程序中某变量或表达式的值是否发生改变,watch 命令的语法非常简单,如下所示:


(gdb) watch cond


其中,conde 指的就是要监控的变量或表达式。


和 watch 命令功能相似的,还有 rwatch 和 awatch 命令。其中:


* rwatch 命令:只要程序中出现读取目标变量(表达式)的值的操作,程序就会停止运行;
* awatch 命令:只要程序中出现读取目标变量(表达式)的值或者改变值的操作,程序就会停止运行。



> 
> 强调一下,watch 命令的功能是:只有当被监控变量(表达式)的值发生改变,程序才会停止运行。
> 
> 
> 


举个例子:



(gdb) l <–列出要调试的程序源码
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(“%d”,num);
10 return 0;
(gdb)
11 }
(gdb) b 4 <-- 使用 break 命令打断点
Breakpoint 1 at 0x115c: file main.c, line 4.
(gdb) r <-- 执行程序
Starting program: /home/ubuntu64/demo/main.exe

Breakpoint 1, main (argc=1, argv=0x7fffffffe088) at main.c:4
4 int num = 1;
(gdb) watch num <-- 监控程序中 num 变量的值
Hardware watchpoint 2: num
(gdb) c <-- 继续执行,当 num 值发生改变时,程序才停止执行
Continuing.

Hardware watchpoint 2: num

Old value = 0
New value = 2
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb) c <-- num 值发生了改变,继续执行程序
Continuing.

Hardware watchpoint 2: num

Old value = 2
New value = 4
main (argc=1, argv=0x7fffffffe088) at main.c:5
5 while(num<=100)
(gdb)



> 
> 有关代码中蓝色部分的含义,本文后续会做详细解释。
> 
> 
> 


可以看到在程序运行过程中,通过借助 watch 命令监控 num 的值,后续只要 num 的值发生改变,程序都会停止。感兴趣的读者,可自行尝试使用 awatch 和 rwatch 命令,这里不再给出具体的示例。


如果我们想查看当前建立的观察点的数量,借助如下指令即可:


(gdb) info watchpoints


值得一提的是,对于使用 watch(rwatch、awatch)命令监控 C、C++ 程序中变量或者表达式的值,有以下几点需要注意:


* 当监控的变量(表达式)为局部变量(表达式)时,一旦局部变量(表达式)失效,则监控操作也随即失效;
* 如果监控的是一个指针变量(例如 \*p),则 watch \*p 和 watch p 是有区别的,前者监控的是 p 所指数据的变化情况,而后者监控的是 p 指针本身有没有改变指向;
* 这 3 个监控命令还可以用于监控数组中元素值的变化情况,例如对于 a[10] 这个数组,watch a 表示只要 a 数组中存储的数据发生改变,程序就会停止执行。



> 
> 如果读者只想学习如何使用 watch 命令,则读者这里即可。反之,如果想了解 watch 命令底层是如何实现的,可以继续往下阅读。
> 
> 
> 


### watch命令的实现原理


watch 命令实现监控机制的方式有 2 种,一种是为目标变量(表达式)设置硬件观察点,另一种是为目标变量(表达式)设置软件观察点。


所谓软件观点(software watchpoint),即用 watch 命令监控目标变量(表达式)后,GDB 调试器会以单步执行的方式运行程序,并且每行代码执行完毕后,都会检测该目标变量(表达式)的值是否发生改变,如果改变则程序执行停止。


可想而知,这种“实时”的判别方式,一定程度上会影响程序的执行效率。但从另一个角度看,调试程序的目的并非是为了获得运行结果,而是查找导致程序异常或 Bug 的代码,因此即便软件观察点会影响执行效率,一定程度上也是可以接受的。


所谓硬件观察点(Hardware watchpoint),和前者最大的不同是,它在实现监控机制的同时不影响程序的执行效率。简单的理解,系统会为 GDB 调试器提供少量的寄存器(例如 32 位的 Intel x86 处理器提供有 4 个调试寄存器),每个寄存器都可以作为一个观察点协助 GDB 完成监控任务。


需要注意的是,基于寄存器个数的限制,如果调试环境中设立的硬件观察点太多,则有些可能会失去作用,这种情况下,GDB 调试器会发出如下警告:


Hardware watchpoint num: Could not insert watchpoint


解决方案也很简单,就是删除或者禁用一部分硬件观察点。


除此之外,受到寄存器数量的限制,可能会出现:无法使用硬件观察点监控数据类型占用字节数较多的变量(表达式)。比如说,某些操作系统中,GDB 调试器最多只能监控 4 个字节长度的数据,这意味着 C、C++ 中 double 类型的数据是无法使用硬件观察点监测的。这种情况下,可以考虑将其换成占用字符串少的 float 类型。


目前,大多数 PowerPC 或者基于 x86 的操作系统,都支持采用硬件观点。并且 GDB 调试器在建立观察断点时,会优先尝试建立硬件观察点,只有当前环境不支持硬件观察点时,才会建立软件观察点。借助如下指令,即可强制 GDB 调试器只建立软件观察点:


set can-use-hw-watchpoints 0



> 
> 注意,在执行此命令之前建立的硬件观察点,不会受此命令的影响。
> 
> 
> 


注意,awatch 和 rwatch 命令只能设置硬件观察点,如果系统不支持或者借助如上命令禁用,则 GDB 调试器会打印如下信息:


Expression cannot be implemented with read/access watchpoint.


## GDB catch命令:建立捕捉断点


要知道,GDB 调试器支持在被调试程序中打 3 种断点,分别为普通断点、观察断点和捕捉断点,其中普通断点用 break 命令建立(可阅读《[GDB break]( )》一节),观察断点用 watch 命令建立(可阅读《[GDB watch]( )》一节),本节将讲解如何使用 catch 命令建立捕捉断点。


和前 2 种断点不同,普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。



> 
> 用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
> 
> 
> 


建立捕捉断点的方式很简单,就是使用 catch 命令,其基本格式为:


(gdb) catch event


其中,event 参数表示要监控的具体事件。对于使用 GDB 调试 C、C++ 程序,常用的 event 事件类型如表 1 所示。




| event 事件 | 含 义 |
| --- | --- |
| throw [exception] | 当程序中抛出 exception 指定类型异常时,程序停止执行。如果不指定异常类型(即省略 exception),则表示只要程序发生异常,程序就停止执行。 |
| catch [exception] | 当程序中捕获到 exception 异常时,程序停止执行。exception 参数也可以省略,表示无论程序中捕获到哪种异常,程序都暂停执行。 |
| load [regexp] unload [regexp] | 其中,regexp 表示目标动态库的名称,load 命令表示当 regexp 动态库加载时程序停止执行;unload 命令表示当 regexp 动态库被卸载时,程序暂停执行。regexp 参数也可以省略,此时只要程序中某一动态库被加载或卸载,程序就会暂停执行。 |



> 
> 除表中罗列的以外,event 参数还有其它一些写法,感兴趣的读者可查看 [GDB官网]( )进行了解,这里不再过多赘述。
> 
> 
> 


注意,当前 GDB 调试器对监控 C++ 程序中异常的支持还有待完善,使用 catch 命令时,有以下几点需要说明:


1. 对于使用 catch 监控指定的 event 事件,其匹配过程需要借助 libstdc++ 库中的一些 SDT 探针,而这些探针最早出现在 GCC 4.8 版本中。也就是说,想使用 catch 监控指定类型的 event 事件,系统中 GCC 编译器的版本最低为 4.8,但即便如此,catch 命令是否能正常发挥作用,还可能受到系统中其它因素的影响。
2. 当 catch 命令捕获到指定的 event 事件时,程序暂停执行的位置往往位于某个系统库(例如 libstdc++)中。这种情况下,通过执行 up 命令,即可返回发生 event 事件的源代码处。
3. catch 无法捕获以交互方式引发的异常。


如同 break 命令和 tbreak 命令的关系一样(前者的断点是永久的,后者是一次性的),catch 命令也有另一个版本,即 tcatch 命令。tcatch 命令和 catch 命令的用法完全相同,唯一不同之处在于,对于目标事件,catch 命令的监控是永久的,而 tcatch 命令只监控一次,也就是说,只有目标时间第一次触发时,tcath 命令才会捕获并使程序暂停,之后将失效。


接下来就以下面的 C++ 程序为例,给大家演示 catch(tcatch)命令的用法:



#include
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;
}


此程序存储于 ~/demo/main.cpp 文件中( ~ 表示当前登陆用户的主目录)。


在此基础上,对 main.cpp 文件进行编译,并启动对该程序的调试:



[root@bogon demo]$ ls
main.cpp
[root@bogon demo]# g++ main.cpp -o main.exe -g
[root@bogon demo]$ ls
main.cpp main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe…done.
(gdb)

通过观察程序可以看出,当前程序中通过 throw 手动抛出了 int 异常,此异常能够被 catch 成功捕获。假设我们使用 catch 命令监控:只要程序中引发 int 异常,程序就停止执行:

(gdb) catch throw int <-- 指定捕获“throw int”事件
Catchpoint 1 (throw)
(gdb) r <-- 执行程序
Starting program: ~/demo/main.exe

Catchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6 <-- 程序暂停执行
(gdb) up <-- 回到源码
#1 0x0000555555555287 in main () at main.cpp:8
8 throw 100;
(gdb) c <-- 继续执行程序
Continuing.
next

Catchpoint 1 (exception thrown), 0x00007ffff7e81762 in __cxa_throw ()
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x0000555555555287 in main () at main.cpp:8
8 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)


甚至于,在个别场景中,还可以使用 catch 命令监控 C、C++ 程序动态库的加载和卸载。就以 main.exe 为例,其运行所需加载的动态库可以使用 ldd 命令查看,例如:



[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fffbc1ff000)
libstdc++.so.6 => /usr/lib64/libstdc++.so.6 (0x0000003e75000000)
libm.so.6 => /lib64/libm.so.6 (0x00000037eee00000)
libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x0000003e74c00000)
libc.so.6 => /lib64/libc.so.6 (0x00000037ee200000)
/lib64/ld-linux-x86-64.so.2 (0x00000037eda00000)


就以监控 libstdc++.so.6 为例,在 GDB 调试器中,通过执行如下指令,即可监控该动态库的加载:



(gdb) catch load libstdc++.so.6
Catchpoint 1 (load)
(gdb) r
Starting program: ~/demo/main.exe

Catchpoint 1
Inferior loaded /lib/x86_64-linux-gnu/libstdc++.so.6
/lib/x86_64-linux-gnu/libgcc_s.so.1
/lib/x86_64-linux-gnu/libc.so.6
/lib/x86_64-linux-gnu/libm.so.6
0x00007ffff7fd37a5 in ?? () from /lib64/ld-linux-x86-64.so.2



> 
> 以上实例仅演示了 catch 命令的用法,而 tcatch 命令的语法格式和 catch 完全相同,读者可自行尝试使用 tcatch 命令,观察它的功能。
> 
> 
> 


## GDB条件断点(condition命令)详解


前面章节给大家介绍了 GDB 调试器中普通断点、观察断点以及捕捉断点的功能和用法。其中值得一提的是,对于普通断点的建立,可以使用如下格式的 break 命令:


(gdb) break … if cond


… 参数用于指定生成断点的具体位置;cond 参数用于代指某个表达式。通过此方式建立的普通断点,只有当表达式 cond 成立(值为 True)时,才会发挥它的作用;反之,断点并不会使程序停止执行。


类似上面这种,以某个表达式的是否成立作为条件,从而决定自身是否生效的断点,又称为条件断点。除了普通断点外,观察断点和捕捉断点也可以成为条件断点。


需要说明的是,创建普通条件断点的方式,也同样适用于观察条件断点。通过执行如下命令,即可直接生成一个观察条件断点:


(gdb) watch expr if cond


参数 expr 表示要观察的变量或表达式;参数 cond 用于代指某个表达式。


但是,以上创建条件断点的方法,不适用于捕捉断点。换句话说,捕捉条件断点无法直接生成,需要借助 condition 命令为现有捕捉断点增添一个 cond 表达式,才能使其变成条件断点。


总的来说,借助 condition 命令,我们可以将现有的普通断点、观察断点以及捕捉断点变成条件断点;而普通条件断点和观察条件断点,可以分别通过 break if 命令和 watch if 命令直接生成。


接下来,我将给大家详细地讲解 condition 命令的用法。


### GDB condition命令


严格来说,condition 命令的功能是:既可以为现有的普通断点、观察断点以及捕捉断点添加条件表达式,也可以对条件断点的条件表达式进行修改。


condition 命令没有缩写形式,使用方法很简单,语法格式如下:


(gdb) condition bnum expression  
 (gdb) condition bnum


参数 bnum 用于代指目标断点的编号;参数 expression 表示为断点添加或修改的条件表达式。


以上 2 种语法格式中,第 1 种用于为 bnum 编号的断点添加或修改 expression 条件表达式;第 2 种用于删除 bnum 编号断点的条件表达式,使其变成普通的无条件断点。


举个例子,这里以调试如下 C++ 程序为例:



#include
using namespace std;
int main(){
int num = 1;
while(num<20){
try{
throw num;
}catch(int &e){
num++;
}
}
cout << num << endl;
return 0;
}


程序存储位置为`~/demo/main.cpp`,并已经生成了可供 GDB 调试器使用的执行文件:



[root@bogon demo]# ls
main.cpp main.exe
[root@bogon demo]# gdb main.exe -q
Reading symbols from ~/demo/main.exe…done.
(gdb) l
1 #include
2 using namespace std;
3 int main(){
4 int num = 1;
5 while(num<20){
6 try{
7 throw num;
8 }catch(int &e){
9 num++;
10 }
(gdb)
11 }
12 cout << num << endl;
13 return 0;
14 }
(gdb)

接下来,为读者演示 condition 命令的功能和用法:

(gdb) b 9 <–添加普通断点
Breakpoint 1 at 0x12d0: file main.cpp, line 9.
(gdb) r
Starting program: /home/test/demo/main.exe

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) rwatch num <-- 添加观察断点
Hardware read watchpoint 2: num
(gdb) catch throw int <-- 添加捕捉断点
Catchpoint 3 (throw)
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x00005555555552d0 in main() at main.cpp:9 breakpoint already hit 1 time
2 read watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb) condition 1 num3 <-- 为普通断点添加条件表达式
(gdb) condition 2 num
5 <-- 为观察断点添加条件表达式
(gdb) condition 3 num==7 <-- 为捕捉断点添加条件表达式
(gdb) c
Continuing.

Breakpoint 1, main () at main.cpp:9 <-- 普通条件断点触发
9 num++;
(gdb) p num
$1 = 3
(gdb) c
Continuing.

Hardware read watchpoint 2: num <-- 观察条件断点触发

Value = 5
0x000055555555526f in main () at main.cpp:7
7 throw num;
(gdb) c
Continuing.

Catchpoint 3 (exception thrown), 0x00007ffff7e81762 in __cxa_throw () <-- 捕捉条件断点触发
from /lib/x86_64-linux-gnu/libstdc++.so.6
(gdb) up
#1 0x0000555555555285 in main () at main.cpp:7
7 throw num;
(gdb) p num
$2 = 7
(gdb)


可以看到,通过借助 condition 命令为不同类型断点设置条件表达式,只有当条件表达式成立(值为 True)时,相应的断点才会触发从而使程序暂停运行。


##### GDB ignore命令


ignore 命令也可以使一个断点成为条件断点,但这里的“条件”并非自定义的表达式,而仅为一个整数,它用来表示该断点失效的次数。也就会说,ignore 命令可以使目标断点暂时失去作用,当断点失效的次数超过指定次数时,断点的功能会自动恢复。


ignore 命令也没有缩写形式,其语法格式如下:


ignore bnum count


参数 bnum 为某个断点的编号;参数 count 用于指定该断点失效的次数。


仍以 main.exe 为例:



(gdb) b 9
Breakpoint 1 at 0x400a33: file main.cpp, line 9.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) p num
$1 = 1
(gdb) ignore 1 3
Will ignore next 3 crossings of breakpoint 1.
(gdb) c
Continuing.

Breakpoint 1, main () at main.cpp:9
9 num++;
(gdb) p num
$2 = 5
(gdb)


可以看到,执行 ignore 命令之前,num 变量的值为 1。借助 ignore 命令使编号为 1(作用于第 9 行)的断点失效 3 次后,继续执行程序,最终程序仍暂停至第 9 行,此时 num 的值变为 5。这这恰恰证明了 num 从 1 递增至 5 的过程中,编号为 1 的断点失效了 3 次。


## GDB单步调试程序


《[调用GDB调试器的几种方式]( )》一节中提到,借助 next 命令可以控制 GDB 单步执行程序。所谓单步调试,就是通过一行一行的执行程序,观察整个程序的执行流程,进而尝试发现一些存在的异常或者 Bug。


根据实际场景的需要,GDB 调试器共提供了 3 种可实现单步调试程序的方法,即使用 next、step 和 until 命令。换句话说,这 3 个命令都可以控制 GDB 调试器每次仅执行 1 行代码,但除此之外,它们各自还有不同的功能。


本节就来一一给大家讲解这 3 个命令的功能和用法。讲解过程中,将以调试如下 C 语言程序为例:



#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;
}


此程序存储在`~/demo/main.c`源文件中(~ 表示当前用户的主目录),功能是根据用户输入的 num 值,输出 12+22+…+num2 的值。


### GDB next 命令


next 是最常用来进行单步调试的命令,其最大的特点是当遇到包含调用函数的语句时,无论函数内部包含多少行代码,next 指令都会一步执行完。也就是说,对于调用的函数来说,next 命令只会将其视作一行代码。


next 命令可以缩写为 n 命令,使用方法也很简单,语法格式如下:


(gdb) next count


参数 count 表示单步执行多少行代码,默认为 1 行。


举个例子:



(gdb) b 16
Breakpoint 2 at 0x40058b: file main.c, line 16.
(gdb) r
Starting program: /root/demo/main.exe

Breakpoint 2, main () at main.c:16
16 int num =0;
(gdb) n 2 <-- 单步执行 2 次
3
18 int result = myfunc(num);
(gdb) n
19 printf(“%d”, result);
(gdb) n
20 return 0;
(gdb)


可以看到,当程序单步执行第 18 行时,继续执行 next 指令,下一次将要执行的是第 19 行代码,而非 myfunc() 函数内部的代码。


### GDB step命令


通常情况下,step 命令和 next 命令的功能相同,都是单步执行程序。不同之处在于,当 step 命令所执行的代码行中包含函数时,会进入该函数内部,并在函数第一行代码处停止执行。


step 命令可以缩写为 s 命令,用法和 next 命令相同,语法格式如下:


(gdb) step count


参数 count 表示一次执行的行数,默认为 1 行。


仍以 main.exe 为例:



(gdb) b 18
Breakpoint 1 at 0x4005ab: file main.c, line 18.
(gdb) r
Starting program: ~/demo/main.exe
Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step <-- step 命令进入 myfunc() 函数内部执行
myfunc (num=0) at main.c:7
7 int i = 1;


可以看到,当程序暂停到包含 mufunc() 函数的代码行处时(此时该行代码尚未执行),如果使用 step 命令,则 GDB 在执行该函数代码的同时,会进入 mufunc() 函数内部,并暂停在函数内部的第一行代码处。反之如果使用 next 命令,则程序下一次会执行第 19 行代码,而不是第 7 行,这就是它们最大的不同之处。


### GDB until命令


until 命令可以简写为 u 命令,有 2 种语法格式,如下所示:


1、(gdb) until  
 2、(gdb) until location


其中,参数 location 为某一行代码的行号。


不带参数的 until 命令,可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。注意,until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。


以 main.exe 中 myfunc() 函数的循环为例:



(gdb) b 17
Breakpoint 1 at 0x1201: file main.c, line 17.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:17
17 scanf(“%d”, &num);
(gdb) u
3
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) u
8 int sum = 0;
(gdb) u
9 while(i <= num){
(gdb) u
10 sum += print(i);
(gdb) u
11 i++;
(gdb) u <-- 执行 i++ 操作
9 while(i <= num){
(gdb) u <-- 快速执行完循环体
13 return sum;
(gdb) p sum
$1 = 14


可以看到,这里当程序单步执行完第 11 行代码时,借助 until 命令快速执行完了整个循环体,并在第 13 行代码处停止执行。根据 p 命令输出的 num 变量的值可以确认,整个循环过程确定完整地执行完了。


until 命令还可以后跟某行代码的行号,以指示 GDB 调试器直接执行至指定位置后停止。举个例子:



(gdb) r
Starting program:~/demo/main.exe

Breakpoint 1, main () at main.c:17
17 scanf(“%d”, &num);
(gdb) until 19 <-- 执行至第 19 行停止
3
main () at main.c:19
19 printf(“%d”, result);
(gdb) p result
$3 = 14


可以看到,通过执行 until 19 命令,GDB 调试器直接从第 17 行代码处执行至指定的第 19 行。


## 如何使用GDB进行断点调试?


前面利用 3 节的内容,分别介绍了 GDB 调试器支持在被调试程序中打断点的 3 种方法,即 break、watch 以及 catch 命令。在此基础上,本节给大家讲解:如何借助断点对程序进行调试?


通过在程序的适当位置打断点,观察程序执行至该位置时某些变量(或表达式)的值,进而不断缩小导致程序出现异常或 Bug 的语句的搜索范围,并最终找到,整个过程就称为断点调试。


值得一提的是,整个断点调试的过程,除了要借助 break、watch 或者 catch 命令以外,还要借助其它一些命令,例如在前面章节中,我们已经使用过的 print 命令(查看变量的值)、continue 命令(使程序继续执行)等。


表 1 罗列了断点调试程序过程中,常用的一些命令以及各自的含义。




| 命令(缩写) | 功 能 |
| --- | --- |
| run(r) | 启动或者重启一个程序。 |
| list(l) | 显示带有行号的源码。 |
| continue(c) | 让暂停的程序继续运行。 |
| next(n) | 单步调试程序,即手动控制代码一行一行地执行。 |
| step(s) | 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。 |
| until(u) until location(u location) | 当你厌倦了在一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。 |
| finish(fi) | 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。 |
| return(return) | 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。 |
| jump(j) | 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。 |
| print(p) | 打印指定变量的值。 |
| quit(q) | 退出 GDB 调试器。 |



> 
> 其中,next、step 以及 until 命令,已经在 《[GDB单步调试]( )》一节中做了详细介绍,本节不再赘述。另外,表 1 中罗列的这些命令读者无需死记硬背,因为实际使用 GDB 调试器时,它们都会常常用到,熟能生巧。
> 
> 
> 


为了搞清楚表 1 中这些命令的功能和用法,这里仍以上节创建的 C 语言程序为例:



#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;
}


此程序存储在`~/demo/main.c`源文件中(~ 表示当前用户的主目录)。


1. 首先,表 1 中 run、continue、list、next、print 以及 quit 命令的用法都非常简单,唯一需要注意的一点是,run 命令除了可以启动程序的执行,还可以在任何时候重新启动程序。


例如,以 main.c 为例:



[root@bogon demo]# gdb main.exe -q
Reading symbols from main.exe…
(gdb) l <-- list 命令的缩写,罗列出带有行号的源码
1 #include <stdio.h>
2 int print(int num){
3 int ret = num * num;
4 return ret;

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) b 16 <-- break 命令的缩写,在程序第 16 行处打普通断点
Breakpoint 1 at 0x11fa: file main.c, line 16.
(gdb) r <-- run 命令的缩写,启动程序
Starting program: /home/test/demo/main.exe
Breakpoint 1, main () at main.c:16
16 int num =0;
(gdb) next <-- 单步执行程序,即执行一行代码
17 scanf(“%d”, &num);
(gdb) next <-- 继续单步执行,此时需要输入一个整数传递给 num
3
18 int result = myfunc(num);
(gdb) p num <-- print 命令的缩写,显示 num 的值
$1 = 3 <-- $1 表示 num 变量所在存储区的名称,这里指的是 num 的值为 3
(gdb) n <-- 继续单步执行
19 printf(“%d”, result);
(gdb) n <-- 单步执行
20 return 0;
(gdb) q <-- 退出调试
A debugging session is active.

Inferior 1 [process 4576] will be killed.

Quit anyway? (y or n) y <-- 由于程序执行尚未结束,GDB 会进行再次确认
[root@bogon demo]#



> 
> 事实上,以上很多命令还有其它的语法格式,只是不常用,这里不再过多赘述,感兴趣的读者可自行查阅 [GDB 官网手册]( )。
> 
> 
> 


接下来,重点给大家介绍表 1 中另外几个命令的用法。


### GDB finish和return命令


实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令。与 finish 命令类似的还有 return 命令,它们都可以结束当前执行的函数。


finish 命令和 return 命令的区别是,finish 命令会执行函数到正常退出;而 return 命令是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。


仍以 main.exe 为例,如下演示了 finish 命令的用法:



(gdb) b 18
Breakpoint 1 at 0x4005ab: file main.c, line 18.
(gdb) r
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) finish
Run till exit from #0 myfunc (num=3) at main.c:9
0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
Value returned is $1 = 14
(gdb)


可以看到,当程序运行至第 9 行处使用 finish 命令,GDB 调试器会执行完 myfunc() 函数中的剩余代码,并在执行完函数后停止。接下来重新执行程序,观察 return 命令的功能:



(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: ~/demo/main.exe
3

Breakpoint 1, main () at main.c:18
18 int result = myfunc(num);
(gdb) step
myfunc (num=3) at main.c:7
7 int i = 1;
(gdb) n
8 int sum = 0;
(gdb) n
9 while(i <= num){
(gdb) return 5
Make myfunc return now? (y or n) y
#0 0x00000000004005b5 in main () at main.c:18
18 int result = myfunc(num);
(gdb) n
19 printf(“%d”, result);
(gdb) p result
$3 = 5


可以看到,同样程序执行至第 9 行,借助 return 命令会立即终止执行 myfunc() 函数,同时手动指定该函数的返回值为 5。因此,最终 result 变量的值为 5,而不再是 14。


### GDB jump命令


jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:


(gdb) jump location


其中,location 通常为某一行代码的行号。


也就是说,jump 命令可以略过某些代码,直接跳到 location 处的代码继续执行程序。这意味着,如果你跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump 跳转到的位置后续没有断点,那么 GDB 会直接执行自跳转处开始的后续代码。


举个例子:



(gdb) b 16
Breakpoint 1 at 0x40058b: file main.c, line 16.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:16
16 int num = 0;
(gdb) jump 19
Continuing at 0x4005b8.
0
Program exited normally.


可以看到,由于借助 jump 指令跳过了 result 变量的初始化过程,因此 result 变量的值为 0(或者垃圾值)。



> 
> 注意,从第 16 行直接跳到第 19 行执行,并不意味着 result 变量不能使用。因为对于可执行文件而言,并不存在 num、result 这些变量名,它们都已经被转化为了地址(确定地说是偏移地址),并且程序在执行时,位于 main() 函数中的所有变量的存储地址都会被确定。也就是说,当我们跳到第 19 行输出 result 的值时,实际上是取其存储地址中的数据,只不过由于 result 没有初始化,所以最终结果值可能为 0,也可能为垃圾值。
> 
> 
> 


## GDB print和display命令:查看变量的值


前面章节中提到,使用 GDB 调试程序,最常用的方法是:单步调试或者断点调试程序,期间通过查看某个变量或者表达式的值,判断当前程序的执行过程是否正确,不断缩小异常或 Bug 位于代码中的范围,最终找到并修复。


对于在调试期间查看某个变量或表达式的值,GDB 调试器提供有 2 种方法,即使用 print 命令或者 display 命令。本节就对这 2 个命令的功能和用法做详细的讲解,整个讲解过程将以调试如下 C 语言程序为例:



#include <stdio.h>
int main(){
int num,result=0,i=0;
scanf(“%d”, &num);
while(i<=num){
result += i;
i++;
}
printf(“result=%d\n”, result);
return 0;
}


此程序存储在`~/demo/main.c`文件中(`~` 代指当前系统登录用户的主目录),并且已经其编译为可供 GDB 调试的 main.exe 可执行文件:


[root@bogon demo]# gcc main.c -o main.exe -g  
 [root@bogon demo]# ls  
 main.c main.exe


### GDB print命令


前面章节中,我们已经多次接触并使用了 print 命令,它的功能就是在 GDB 调试程序的过程中,输出或者修改指定变量或者表达式的值。



> 
> 所谓表达式,简单理解就是由多个变量构成的式子。例如 a、b、c 为单个变量,a+b、a+b\*c 即为表达式。
> 
> 
> 


print 命令可以缩写为 p,最常用的语法格式如下所示:


(gdb) print num  
 (gdb) p num


其中,参数 num 用来代指要查看或者修改的目标变量或者表达式。


以调试 main.exe 为例:



[root@bogon demo]# gdb main.exe -q
Reading symbols from ~/demo/main.exe…done.
(gdb) l
1 #include <stdio.h>
2 int main(){
3 int num,result=0,i=0;
4 scanf(“%d”, &num);
5 while(i<=num){
6 result += i;
7 i++;
8 }
9 printf(“result=%d\n”, result);
10 return 0;
(gdb)
11 }
(gdb) b 3
Breakpoint 1 at 0x40053c: file main.c, line 3.
(gdb) r
Starting program: /root/demo/main.exe

Breakpoint 1, main () at main.c:4
4 scanf(“%d”, &num);
(gdb) n
3
5 while(i<=num){
(gdb) p num <–输出 num 的值
$1 = 3
(gdb) p num=4 <-- 修改 num 的值为 4
$2 = 4
(gdb) b 9
Breakpoint 2 at 0x400569: file main.c, line 9.
(gdb) c
Continuing.

Breakpoint 2, main () at main.c:9
9 printf(“result=%d\n”, result);
(gdb) p result <-- 输出 result 的值
$2 = 10
(gdb) p result=20 <-- 修改 result 的值
$2 = 20
(gdb) c
Continuing.
result=20

Program exited normally.
(gdb)


可以看到,调试 main.exe 的过程中,我们先后使用 p 命令输出了程序中 num 和 result 变量的值,同时还使用该命令对它们的值做了修改。



> 
> print 命令还有更高级的用法,后续会给读者做详细的讲解。
> 
> 
> 


### GDB display命令


和 print 命令一样,display 命令也用于调试阶段查看某个变量或表达式的值,它们的区别是,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来,而 print 命令则不会。


也就是说,使用 1 次 print 命令只能查看 1 次某个变量或表达式的值,而同样使用 1 次 display 命令,每次程序暂停执行时都会自动打印出目标变量或表达式的值。因此,当我们想频繁查看某个变量或表达式的值从而观察它的变化情况时,使用 display 命令可以一劳永逸。


display 命令没有缩写形式,常用的语法格式如下 2 种:


(gdb) display expr  
 (gdb) display/fmt expr


其中,expr 表示要查看的目标变量或表达式;参数 fmt 用于指定输出变量或表达式的格式,表 1 罗列了常用的一些 fmt 参数。




| /fmt | 功 能 |
| --- | --- |
| /x | 以十六进制的形式打印出整数。 |
| /d | 以有符号、十进制的形式打印出整数。 |
| /u | 以无符号、十进制的形式打印出整数。 |
| /o | 以八进制的形式打印出整数。 |
| /t | 以二进制的形式打印出整数。 |
| /f | 以浮点数的形式打印变量或表达式的值。 |
| /c | 以字符形式打印变量或表达式的值。 |



> 
> 注意,display 命令和 /fmt 之间不要留有空格。以 /x 为例,应写为 (gdb)display/x expr。
> 
> 
> 


仍以调试 main.exe 为例:



(gdb) b 4
Breakpoint 1 at 0x40053c: file main.c, line 4.
(gdb) b 9
Breakpoint 2 at 0x400569: file main.c, line 9.
(gdb) r
Starting program: /root/demo/main.exe

Breakpoint 1, main () at main.c:4
4 scanf(“%d”, &num);
(gdb) display num
1: num = 32767
(gdb) display/t result
2: /t result = 0
(gdb) n
3
5 while(i<=num){
2: /t result = 0
1: num = 3
(gdb) c
Continuing.

Breakpoint 2, main () at main.c:9
9 printf(“result=%d\n”, result);
2: /t result = 110
1: num = 3
(gdb) c
Continuing.
result=6

Program exited normally.
(gdb)


可以看到,使用 display 命令查看 num 和 result 变量值时,不仅在执行该命令的同时会看到目标变量的值,后续每次程序停止执行时,GDB 调试器都会将目标变量的值打印出来。


事实上,对于使用 display 命令查看的目标变量或表达式,都会被记录在一张列表(称为自动显示列表)中。通过执行`info dispaly`命令,可以打印出这张表:


(gdb) info display  
 Auto-display expressions now in effect:  
 Num Enb Expression  
 2: y /t result  
 1: y num


其中,各列的含义为:


* Num 列为各变量或表达式的编号,GDB 调试器为每个变量或表达式都分配有唯一的编号;
* Enb 列表示当前各个变量(表达式)是处于激活状态还是禁用状态,如果处于激活状态(用 y 表示),则每次程序停止执行,该变量的值都会被打印出来;反之,如果处于禁用状态(用 n 表示),则该变量(表达式)的值不会被打印。
* Expression 列:表示查看的变量或表达式。


对于不需要再打印值的变量或表达式,可以将其删除或者禁用。


1. 通过执行如下命令,即可删除自动显示列表中的变量或表达式:


(gdb) undisplay num…  
 (gdb) delete display num…


参数 num… 表示目标变量或表达式的编号,编号的个数可以是多个。


举个例子:



(gdb) undisplay 1
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
(gdb)


可以看到,借助 undisplay 命令成功删除了编号为 1 的 num 变量。


2. 通过执行如下命令,可以禁用自动显示列表中处于激活状态下的变量或表达式:


(gdb) disable display num…


num… 表示要禁用的变量或表达式的编号,编号的个数可以是多个,表示一次性禁用多个变量或表达式


举个例子:



(gdb) disable display 2
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: n /t result
(gdb)


可以看到,编号为 2 的 result 变量的 Enb 由 y 变成了 n。处于禁用状态的变量或表达式,程序停止执行时将不再自动打印出它们的值。


当然根据需要,也可以激活当前处于禁用状态的变量或表达式,执行如下命令即可:


(gdb) enable display num…


参数 num… 表示要激活的变量或表达式的编号,编号的个数可以是多个,表示一次性激活多个变量或表达式。


举个例子:



(gdb) enable display 2
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
2: y /t result
(gdb)


总的来说,每次程序停止执行时,GDB 调试器会将自动显示列表中处于激活状态下的变量或表达式的值打印出来,display 命令可以实现在查看目标变量或表达式的值的同时,将其添加到自动显示列表中,而 print 命令则只会打印出目标变量或表达式的值。


## GDB禁用和删除断点


我们知道,GDB 调试器支持 3 种断点,分别为普通断点(用 break 命令创建)、观察断点(用 watch 命令建立)以及捕捉断点(用 catch 命令建立)。并且如果需要的话,我们可以在被调试程序中打多个断点,甚至于 GDB 允许在同一位置打多个断点。


这就产生一个问题,如果之前建立的断点不再需要或者暂时不需要,该如何删除或者禁用呢?常用的方式有 2 种:


1. 使用 quit 命令退出调试,然后重新对目标程序启动调试,此方法会将消除上一次调试操作中建立的所有断点;
2. 使用专门删除或禁用断点的命令,既可以删除某一个断点,也可以删除全部断点。


本节教大家如何使用命令删除或者禁用之前已建好的断点,讲解过程中,将以调试如下 C 语言程序为例:



#include <stdio.h>
int main(){
int num = 0;
scanf(“%d”, &num);
printf(“%d”, num);
return 0;
}


程序存储在`~/demo/main.c`文件中,并已将其编译为可调试的 main.exe 可执行文件:



[root@bogon demo]# gcc main.c -o main.exe -g
[root@bogon demo]# ls
main.c main.exe


### 查看当前已建好的断点


对于当前调试环境中已经建好且尚未删除的断点,可以通过以下 2 种方式查看它们。


1. 借助如下指令,可以查看当前调试环境中存在的所有断点,包括普通断点、观察断点以及捕捉断点:


(gdb) info breakpoint [n]  
 (gdb) info break [n]


参数 n 作为可选参数,为某个断点的编号,表示查看指定断点而非全部断点。


要知道,任何类型的断点在建立时,GDB 调试器都会为其分配一个独一无二的断点编号。以 main.exe 为例,我们尝试建立如下断点:



(gdb) b 1
Breakpoint 1 at 0x1189: file main.c, line 2.
(gdb) r
Starting program: ~/demo/main.exe

Breakpoint 1, main () at main.c:2
2 int main(){
(gdb) watch num
Hardware watchpoint 2: num
(gdb) catch throw int
Catchpoint 3 (throw)


可以看到,我们通过 break 命令建立了一个普通断点,其编号为 1;通过 watch 命令建立了一个观察断点,其编号为 2;通过 catch 命令建立了一个捕捉断点,其编号为 3。


在此基础上,可以通过执行 info break 或者 info breakpoint 命令,查看所有断点的具体信息:


(gdb) info break  
 Num Type Disp Enb Address What  
 1 breakpoint keep y 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time  
 2 hw watchpoint keep y num  
 3 catchpoint keep y exception throw matching: int  
 (gdb)


借助每个断点不同的编号,也可以进行精准查询:


(gdb) info break 1  
 Num Type Disp Enb Address What  
 1 breakpoint keep y 0x000000000040053c in main at main.c:2 breakpoint already hit 1 time



> 
> 以上输出信息中各列的含义分别是:断点编号(Num)、断点类型(Type)、是临时断点还是永久断点(Disp)、目前是启用状态还是禁用状态(Enb)、断点的位置(Address)、断点当前的状态(作用的行号、已经命中的次数等,用 What 列表示)。
> 
> 
> 


2. 除此之外,对于调试环境中已经建好且未删除的观察断点,也可以使用 info watchpoint 命令进行查看,语法格式如下:


(gdb) info watchpoint [n]


n 为可选参数,为某个观察断点的编号,功能是只查看该编号的观察断点的信息,而不是全部的观察断点。


继续在上面的调试环境中,执行如下指令:



(gdb) info watchpoint
Num Type Disp Enb Address What
2 hw watchpoint keep y num
(gdb) info watchpoint 1
No watchpoint number 1.


由于当前环境中仅有 1 个观察断点,因此 info watchpoint 命令仅罗列了编号为 2 的观察断点的信息。需要注意的是,该命令仅能用于查看观察断点,普通断点和捕捉断点无法使用该命令。


### GDB删除断点


无论是普通断点、观察断点还是捕捉断点,都可以使用 clear 或者 delete 命令进行删除。


##### 1) clear命令


clear 命令可以删除指定位置处的所有断点,常用的语法格式如下所示:


(gdb) clear location


参数 location 通常为某一行代码的行号或者某个具体的函数名。当 location 参数为某个函数的函数名时,表示删除位于该函数入口处的所有断点。


在上面调试环境中,继续执行如下命令:



(gdb) clear 2

(gdb) info break
Deleted breakpoint 1
Num Type Disp Enb Address What
2 hw watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb)


可以看到,断点编号为 1、位于程序第 2 行代码处的普通断点已经被删除了。


##### 2) delete 命令


delete 命令(可以缩写为 d )通常用来删除所有断点,也可以删除指定编号的各类型断点,语法格式如下:


delete [breakpoints] [num]


其中,breakpoints 参数可有可无,num 参数为指定断点的编号,其可以是 delete 删除某一个断点,而非全部。


举个例子:



(gdb) delete 2
(gdb) info break
Num Type Disp Enb Address What
3 catchpoint keep y exception throw matching: int


可以看到,delete 命令删除了编号为 2 的观察断点。


如果不指定 num 参数,则 delete 命令会删除当前程序中存在的所有断点。例如:



(gdb) delete
Delete all breakpoints? (y or n) y
(gdb) info break
No breakpoints or watchpoints.


### GDB禁用断点


所谓禁用,就是使目标断点暂时失去作用,必要时可以再将其激活,恢复断点原有的功能。


禁用断点可以使用 disable 命令,语法格式如下:


disable [breakpoints] [num…]


breakpoints 参数可有可无;num… 表示可以有多个参数,每个参数都为要禁用断点的编号。如果指定 num…,disable 命令会禁用指定编号的断点;反之若不设定 num…,则 disable 会禁用当前程序中所有的断点。


举个例子:



(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep y num
3 catchpoint keep y exception throw matching: int
(gdb) disable 1 2
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep n num
3 catchpoint keep y exception throw matching: int
(gdb)


可以看到,对于用 disable 命令禁用的断点,Enb 列用 n 表示其处于禁用状态,用 y 表示该断点处于激活状态。


对于禁用的断点,可以使用 enable 命令激活,该命令的语法格式有多种,分别对应有不同的功能:


enable [breakpoints] [num…] 激活用 num… 参数指定的多个断点,如果不设定 num…,表示激活所有禁用的断点  
 enable [breakpoints] once num… 临时激活以 num… 为编号的多个断点,但断点只能使用 1 次,之后会自动回到禁用状态  
 enable [breakpoints] count num… 临时激活以 num… 为编号的多个断点,断点可以使用 count 次,之后进入禁用状态  
 enable [breakpoints] delete num… 激活 num… 为编号的多个断点,但断点只能使用 1 次,之后会被永久删除。


其中,breakpoints 参数可有可无;num… 表示可以提供多个断点的编号,enable 命令可以同时激活多个断点。


仍以上面的调试环境为例,当下程序停止在第 2 行(main() 函数开头处),此时执行如下命令:



(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint keep n num
3 catchpoint keep y exception throw matching: int
(gdb) enable delete 2
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
2 hw watchpoint del y num
3 catchpoint keep y exception throw matching: int
(gdb) c
Continuing.

Hardware watchpoint 2: num

Old value = 32767
New value = 0
main () at main.c:4
4 scanf(“%d”, &num);
(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep n 0x0000555555555189 in main at main.c:2 breakpoint already hit 1 time
3 catchpoint keep y exception throw matching: int
(gdb)


可以看到,通过借助 enable delete 命令,我们激活了编号为 2 的观察断点,但其只能作用 1 次,因此当继续执行程序时,其会在程序第 4 行暂停,随时该断点会被删除。


## 如何用GDB调试多线程程序?


顾名思义,多线程程序指的就是拥有多个(≥2)线程的程序,各个线程可以独立完成一项任务,更重要的是它们可以并发执行。这也就意味着完成相同的任务,多线程程序执行花费的时间往往会少于单线程程序。


但有得也有失,多线程程序的编写更容易产生异常或 Bug(例如线程之间因竞争同一资源发生了死锁、多个线程同时对同一资源进行读和写等等),调试多线程程序,往往需要耗费更多的精力。


庆幸的是,GDB 调试器不仅仅支持调试单线程程序,还支持调试多线程程序。本质上讲,使用 GDB 调试多线程程序的过程和调试单线程程序类似,不同之处在于,调试多线程程序需要监控多个线程的执行过程,进而找到导致程序出现问题的异常或 Bug,而调试单线程程序只需要监控 1 个线程。


表 1 罗列了 GDB 调试多线程程序时常用的命令以及它们各自的功能。




| 调试命令 | 功 能 |
| --- | --- |
| info threads | 查看当前调试环境中包含多少个线程,并打印出各个线程的相关信息,包括线程编号(ID)、线程名称等。 |
| thread id | 将线程编号为 id 的线程设置为当前线程。 |
| thread apply id… command | id… 表示线程的编号;command 代指 GDB 命令,如 next、continue 等。整个命令的功能是将 command 命令作用于指定编号的线程。当然,如果想将 command 命令作用于所有线程,id… 可以用 all 代替。 |
| break location thread id | 在 location 指定的位置建立普通断点,并且该断点仅用于暂停编号为 id 的线程。 |
| set scheduler-locking off|on|step | 默认情况下,当程序中某一线程暂停执行时,所有执行的线程都会暂停;同样,当执行 continue 命令时,默认所有暂停的程序都会继续执行。该命令可以打破此默认设置,即只继续执行当前线程,其它线程仍停止执行。 |



> 
> 表 1 也仅罗列了 GDB 调试多线程程序的一部分常用命令,有关更多其他命令,读者可前往 [GDB官网]( )了解。
> 
> 
> 


接下来,我将以调试如下 C 语言多线程程序为例,给大家详细讲解表 1 中这些命令的功能和用法:



#include <stdio.h>
#include <pthread.h>
void* thread_job(void*name)
{
char * thread_name = (char*)name;
printf(“this is %s\n”,thread_name);
printf(“http://c.biancheng.net\n”);
}
int main()
{
pthread_t tid1,tid2;
pthread_create(&tid1, NULL, thread_job, “thread1_job”);
pthread_create(&tid2, NULL, thread_job, “thread2_job”);
pthread_join(tid1,NULL);
pthread_join(tid2,NULL);
printf(“this is main\n”);
return 0;
}


此程序的存储位置为 ~/demo/main.c。可以看到,此程序中包含 3 个线程,分别为 main 主线程、tid1 子线程和 tid2 子线程。


需要注意的是,将此程序编译为可供 GDB 调试的可执行程序时,需执行如下命令:


gcc main.c -o main.exe -g -lpthread


因为 pthread 线程库并不属于 Linux 系统中的默认库,所以编译、链接时就需要为 gcc 命令附加 -lpthread 参数。


### GDB查看所有线程


info threads 命令的功能有 2 个,既可以查看当前调试环境下存在的线程数以及各线程的具体信息,也可以通过指定线程的编号查看某个线程的具体信息。


info threads 命令的完整语法格式如下:


(gdb) info threads [id…]


其中,参数 id… 作为可选参数,表示要查看的线程编号,编号个数可以是多个。


以 main.exe 程序为例:


(gdb) b 6  
 Breakpoint 1 at 0x11d9: file main.c, line 6.  
 (gdb) r  
 Starting program: ~/demo/main.exe  
 [Thread debugging using libthread\_db enabled]  
 Using host libthread\_db library “/lib/x86\_64-linux-gnu/libthread\_db.so.1”.  
 [New Thread 0x7ffff7d9f700 (LWP 54283)] <–新线程  
 [New Thread 0x7ffff759e700 (LWP 54284)] <–新线程  
 [Switching to Thread 0x7ffff7d9f700 (LWP 54283)] <–该线程作为当前线程,因为它最先碰到断点


Thread 2 “main.exe” hit Breakpoint 1, thread\_job (name=0x555555556027) at main.c:6  
 6 char \* thread\_name = (char\*)name;  
 (gdb) info threads  
 Id Target Id Frame  
 1 Thread 0x7ffff7da0740 (LWP 54279) “main.exe” \_\_pthread\_clockjoin\_ex (threadid=140737351644928, thread\_return=0x0, clockid=, abstime=, block=) at pthread\_join\_common.c:145  
 \* 2 Thread 0x7ffff7d9f700 (LWP 54283) “main.exe” thread\_job (name=0x555555556027) at main.c:6  
 3 Thread 0x7ffff759e700 (LWP 54284) “main.exe” thread\_job (name=0x555555556033) at main.c:6



> 
> 其中 Id 列表示各个线程的编号(ID 号);Target Id 列表示各个线程的标识符;Frame 列打印各个线程执行的有关信息,例如线程名称,线程暂停的具体位置等。
> 
> 
> 


要知道,使用 GDB 调试多线程程序时,同一时刻我们调试的焦点都只能是某个线程,被称为当前线程。整个调试过程中,GDB 调试器总是会从当前线程的角度为我们打印调试信息。如上所示,当执行 r 启动程序后,GDB 编译器自行选择标识号为 LWP 54283(编号为 2)的线程作为当前线程,则随后打印的暂停运行的信息就与该线程有关,而没有打印出编号为 1 和 3 的暂停信息。


GDB 调试器为了方便用户快速识别出当前线程,执行 info thread 命令后,Id 列前标有 \* 号的线程即为当前线程。



> 
> 这里要提醒大家的,我们输入的调试命令并不仅仅作用于当前线程,例如 continue、next 等,默认情况下它们作用于所有线程。
> 
> 
> 


### GDB调整当前线程


用 GDB 调试多线程程序的过程中,根据需要可以随时对当前线程进行调整,这就需要用到 thead id 命令。


thread id 命令用于将编号为 id 的线程设定为当前线程。举个例子:


(gdb) thread 3  
 [Switching to thread 3 (Thread 0x7ffff759e700 (LWP 54284))] <–切换当前线程  
 #0 thread\_job (name=0x555555556033) at main.c:6  
 6 char \* thread\_name = (char\*)name;  
 (gdb) info threads  
 Id Target Id Frame  
 1 Thread 0x7ffff7da0740 (LWP 54279) “main.exe” \_\_pthread\_clockjoin\_ex (threadid=140737351644928, thread\_return=0x0, clockid=, abstime=, block=) at pthread\_join\_common.c:145  
 2 Thread 0x7ffff7d9f700 (LWP 54283) “main.exe” thread\_job (name=0x555555556027) at main.c:6  
 \* 3 Thread 0x7ffff759e700 (LWP 54284) “main.exe” thread\_job (name=0x555555556033) at main.c:6


可以看到,改变当前线程的同时,GDB 调试器为我们打印出了该线程暂停执行的具体信息。再次执行 info threads 命令可以看到,编号为 3 的线程确实成为了新的当前线程。


### GDB执行特定线程


如果想单独控制某一线程进行指定的操作,可以借助 thread apply id… command 命令实现:


(gdb) thread apply id… command


参数 id… 表示要控制的目标线程的编号,编号个数可以是多个。如果想控制所有线程,可以用 all 代替书写所有线程的编号;参数 command 表示要目标线程执行的操作,例如 next、continue 等。


举个例子:


(gdb) info threads  
 Id Target Id Frame  
 1 Thread 0x7ffff7da0740 (LWP 54279) “main.exe” \_\_pthread\_clockjoin\_ex (threadid=140737351644928, thread\_return=0x0, clockid=, abstime=, block=) at pthread\_join\_common.c:145  
 2 Thread 0x7ffff7d9f700 (LWP 54283) “main.exe” thread\_job (name=0x555555556027) at main.c:6  
 \* 3 Thread 0x7ffff759e700 (LWP 54284) “main.exe” thread\_job (name=0x555555556033) at main.c:6  
 (gdb) thread apply 2 next


Thread 2 (Thread 0x7ffff759e700 (LWP 54284)):  
 [Switching to Thread 0x7ffff759e700 (LWP 54284)] <-- 由于 3 号线程暂停执行,所以切换 3 号线程作为当前线程


Thread 3 “main.exe” hit Breakpoint 1, thread\_job (name=0x555555556033) at main.c:6  
 6 char \* thread\_name = (char\*)name;  
 (gdb) thread apply 2 next


Thread 2 (Thread 0x7ffff7d9f700 (LWP 54283)):  
 7 printf(“this is %s\n”,thread\_name);  
 (gdb)


如上所示,当我们调用 thread apply 2 next 命令对 2 号线程进行逐步调试时,3 号线程也会运行,这是为什么呢?这和 GDB 调试器的调试机制有关。


默认情况下,无论哪个线程暂停执行,其它线程都会随即暂停;反之,一旦某个线程启动(借助 next、step、continue 命令),其它线程也随即启动。GDB 调试默认的这种调试模式(称为全停止模式),一定程序上可以帮助我们更好地监控程序中各个线程的执行。



> 
> 注意,当对某个线程进行单步调试时,其它线程也会随即执行和停止,但执行的往往不只是一行代码,可能是多行代码。
> 
> 
> 


### GDB为特定线程设置断点


当调试环境中拥有多个线程时,我们可以选择为特定的线程设置断点,该断点仅对指定线程有效。


为特定的某个线程设置断点,可以使用如下命令:


(gdb) break location thread id  
 (gdb) break location thread id if…


location 表示设置断点的具体位置;id 表示断点要作用的线程的编号;if… 参数作用指定断点激活的条件,即只有条件符合时,断点才会发挥作用。



> 
> 默认情况下,当某个线程执行遇到断点时,GDB 调试器会自动将该线程作为当前线程,并提示用户 “[Switching to Thread n]”,其中 n 即为新的当前线程。
> 
> 
> 


举个例子:


(gdb) break 7 thread 3  
 Breakpoint 5 at 0x5555555551e1: file main.c, line 7.  
 (gdb) info break  
 Num Type Disp Enb Address What  
 4 breakpoint keep y 0x00005555555551d9 in thread\_job at main.c:6 breakpoint already hit 1 time  
 5 breakpoint keep y 0x00005555555551e1 in thread\_job at main.c:7 thread 3 stop only in thread 3


可以看到,我们在第 7 行代码处为 3 号线程单独设置了一个普通断点,该断点仅对 3 号线程有效。



> 
> 有关在设置断点的基础为其添加触发条件,我已经在《[GDB条件断点]( )》一节做了详细的讲解,这里不再重复赘述。
> 
> 
> 


### GDB设置线程锁


前面提到,使用 GDB 调试多线程程序时,默认的调试模式为:一个线程暂停运行,其它线程也随即暂停;一个线程启动运行,其它线程也随即启动。要知道,这种调试机制确实能帮我们更好地监控各个线程的“一举一动”,但并非适用于所有场景。


一些场景中,我们可能只想让某一特定线程运行,其它线程仍维持暂停状态。要想达到这样的效果,就需要借助 set scheduler-locking 命令。 此命令可以帮我们将其它线程都“锁起来”,使后续执行的命令只对当前线程或者指定线程有效,而对其它线程无效。


set scheduler-locking 命令的语法格式如下:


(gdb) set scheduler-locking mode


其中,参数 mode 的值有 3 个,分别为 off、on 和 step,它们的含义分别是:


* off:不锁定线程,任何线程都可以随时执行;
* on:锁定线程,只有当前线程或指定线程可以运行;
* step:当单步执行某一线程时,其它线程不会执行,同时保证在调试过程中当前线程不会发生改变。但如果该模式下执行 continue、until、finish 命令,则其它线程也会执行,并且如果某一线程执行过程遇到断点,则 GDB 调试器会将该线程作为当前线程。


举个例子:


(gdb) info threads  
 Id Target Id Frame  
 1 Thread 0x7ffff7da0740 (LWP 54279) “main.exe” \_\_pthread\_clockjoin\_ex (threadid=140737351644928, thread\_return=0x0, clockid=, abstime=, block=) at pthread\_join\_common.c:145  
 \* 2 Thread 0x7ffff7d9f700 (LWP 54283) “main.exe” thread\_job (name=0x555555556027) at main.c:6  
 3 Thread 0x7ffff759e700 (LWP 54284) “main.exe” thread\_job (name=0x555555556033) at main.c:6  
 (gdb) set scheduler-locking on  
 (gdb) next  
 7 printf(“this is %s\n”,thread\_name);  
 (gdb) info threads  
 Id Target Id Frame  
 1 Thread 0x7ffff7da0740 (LWP 54279) “main.exe” \_\_pthread\_clockjoin\_ex (threadid=140737351644928, thread\_return=0x0, clockid=, abstime=, block=) at pthread\_join\_common.c:145  
 \* 2 Thread 0x7ffff7d9f700 (LWP 54283) “main.exe” thread\_job (name=0x555555556027) at main.c:7  
 3 Thread 0x7ffff759e700 (LWP 54284) “main.exe” thread\_job (name=0x555555556033) at main.c:6  
 (gdb)


可以看到,通过执行 set scheduler-locking on 命令,接下来的 next 命令只对当前线程(2号线程)有效,其它线程仍保持原有的暂停状态。


同时,我们可以通过执行 show scheduler-locking 命令,查看各个线程锁定的状态,例如:


(gdb) show scheduler-locking  
 Mode for locking scheduler during execution is “on”.


显然,当前 set scheduler-locking 命令的值为 on。


## GDB调试多进程程序


GDB调试器不只可以调试多线程程序,还可以调试多进程程序。


对于 C 和 C++ 程序而言,多进程的实现往往借助的是`<unistd.h>`头文件中的 fork() 函数或者 vfork() 函数。举个例子:



#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if(pid == 0)
{
printf(“this is child,pid = %d\n”,getpid());
}
else
{
printf(“this is parent,pid = %d\n”,getpid());
}
return 0;
}


程序的存储路径为`~/demo/myfork.c`。可以看到,程序中包含 2 个进程,分别为父进程(又称主进程)和使用 fork() 函数分离出的子进程。


事实上在多数 Linux 发行版系统中,GDB 并没有对多进程程序提供友好的调试功能。无论程序中调用了多少次 fork() 函数(或者 vfork() 函数),从父进程中分离出多少个子进程,GDB 默认只调试父进程,而不调试子进程。


那么问题就出现了,如何使用 GDB 调试多进程程序中的子进程呢?


### GDB attach命令调试进程


首先,无论父进程还是子进程,都可以借助 attach 命令启动 GDB 调试它。attach 命令用于调试正在运行的进程,要知道对于每个运行的进程,操作系统都会为其配备一个独一无二的 ID 号。在得知目标子进程 ID 号的前提下,就可以借助 attach 命令来启动 GDB 对其进行调试。


这里还需要解决一个问题,很多场景中子进程的执行时间都是一瞬而逝的,这意味着,我们可能还未查到它的进程 ID 号,该进程就已经执行完了,何谈借助 attach 命令对其调试呢?对于 C、C++ 多进程程序,解决该问题最简单直接的方法是,在目标进程所执行代码的开头位置,添加一段延时执行的代码。


例如,将上面程序中`if(pid==0)`判断语句整体做如下修改:



if(pid == 0)
{
int num =10;
while(num==10){
sleep(10);
}
printf(“this is child,pid = %d\n”,getpid());
}


可以看到,通过添加第 3~6 行代码,该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。



> 
> 有读者可能会问,进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。
> 
> 
> 


就以调试修改后的 myfork.c 程序(已将其编译为 myfork.exe 可执行文件)为例:


[root@bogon demo]# gdb myfork.exe -q  
 Reading symbols from ~/demo/myfork.exe…done.  
 (gdb) r  
 Starting program: ~/demo/myfork.exe  
 Detaching after fork from child process 5316. <-- 子进程的 ID 号为 5316  
 this is parent,pid = 5313 <-- 父进程执行完毕


Program exited normally.  
 (gdb) attach 5316 <-- 跳转调试 ID 号为 5316 的子进程  
 …  
 (gdb) n <-- 程序正在运行,所有直接使用 next 命令就可以进行单步调试  
 Single stepping until exit from function \_\_nanosleep\_nocancel,  
 which has no line number information.  
 0x00000037ee2acb50 in sleep () from /lib64/libc.so.6  
 (gdb) n  
 Single stepping until exit from function sleep,  
 which has no line number information.  
 main () at myfork.c:10  
 10 while(num==10){  
 (gdb) p num=1  
 $1 = 1  
 (gdb) n <-- 跳出循环  
 13 printf(“this is child,pid = %d\n”,getpid());  
 (gdb) c  
 Continuing.  
 this is child,pid = 5316


Program exited normally.  
 (gdb)



> 
> 对于子进程 ID 号的获取,除了依靠 GDB 调试器打印出的信息,也可以使用 pidof 命令手动获取。有关 pidof 命令获取进程 ID 好的具体用法,我已经在《[调用GDB的几种方式]( )》一节中做了详细的讲解,这里不再重复赘述。
> 
> 
> 


### GDB显式指定要调试的进程


前面提到,GDB 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。


##### GDB follow-fork-mode选项


确切地说,对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:


(gdb) set follow-fork-mode mode


参数 mode 的可选值有 2 个:


* parent:选项的默认值,表示 GDB 调试器默认只调试父进程;


![img](https://img-blog.csdnimg.cn/img_convert/74e45325302a5f271f7663ad3bf75463.png)
![img](https://img-blog.csdnimg.cn/img_convert/f119d8bc093906ec674634c20a448d40.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618668825)**




if(pid == 0)
{
int num =10;
while(num==10){
sleep(10);
}
printf(“this is child,pid = %d\n”,getpid());
}


可以看到,通过添加第 3~6 行代码,该进程执行时会直接进入死循环。这样做的好处有 2 个,其一是帮助 attach 命令成功捕捉到要调试的进程;其二是使用 GDB 调试该进程时,进程中真正的代码部分尚未得到执行,使得我们可以从头开始对进程中的代码进行调试。



> 
> 有读者可能会问,进程都已经进行死循环了,后续代码还可以进行调试吗?当然可以,以上面示例中给出的死循环,我们只需用 print 命令临时修改 num 变量的值,即可使程序跳出循环,从而执行后续代码。
> 
> 
> 


就以调试修改后的 myfork.c 程序(已将其编译为 myfork.exe 可执行文件)为例:


[root@bogon demo]# gdb myfork.exe -q  
 Reading symbols from ~/demo/myfork.exe…done.  
 (gdb) r  
 Starting program: ~/demo/myfork.exe  
 Detaching after fork from child process 5316. <-- 子进程的 ID 号为 5316  
 this is parent,pid = 5313 <-- 父进程执行完毕


Program exited normally.  
 (gdb) attach 5316 <-- 跳转调试 ID 号为 5316 的子进程  
 …  
 (gdb) n <-- 程序正在运行,所有直接使用 next 命令就可以进行单步调试  
 Single stepping until exit from function \_\_nanosleep\_nocancel,  
 which has no line number information.  
 0x00000037ee2acb50 in sleep () from /lib64/libc.so.6  
 (gdb) n  
 Single stepping until exit from function sleep,  
 which has no line number information.  
 main () at myfork.c:10  
 10 while(num==10){  
 (gdb) p num=1  
 $1 = 1  
 (gdb) n <-- 跳出循环  
 13 printf(“this is child,pid = %d\n”,getpid());  
 (gdb) c  
 Continuing.  
 this is child,pid = 5316


Program exited normally.  
 (gdb)



> 
> 对于子进程 ID 号的获取,除了依靠 GDB 调试器打印出的信息,也可以使用 pidof 命令手动获取。有关 pidof 命令获取进程 ID 好的具体用法,我已经在《[调用GDB的几种方式]( )》一节中做了详细的讲解,这里不再重复赘述。
> 
> 
> 


### GDB显式指定要调试的进程


前面提到,GDB 调试多进程程序时默认只调试父进程。对于内核版本为 2.5.46 甚至更高的 Linux 发行版系统来说,可以通过修改 follow-fork-mode 或者 detach-on-fork 选项的值来调整这一默认设置。


##### GDB follow-fork-mode选项


确切地说,对于使用 fork() 或者 vfork() 函数构建的多进程程序,借助 follow-fork-mode 选项可以设定 GDB 调试父进程还是子进程。该选项的使用语法格式为:


(gdb) set follow-fork-mode mode


参数 mode 的可选值有 2 个:


* parent:选项的默认值,表示 GDB 调试器默认只调试父进程;


[外链图片转存中...(img-lJDWz1rd-1715712281495)]
[外链图片转存中...(img-nDumuZAN-1715712281495)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.youkuaiyun.com/topics/618668825)**

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值