既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
注意,该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、–quiet)选项,可将比部分信息屏蔽掉:
[root@bogon demo]# gdb main.exe --silent
Reading symbols from main.exe...(no debugging symbols found)...done.
(gdb)
无论使用以上哪种方式,最终都可以启动 GDB 调试器,启动成功的标志就是最终输出的 (gdb)。通过在 (gdb) 后面输入指令,即可调用 GDB 调试进行对应的调试工作。
GDB 调试器提供有大量的调试选项,可满足大部分场景中调试代码的需要。如表 1 所示,罗列了几个最常用的调试指令及各自的作用:
调试指令 | 作 用 |
---|---|
(gdb) break xxx (gdb) b xxx | 在源代码指定的某一行设置断点,其中 xxx 用于指定具体打断点的位置。 |
(gdb) run (gdb) r | 执行被调试的程序,其会自动在第一个断点处暂停执行。 |
(gdb) continue (gdb) c | 当程序在某一断点处停止运行后,使用该指令可以继续执行,直至遇到下一个断点或者程序结束。 |
(gdb) next (gdb) n | 令程序一行代码一行代码的执行。 |
(gdb) print xxx (gdb) p xxx | 打印指定变量的值,其中 xxx 指的就是某一变量名。 |
(gdb) list (gdb) l | 显示源程序代码的内容,包括各行代码所在的行号。 |
(gdb) quit (gdb) q | 终止调试。 |
如上所示,每一个指令既可以使用全拼,也可以使用其首字母表示。另外,表 1 中罗列的指令仅是冰山一角,GDB 还提供有大量的选项,可以通过 help 选项来查看。有关 help 选项的具体用法,读者可阅读《GDB查看命令》一节,这里不再做具体赘述。
仍以 main.exe 可执行程序为例,接下来为读者演示表 1 中部分选项的功能和用法:
(gdb) l <-- 显示带行号的源代码
1 #include <stdio.h>
2 int main ()
3 {
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;
(gdb) <-- 默认情况下,l 选项只显示 10 行源代码,如果查看后续代码,安装 Enter 回车即可
11 }
12 return 0;
13 }
(gdb) b 7 <-- 在第 7 行源代码处打断点
Breakpoint 1 at 0x400504: file main.c, line 7.
(gdb) r <-- 运行程序,遇到断点停止
Starting program: /home/mozhiyan/demo1/main.exe
Breakpoint 1, main () at main.c:7
7 while (n <= 100)
Missing separate debuginfos, use: debuginfo-install glibc-2.17-55.el7.x86_64
(gdb) p n <-- 查看代码中变量 n 的值
$1 = 1 <-- 当前 n 的值为 1,$1 表示该变量所在存储区的名称
(gdb) b 12 <-- 在程序第 12 行处打断点
Breakpoint 2 at 0x40051a: file main.c, line 12.
(gdb) c <-- 继续执行程序
Continuing.
Breakpoint 2, main () at main.c:12
12 return 0;
(gdb) p n <-- 查看当前 n 变量的值
$2 = 101 <-- 当前 n 的值为 101
(gdb) q <-- 退出调试
A debugging session is active.
Inferior 1 [process 3080] will be killed.
Quit anyway? (y or n) y <-- 确实是否退出调试,y 为退出,n 为不退出
[root@bogon demo]#
后续章节会对以上指令做详细的讲解,这里简单了解即可,不必深究。
调用GDB调试器的4种方式
《GDB调试C/C++程序》一节演示了用 GDB 调试 C(或者 C++)程序的整个过程,其中对 main.exe 文件启动 GDB 调试,执行的指令为:
[root@bogon demo]# gdb main.exe
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
…
(gdb)
要知道,这仅是调用 GDB 调试器最常用的一种方式,GDB 调试器还有其它的启动方式。并且,为了满足不同场景的需要,启动 GDB 调试器时还可以使用一些参数选项,从而控制它启动哪些服务或者不启动哪些服务。
调用GDB的方式
总的来说,调用 GDB 调试器的方法有 4 种。
- 直接使用 gdb 指令启动 GDB 调试器:
[root@bogon demo]# gdb
ubuntu64@ubuntu64-virtual-machine:~/demo$ gdb
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
Type “apropos word” to search for commands related to “word”.
(gdb)
此方式启动的 GDB 调试器,由于事先未指定要调试的具体程序,因此需启动后借助 file 或者 exec-file 命令指定(后续章节会做详细讲解)。
2) 调试尚未执行的程序
对于具备调试信息(使用 -g 选项编译而成)的可执行文件,调用 GDB 调试器的指令格式为:
gdb program
其中,program 为可执行文件的文件名,例如上节创建好的 main.exe。
3) 调试正在执行的程序
在某些情况下,我们可能想调试一个当前已经启动的程序,但又不想重启该程序,就可以借助 GDB 调试器实现。
也就是说,GDB 可以调试正在运行的 C、C++ 程序。要知道,每个 C 或者 C++ 程序执行时,操作系统会使用 1 个(甚至多个)进程来运行它,并且为了方便管理当前系统中运行的诸多进程,每个进程都配有唯一的进程号(PID)。
如果需要使用 GDB 调试正在运行的 C、C++ 程序,需要事先找到该程序运行所对应的进程号。查找方式很简单,执行如下命令即可:
pidof 文件名
比如,我们将上节创建的 main.c 源文件修改为:
#include <stdio.h>
int main()
{
int num = 1;
while(1)
{
num++;
}
return 0;
}
执行 gcc main.c -o main.exe -g 编译指令,获得该源程序对应的具备调试信息的 main.exe 可执行文件,并在此基础上执行:
[root@bogon demo]# ./main.exe
<–程序一直运行
显然,程序中存在死循环(5~8 行),它会一直执行。此时,借助 pidof 指令即可获取它对应的进程号:
[root@bogon demo]# pidof main.exe
1830
可以看到,当前正在执行的 main.exe 对应的进程号为 1830。在此基础上,可以调用 GDB 对该程序进行调试,调用指令有以下 3 种形式:
- gdb attach PID
- gdb 文件名 PID
- gdb -p PID
其中,PID 指的就是要调取的程序对应的进程号。
以调试进程号为 1830 的程序为例,执行如下指令:
[root@bogon demo]# gdb -p 1830
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
0x00005645319c613c in main () at main.c:6
6 num++;
注意,当 GDB 调试器成功连接到指定进程上时,程序执行会暂停。如上所示,程序暂停至第 6 行代码 num++ 的位置,此时可以通过断点调试、逐步运行等方式监控程序的执行过程。例如:
(gdb) l <-- 查看源码以及各行行号
1 #include<stdio.h>
2 int main()
3 {
4 int num = 1;
5 while(1){
6 num++;
7 }
8 return 0;
9 }
(gdb) b 6 <–在程序第 6 行代码处打断点
Breakpoint 1 at 0x5645319c6138: file main.c, line 6.
(gdb) c <–令程序进行执行,其会在下一个断点处停止
Continuing.
Breakpoint 1, main () at main.c:6
6 num++;
(gdb) p num <-- 查看当前 num 的值
$2 = 47100335
注意,当调试完成后,如果想令当前程序进行执行,消除调试操作对它的影响,需手动将 GDB 调试器与程序分离,分离过程分为 2 步:
- 执行 detach 指令,使 GDB 调试器和程序分离;
- 执行 quit(或 q)指令,退出 GDB 调试。
4) 调试执行异常崩溃的程序
除了以上 3 种情况外,C 或者 C++ 程序运行过程中常常会因为各种异常或者 Bug 而崩溃,比如内存访问越界(例如数组下标越界、输出字符串时该字符串没有 \0 结束符等)、非法使用空指针等,此时就需要调试程序。
值得一提的是,在 Linux 操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为 core 文件,Linux 系统所具备的这种功能又称为核心转储(core dump)。幸运的是,GDB 对 core 文件的分析和调试提供有非常强大的功能支持,当程序发生异常崩溃时,通过 GDB 调试产生的 core 文件,往往可以更快速的解决问题。
默认情况下,Linux 系统是不开启 core dump 这一功能的,读者可以借助执行ulimit -c
指令来查看当前系统是否开启此功能:
[root@bogon demo]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
…
其中,如果 core file size(core 文件大小)对应的值为 0,表示当前系统未开启 core dump 功能。这种情况下,可以通过执行如下指令改变 core 文件的大小:
[root@bogon demo]# ulimit -c unlimited
[root@bogon demo]# ulimit -a
core file size (blocks, -c) unlimited
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
…
其中,unlimited 表示不限制 core 文件的大小。
由此,当程序执行发生异常崩溃时,系统就可以自动生成相应的 core 文件。
举个例子,修改 main.c 源程序文件中的代码为:
#include <stdio.h>
int main()
{
char \*p = NULL;
\*p = 123;
return 0;
}
重新编译,即执行如下指令:
[root@bogon demo]# gcc main.c -o main.exe -g
[root@bogon demo]# ./main.exe
Segmentation fault (core dumped) <–发生段错误,并生成了 core 文件
[root@bogon demo]# ls
core main.c main.exe
段错误又称为访问权限冲突,指的是当前程序访问了不可访问的存储空间,比如访问的不存在的空间,又或者是受系统保护的内存空间。
观察此程序不难发现,由于 p 指针初始化为 NULL,即不指向任何存储空间,但后续却执行*p=123
操作,显然是不可行的。因此,该程序执行时会发生崩溃,Linux 系统会记录必要的崩溃信息,并存储到 core 文件中。
默认情况下,core 文件的生成位置同该程序所在的目录相同。当然我们也可以指定 core 文件的生成的位置,感兴趣的读者可自行研究,这里不再介绍。
对于 core 文件的调试,其调用 GDB 调试器的指令为:
[root@bogon demo]# gdb main.exe core
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
…
Reading symbols from main.exe…
[New LWP 4296]
warning: Unexpected size of section .reg-xstate/4296' in core file. Core was generated by
./main.exe’.
Program terminated with signal SIGSEGV, Segmentation fault.
warning: Unexpected size of section `.reg-xstate/4296’ in core file.
#0 0x00005583b933013d in main () at main.c:5
5 *p = 123;
可以看到,程序发生崩溃的位置是在 main.c 中的第 5 行。甚至于,对于 core 文件中记录的崩溃信息,可以使用 where、print、bt 等指令查看,有关这些指令的功能和用法,由于并非本节重点,这里不再具体赘述,后续章节会做详细讲解。
GDB调试器启动可用参数
表 1 罗列了一些在启动 GDB 调试器时常用的指令参数,以及它们各自的功能。
参 数 | 功 能 |
---|---|
-pid number -p number | 调试进程 ID 为 number 的程序。 |
-symbols file -s file | 仅从指定 file 文件中读取符号表。 |
-q -quiet -silent | 取消启动 GDB 调试器时打印的介绍信息和版权信息 |
-cd directory | 以 directory 作为启动 GDB 调试器的工作目录,而非当前所在目录。 |
–args 参数1 参数2… | 向可执行文件传递执行所需要的参数。 |
其中有些参数,我们已经在前面的学习给大家做了具体的演示,这里不再重复赘述,读者可自行尝试使用。除此之外,启动 GDB 调试器时还有其它参数指令可以使用,感兴趣的读者可查阅 GDB 官网做系统了解。有关表 1 以及 GDB 调试器支持的其它指令,后续章节用到时会做详细讲解。
gdb run(r)命令:启动程序
使用 GDB 调试器调试程序的过程,其实就是借助 GDB 调试器来监控程序的执行流程,进而发现程序中导致异常或者 Bug 的代码。通过前面章节的学习,读者已经学会了如何启动 GDB 调试器,在此基础上,本节继续为大家讲解如何在 GDB 调试器中启动(运行)程序,以及启动程序过程中的一些注意事项。
根据不同场景的需要,GDB 调试器提供了多种方式来启动目标程序,其中最常用的就是 run 指令,其次为 start 指令。也就是说,run 和 start 指令都可以用来在 GDB 调试器中启动程序,它们之间的区别是:
- 默认情况下,run 指令会一直执行程序,直到执行结束。如果程序中手动设置有断点,则 run 指令会执行程序至第一个断点处;
- start 指令会执行程序至 main() 主函数的起始位置,即在 main() 函数的第一行语句处停止执行(该行代码尚未执行)。
可以这样理解,使用 start 指令启动程序,完全等价于先在 main() 主函数起始位置设置一个断点,然后再使用 run 指令启动程序。另外,程序执行过程中使用 run 或者 start 指令,表示的是重新启动程序。
问一个问题,GDB 调试器启动后是否就可以直接使用 run 或者 start 指令了呢?答案当然是否定的。我们知道,启动 GDB 调试器的方式有多种,其中简单的方法就是直接使用 gdb 指令,例如:
[root@bogon demo]# gdb
GNU gdb (GDB) 8.0.1
Copyright © 2017 Free Software Foundation, Inc.
… <-- 省略部分输出信息
Type “apropos word” to search for commands related to “word”.
(gdb)
注意,使用此方式启动的 GDB 调试器,尚未指定要调试的目标程序,何谈使用 run 或者 start 指令呢?
不仅如此,在进行 run 或者 start 指令启动目标程序之前,还可能需要做一些必要的准备工作,大致包括以下几个方面:
- 如果启动 GDB 调试器时未指定要调试的目标程序,或者由于各种原因 GDB 调试器并为找到所指定的目标程序,这种情况下就需要再次手动指定;
- 有些 C 或者 C++ 程序的执行,需要接收一些参数(程序中用 argc 和 argv[] 接收);
- 目标程序在执行过程中,可能需要临时设置 PATH 环境变量;
- 默认情况下,GDB 调试