GDB(GNU symbolic debugger) 是由 GNU 软件系统社区提供的调试工具。当下的 GDB 支持调试多种编程语言编写的程序,包括 C、C++、Go、Objective-C、OpenCL、Ada 等。实际场景中,GDB 更常用来调试 C 和 C++ 程序,同 GCC 配套组成了一套完整的开发环境。
何谓调试?就是让代码一步一步慢慢执行,跟踪程序的运行过程。比如,可以让程序停在某个地方,查看当前所有变量的值,或者内存中的数据;也可以让程序一次只执行一条或者几条语句,看看程序到底执行了哪些代码。
所以GDB可以帮我们:
程序启动时,可以按照我们自定义的要求运行程序,例如设置参数和环境变量;
可使被调试程序在指定代码处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等),即支持断点调试;
程序执行过程中,可以改变某个变量的值,还可以改变代码的执行顺序,从而尝试修改程序中出现的逻辑错误。
我们需要在用gcc/g++编译器编译可执行程序时需要加入 -g 参数,才能去生成可供GDB调试的可执行程序。
参数 -g :在编译的时候,加入调试信息,让该程序可以被调试器调试。
所谓调试信息就是各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等
注意⚠️:一旦生成了可调试程序文件,就不要再去修改源文件里面的信息,否则调试的时候就会出现一些错误。如果需要修改源文件,则修改后要重新生成调试文件以供调试。
这个时候,我们输入命令
gdb 文件名
就可以进入到gdb调试页面了,这个时候会生成一系列的免责条款,加上参数:–silet 就可以屏蔽掉这些免责信息。
在使用gdb调试命令的过程中,为了操作方便,有一些时候我们可以按下tab键去进行自动补全操作。
上文已经讲过如何生成可供gdb调试的调试文件,和如何启动gdb调试界面来调试程序了,接下来我们讲一下,如何退出gdb。
在gdb调试页面中,我们输入 q/quit ,即可退出gdb调试界面回到终端。
我们刚刚进入到gdb调试界面时,除了有一堆免责信息,其他的什么都是没有的。如果需要查看源代码,我们就要输入 l/list 去查看源代码。默认设置是查看主函数文件的前10行。
l/list //查看主函数文件的前10行
l/list 行号 //查看包括当前行号的前5行与后5行代码
l/list 函数名 //查看当前函数所在文件的前10行
l/list 文件名:行号 //查看指定文件行号的上下5行文件
我们可以通过命令: set list/listsize 行号 来去修改默认查看的总行数。使用命令:show list/listsize,来查看当前一次性可以查看多少行。
当我们有时候觉得调试信息太杂乱,或者不想看到当前的这些调试信息时,可以使用命令:
!clear
来将当前调试界面上的所有调试信息清空。
我们在源代码的对应位置设置断点后,可以使被调试程序在断点处暂停运行,并查看当前程序的运行状态(例如当前变量的值,函数的执行结果等)。
注意⚠️:停在断点处的那一行代码是还未执行的代码。
通过命令
b/break 行号 //在主文件(有main函数的文件)对应行上设置断点。
b/break 函数名 //在主函数指定的函数体内第一行处打上断点。
b/break 文件名:行号 //在指定文件的对应行上打上断点。
b/break 文件名:函数名 //在指定文件内的函数体内的第一行代码处打上断点
使用命令:
i/info b/break
就可以查看所有已设置的断点信息,如在第几行,断点的编号是多少等
(gdb) i b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000000b09 in main() at main.cpp:8
2 breakpoint keep y 0x00000000000009b5 in bubbleSort(int*, int) at bubble.cpp:8
使用命令:
d/del 断点编号
就可以删除错误设置的断点
断点设置完成时,默认生效。当我们希望某些断点不要生效时,就使用命令:
dis/disable 断点编号
就可以使对应编号的断点无效化。如果想要无效化的断点重新生效了话,就要使用命令:
ena/enable 断点编号
来使对应断点重新生效。
当我们设置的断点在某个循环内,我们可以通过设置条件断点,来让程序停在对应的条件处:
b 行号 if 变量名==值
之前讲过了如何去在程序上设置断点,现在就可以开始讲如何运行调试程序了。我们使用 r/run 命令 和 start 命令都可以开始运行调试程序。
它们的区别在于:
next、continue、step命令都是关于程序继续运行的命令,但是他们都之间都略有区别
执行命令 c/continue 的意思是使程序继续运行,只到下一处断点才会停下
执行命令 n/next 则是使程序向下执行一行,如果这一行是一个被调用的函数,使用next命令,不会进入到函数题内。
执行命令 s/step 也是使程序向下执行一行与 next 不同的便是遇到被调用的函数,会进入到函数体内部。而我们如果想要出这个函数体,则可以使用 finish 命令去跳出函数体。
我们可以使用命令:
p/print 变量名
display 变量名
去获取对应变量名的值
它们直接的区别是,print只会在执行print命令的时候,将对应变量的值打印一遍。而使用display命令则是打印对应的变量值,当程序继续执行后,继续输出对应变量当前的值。
被display设置过的变量也称之为自动变量,多用于循环体内。
如这么一段代码:
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
(gdb) print array[i]
$1 = 12
(gdb) n
15 for(int i = 0; i < len; i++) {
(gdb) display array[i]
1: array[i] = 12
(gdb) n
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
1: array[i] = 22
(gdb)
15 for(int i = 0; i < len; i++) {
1: array[i] = 22
(gdb)
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
1: array[i] = 27
(gdb)
15 for(int i = 0; i < len; i++) {
1: array[i] = 27
(gdb)
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
1: array[i] = 55
(gdb)
15 for(int i = 0; i < len; i++) {
1: array[i] = 55
(gdb)
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
1: array[i] = 67
(gdb)
15 for(int i = 0; i < len; i++) {
1: array[i] = 67
(gdb)
18 cout << endl;
(gdb)
我们就可以看出在循环体内,每向下执行一步,display变量都会输出一遍变量的值,而print只输出了一遍。
我们可以使用命令:
i/info display
去查看自己设置了哪些自动变量,也可以使用命令:
undisplay 自动变量编号 去删除刚刚设置的自动变量
使用命令:
ptype 变量名
则可以查看对应变量的类型
当我们程序停在一个循环体内的某一条输出语句时,如何跳出循环体呢?
首先我们将循环体内的断点删除或是停用。使用命令c 即可调到这个循环体外的下一个断点处。
使用n则是程序向下执行一行。不会跳出这个循环,使用命令s与n一样。
这个时候使用until命令则会回到未循环体的第一行,这个时候就可以重新选择是进入循环还是跳出循环了。
(gdb) run
Starting program: /home/nowcoder/Linux/lession08/mainApp
Breakpoint 2, main () at main.cpp:8
8 int array[] = {12, 27, 55, 22, 67};
(gdb) c
Continuing.
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
(gdb) display array[i]
2: array[i] = 12
(gdb) c
Continuing.
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
2: array[i] = 22
(gdb) disa 3
(gdb) until
15 for(int i = 0; i < len; i++) {
2: array[i] = 22
(gdb)
finish命令与until命令很相似,如果此时程序停在了函数体内部,如果函数体内部向下执行没有断点,则使用finish则可以跳出函数体,将程序停在执行完函数体的下一行。
如果函数体内部向下有断点,使用finish命令则就会停在断点处。
我们可以使用命令:
set var 变量名=变量值
去修改某些变量的值,从而达到修改输出的目的。
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
2: array[i] = 22
(gdb) set var array[i] = 23
(gdb) c
Continuing.
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
2: array[i] = 27
(gdb)
Continuing.
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
2: array[i] = 55
(gdb)
Continuing.
Breakpoint 3, main () at main.cpp:16
16 cout << array[i] << " ";
2: array[i] = 67
(gdb)
Continuing.
冒泡排序之后的数组: 12 23 27 55 67
===================================
选择排序之后的数组: 11 25 36 47 80
[Inferior 1 (process 1113) exited normally]
(gdb)
使用GDB调试的时候,GDB默认只能跟踪一个进程,可以在fork函数调用之前,通过指令设置GDB调试工具追踪父进程或者是跟踪子进程,默认
是跟踪父进程
。
设置调试父进程或者子进程:set follow-fork-mode parent/child
输入命令: show follow-fork-mode
gdb会告诉你,你现在正在调试的是父进程还是子进程
设置调试模式:set detach-on-fork on/off
这个是设置的意思是,使用了fork函数后,另一个进程是否脱离gdb的调试。默认是on,也就是开启状态。
换句话说也就是,处于on状态下,表示调试当前进程的时候,其他的进程继续运行,处于off状态下,其他进程被gdb挂起。
输入命令: show fdetach-on-fork on/off
gdb会告诉你,另一个进程是否会脱离。
查看调试的进程:info inferiors //会把调试的所有进程的进程号标识在这里,当前调试的会有一个*号,并且会有一个Num去记录gdb调试进程的id。
切换当前调试的进程:inferior id
使进程脱落GDB调试:detach inferiors id