▶️ 初识GDB
GDB 功能
GDB 是 GNU 开源组织发布的一个强大的 UNIX 下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像 VC、BCB 等 IDE 的调试,但如果你是在 UNIX 平台下做软件,你会发现 GDB 这个调试工具有比 VC、BCB 的图形化调试器更强大的功能。一般来说,GDB 主要帮忙你完成下面几个方面的功能:
- 设置断点(断电可以是条件表达式),使程序在指定的代码行上暂停执行,便于观察。
- 单步执行程序,便于调试。
- 查看程序中变量值得变化。
- 动态改变程序的执行环境。
- 分析崩溃程序产生的 core 文件。
安装GDB
在终端输入以下命令安装 GDB:
sudo apt-get update
sudo apt-get install gdb
#如果是centos 是yum安装
可以使用 gdb -version 查看版本号 ,进入后 gdb 可以 q退出
进入GDB
GDB 是一个命令行方式的调试工具,它不同于我们在 Windows 下常见的 Turbo C、VC 等图形化程序开发工具。GDB 使用非常简单,只要在 Linux 的命令行提示符下输入 gdb,系统便会启动 GDB:
想要退出可以输入 quit 命令。我们也可以在 gdb 后面给出文件名,直接指定想要调试的程序,GDB 就会自动调用这个可执行文件进行调试,命令形式如下:
$ gdb filename
告诉 GDB 装入名为 filename 的可执行文件进行调试。
另外,为了使 GDB 正常工作,必须使程序在编译的时候包含调试信息,这需要在 GCC 编译时加上
-g或者-ggbb选项。调试信息包好了程序中的每个变量的类型和在可执行文件中的地址映射以及源代码的行号。
而 GDB 正是利用这些信息使源代码和机器码相关联。
❗ GDB常用命令
GDB 支持很多的命令使用户能实现不同的功能,有简单的文件装入命令,有允许程序员检查所调用的堆栈内容的复杂命令,为方便本节后续内容的讲解和方便学员查阅,这里先将 GDB 常用命令列出:
| 命令 | 含义描述 |
|---|---|
| file | 装入想要的调试的可执行文件。 |
| run | 执行当前被调试的程序。 |
| kill | 终止正在调试的程序。 |
| step | 执行一行源代码而且进入函数内部。 |
| next | 执行一行源代码但不进入函数内部。 |
| break | 在代码里设置断点,这将使程序执行到这里时被挂起。 |
| 打印表达式或变量的值,或打印内存中某个变量开始的一段连续区域的值,还以用来对变量进行赋值。 | |
| display | 设置自动显示的表达式或变量,当程序停住或在单步追踪时,这些变量会自动显示其当前值。 |
| list | 列出产生执行文件的源代码的一部分。 |
| quit | 退出 GDB。 |
| watch | 使你能监视一个变量的值而不管它何时被改变。 |
| backtrace(或 bt) | 回溯追踪。 |
| frame n | 定位到发生错误的代码段,n 为 backtrace 命令的输出结果中的行号。 |
| examine | 查看内存地址中的值。 |
| jump | 是程序跳转执行。 |
| signal | 产生信号量。 |
| return | 强制函数返回。 |
| call | 强制调用函数。 |
| make | 使用户不退出 GDB 就可以重新产生可执行文件。 |
| shell | 使用户不离开 GDB 就执行 Linux 的 shell 命令。 |
❗ GDB 常用指令
GDB调试初试
我们先通过一个具体的简单实例来向大家介绍如何使用 GDB 调试器来分析程序中的错误,帮助快速入门。下面是一段 C 语言代码:
#include <stdio.h>
int main(void){
int input = 0;
printf("input an interger:\n");
scanf("%d",input);
printf("the interger you input is%d\n",input);
return 0;
}
一眼看出这里是由于scanf里面input未加地址符&错误
来试试gdb调试:
我们先把这段代码保存在桌面上,命名为 demo.c。
然后使用 GCC 编译 demo.c,并且加上 -ggdb 调试选项:
$ cd Desktop
$ ls #查看当前文件
$ gcc -ggdb demo.c -o demo
为了更快的发现错误的所在,我们使用 GDB 进行一个简单的调试:
当出现提示符(gdb)的时候,表示调试器已经做好了准备可以进行调试了,现在可以通过 run 命令来让程序开始在 GDB 的监控下运行。
分析一下 GDB 给出的输出结果,不难看出,程序是由于段错误而导致异常中止的,说明内存操作出了问题,具体发生问题的地方是在调用 _IO_vfscanf——internal() 的时候。为了得到更有价值的信息,可以使用 GDB 提供的溯源跟踪命令即 backtrace 命令,执行结果如下:
通过以上的信息不难看出 GDB 已经将错误信息定位到了第五行,现在可以仔细检查一下了。
使用 GDB 提供的 frame 命令可以定位到发生错误代码的代码段,该命令后面跟着的数值是在 backtrace 中找到的行号。
通过上面的调试我们可以确定发生错误的信息是 input,之后我们将 input 改为 &input 即可。
修改后退出 GDB,然后执行以下命令重新编译并运行:
$ gcc -ggdb demo.c -o demo
$ ./demo
至此,一个简易的调试完成,可能你暂时没有感受的 GDB 带来的便利,因为我们只是对 GDB 的使用有一个初步的认识,下一节开始我们将对其进行详细的介绍。
总结
本小节我们学习了以下知识点:
- GDB 概述
- GDB 常用命令
- GDB 调试初步
▶️ GDB 常用命令详解
GDB 的命令有很多,善于使用 GDB 命令能够提升我们调试的效率。本小节我们将对 GDB 常用命令进行详细的介绍,学会如何使用这些命令以及在什么场景使用它们是核心内容。
知识点
- 代码实例
- list 命令
- run 命令
- break 命令
- info 命令
- 单步命令
- continue 命令
- print 命令
- GDB 查看命令
代码例子
将以下代码保存在桌面上,命名为 demo.c。
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int sum[10] ={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int i;
int array1[10] ={11, 22, 33, 44, 55, 66, 77, 88, 99, 00};
int array2[10] ={1, 2, 3, 4, 5, 4, 3, 2, 1, 0};
for (i = 0; i < 10; i++)
{
sum[i] = add(array1[i], array2[i]);
}
}
使用命令 gcc –ggdb demo.c –o demo 编译上述程序,得到包含调试信息的二进制文件 demo,执行 gdb demo 命令进入调试状态:
list 命令
在 GDB 中运行 list 命令(缩写 l)可以列出代码,list 的具体形式包括:
list < linenum >
list <linenum>:显示程序第linenum行周围的 10 行代码,如:
list 10
list < function >
list <function>:显示函数名为function的函数周围的 10 行,如:
list main
list add
list
list:输出从上次调用list命令开始往后的 10 行程序代码(如果不够 10 行则会输出到最后)。
list
list -
list -:输出从上次调用list命令开始往前的 10 行程序代码。
list 14
list -
run 命令
将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
/* 函数声明 */
int max(int num1, int num2);
int main ()
{
/* 局部变量定义 */
int a,b,ret;
scanf("%d",&a);
scanf("%d",&b);
/* 调用函数来获取最大值 */
ret = max(a, b);
printf( "Max value is : %d\n", ret );
return 0;
}
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2)
{
/* 局部变量声明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
这个时候我们执行 run 命令运行程序:
出现空白后,依次输入参数并回车。
break 命令
添加断点
在 GDB 中用 break 命令来设置断点,设置断点的方法包括:
break <function>:在进入指定函数时停住。break <linenum>:在指定行号停住。break +offset/break -offset:在当前行号的前面或后面的offset行停住,offset 为自然数。break filename:linenum:在源文件filename的linenum行处停住。break *address:在程序运行的内存地址处停住。break:break命令没有参数时,表示在下一条指令处停住。break ... if <condition>:“...” 可以是上述的break、break +offset / break –offset中的参数,condition表示条件,在条件成立时停住。比如在循环体中,可以设置break if i=100,表示当i为 100 时停住程序。
这里使用 run 命令的 C 程序代码,没有保存的可以将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
/* 函数声明 */
int max(int num1, int num2);
int main ()
{
/* 局部变量定义 */
int a,b,ret;
scanf("%d",&a);
scanf("%d",&b);
/* 调用函数来获取最大值 */
ret = max(a, b);
printf( "Max value is : %d\n", ret );
return 0;
}
/* 函数返回两个数中较大的那个数 */
int max(int num1, int num2)
{
/* 局部变量声明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
首先是 break <function> ,指定想要断点的函数,在进入指定函数时停住。
-
输入
run命令运行,依次写入参数,程序会在运行到指定函数时停住。 -
想要执行下一步,可以输入
next命令,其简写为n。
然后我们介绍 break <linenum>,在指定行数停住。
查看断点
在 GDB 下查看断点使用命令 info break 或者简写 i b。
打印出了刚刚添加的 max 函数的断点信息:编号、类型、显示状态、是否启用、地址、其他信息。
删除断点
在 GDB 上删除断点使用命令 delet 断点Num,简写 d 断点num。
num可以用info break 查看编号
删除后再次查看断点,提示当前没有断点,即删除成功。
禁用断点
在 GDB 下禁用断点使用命令 disable num。
num可以用info break 查看编号
可以看到字段 Enb 已经变为 n 了,表示这个断点已经被禁用。
info 命令
info 命令可以在调试时用来查看寄存器、断点、观察点和信号等信息,常用命令如下:
info registers:查看除了浮点寄存器以外的寄存器。info all-registers:查看所有寄存器,包括浮点寄存器。info registers:查看所指定的寄存器。info break查看断点信息。info watchpoints列出当前所设置的所有观察点。info line命令来查看源代码在内存中的地址(后面可以跟行号、函数名、文件名:行号、文件名:函数名等多种形式)。info threads可以看多线程。
其他关于 info 的命令可以输入 info 进行查看:
单步命令
在调试过程中,next 命令用于单步执行。next 的单步执行不会进入函数的内部,与 next 对应的 step(缩写 s)命令则在单步执行一个函数时,会进入其内部。
#include<stdio.h>
int add(int a, int b)
{
return a + b;
}
int main()
{
int sum[10] ={0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int i;
int array1[10] ={11, 22, 33, 44, 55, 66, 77, 88, 99, 00};
int array2[10] ={1, 2, 3, 4, 5, 4, 3, 2, 1, 0};
for (i = 0; i < 10; i++)
{
sum[i] = add(array1[i], array2[i]);
}
}
下面演示了 step 命令的执行情况,在 15 行的 add() 函数调用处执行 step 会进入其内部的 return a+b; 语句:
然后我们来看与 next 命令的区别:
next 的单步执行不会进入函数的内部。直接执行完函数。
单步执行的更复杂用法:
step <count>:单步跟踪,如果有函数调用,则进入该函数(进入函数的前提是,此函数被编译有 debug 信息)。
step后面不加count表示一条条地执行,加表示执行后面的count条指令,然后再停住。
next <count>:单步跟踪,如果有函数调用,它不会进入该函数。
同样地,next后面不加count表示一条条地执行,加表示执行后面的count条指令,然后再停住。
set step-mode:set step-mode on用于打开step-mode模式,这样,在进行单步跟踪时,程序不会因为没有 debug 信息而不停住,这个参数的设置可便于查看机器码。set step-mod off用于关闭step-mode模式。
finish:运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
until:一直在循环体内执行单步,退不出来是一件令人烦恼的事情,until命令可以运行程序直到退出循环体。
continue 命令
当程序被停住后,可以使用 continue 命令(缩写 c)恢复程序的运行直到程序结束,或到达下一个断点。
将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
int main()
{
int a=0;
for(int i=0; i<10; i++)
a+=i;
}
为了方便理解,我们搭配着 watch 命令进行使用,在使用 watch 时步骤如下:
- 使用
break命令在要观察的变量所在处设置断点。 - 使用
run命令执行,直到断点。 - 使用
watch命令设置观察点。 - 使用
continue命令观察设置的观察点是否有变化。
print 命令
在调试程序时,当程序被停住时,可以使用 print 命令(缩写为 p),print 的输出格式包括:
x:按十六进制格式显示变量。d:按十进制格式显示变量。u:按十六进制格式显示无符号整型。o:按八进制格式显示变量。t:按二进制格式显示变量。a:按十六进制格式显示变量。c:按字符格式显示变量。f:按浮点数格式显示变量。
将以下代码保存在桌面上,命名为 demo.c。编译这段代码,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include<stdio.h>
int main()
{
int a[] = {1,2,3,4,5,6};
return 0;
}
我们现在来理解这些是什么意思:
print a[4]:打印a[4]的值。print a+4:打印a[4]的地址。print &a[4]:打印a[4]的地址。x a[4]:访问a[4]值所代表的内存,即打印a[4]值代表内存里面的值。x a+4:访问指针a+4代表内存里面的值,即 5。x &a[4]:访问指针a+4代表内存里面的值,即 5。x &(a+4):访问a+4指针所在的地址,不存在。
print 就是打印给定变量(参数是什么,就打印什么),x 打印给定变量代表的内存地址里的值(即 x 后面的参数 是地址值,打印的是地址所在内存单元的值)。
更多用法 :# GDB print命令高级用法
当 print 命令不指定任何 options 参数时,print 和 /fmt 之间不用添加空格,例如以十六进制的形式输出 num 整形变量的值,执行命令为 (gdb) print/x num。
GDB 查看命令 help
GDB 中调试的命令非常的多,我们暂时只对上述的命令进行详细介绍,具体可以通过 help 命令查看。
查看命令的种类
查看各个种类的命令可以进入到 GDB 的命令行模式中,使用 help 命令查看,使用方式:
(gdb) help
查看具体某个类型中的命令
使用 help 命令,向我们展示了命令总体被划分成了 12 种,其中每一种又会包含许多的命令,查看各个种类种的命令使用方法:
(gdb) help <class>
其中 <class> 表示 help 命令显示的 GDB 中的命令的种类,例如:
列举的只是 breakpoints 这个种类中的一小部分,关于 breakpoints 相关的命令非常多,可以输入 return 继续查看。
命令的具体使用方式
如果我们想知道具体某条命令的使用方法,仍然可以使用 help 命令,使用方法如下:
(gdb) help <command>
<command> 表示的是具体的一条命令,会显示出这条命令的含义以及使用方式,例如:
总结
本小节我们学习了以下知识点:
- 代码实例
- list 命令
- run 命令
- break 命令
- info 命令
- 单步命令
- continue 命令
- print 命令
- GDB 查看命令
▶️ GDB 多线程与多进程调试
本小节我们将学习 GDB 的多线程与多进程调试。
多线程顾名思义就是实现多个线程并发执行,简单的说就是同时处理多项任务。我们在开发过程中会经常使用到多线程,当然出现的问题也是不可避免的。
在 C 语言中创建多进程程序需要使用 fork 相关的一些函数,调用一次 fork 函数就会创建一个进程。
多进程调试时,我们需要对调试的进程和未调试的进程进行设置。
知识点
- GDB 调试多线程
- 查看线程的相关信息
- GDB 调试多进程
- 调试多进程实际操作
➡️ GDB调试多线程
多线程调试的主要任务是准确及时地捕捉被调试程序线程状态的变化的事件,并且 GDB 针对根据捕捉到的事件做出相应的操作,其实最终的结果就是维护一个叫 thread list 的链表。
查看线程的相关信息
使用 GDB 调试多线程的程序时,可以使用下面的命令获取线程的信息,命令展示如下:
info threads
显示可以调试的所有线程,GDB 会为每个线程分配一个 Id,编号一般从 1 开始,当前调试的线程编号前有 *。
调试多线程命令
调试多线程的程序和调试普通的单线程的程序是不同的。当我们调试多线程中的某一个线程时,需要对其他线程的状态做一些设置,包括主线程调试时其他线程是否运行以及运行时需要执行的命令。下面是调试多线程命令的详细介绍:
1. thread ID
-
调试线程时,可以做到切换线程,使用命令:
thread [ID]通过线程的编号切换到指定的线程:
thread 3 //切换到编号为 3 的线程。
2. thread apply ID 命令
-
运行中的线程指定执行命令,命令格式:
thread apply [ID……] [command]这个命令可以指定多个 ID,使用
all表示指定所有的线程。command表示执行的命令:(gdb) thread apply 1 continue // 1号线程执行continue命令。 (gdb) thread apply 1 2 continue //1号和二号线程执行continue命令 (gdb) thread apply all continue //所有的线程执行continue命令
3. 锁定其他执行线程 set scheduler-locking mode
-
锁定执行的线程,命令格式:
set scheduler-locking [mode]该命令表示当调试一个线程时其他的线程是否继续执行。
mode有三种选项,分别是:off、on、step。- 当
mode为off时,表示不锁定线程,这是 GDB 的默认选项。 - 当
mode的值为on时,表示锁定其他的线程,只有当前线程执行。 - 当
mode为step时,表示如果使用step单步执行,只有被调试的程序执行。
- 当
在线程中设置断点
我们可以设置断点在所有的线程上或是在某个特定的线程,使用以下命令:
break <linespec> thread [ID]
break <linespec> thread [ID] if ...
linespec 指定了断点设置在的源程序的行号,ID 表示线程的编号。
例如:把线程3设置到程序14行
例子:
下面我们以以下多线程的程序为例,在 GDB 模式下测试各种命令。由于 pthread 不是 Linux 下的默认的库,也就是在链接的时候,无法找到 phread 库中哥函数的入口地址,于是链接会失败。在 GCC 编译的时候,附加要加 -lpthread 参数即可解决。
将以下代码保存在桌面上,命名为 demo.c,执行命令 gcc -ggdb demo.c -o demo -pthread,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
static void *thread_job(void *s)
{
printf("this is 1\n");
}
static void *thread_job1(void *s)
{
printf("this is 2\n");
}
int main(void)
{
pthread_t tid,tid1;
pthread_create(&tid, NULL, thread_job, NULL);
pthread_create(&tid1, NULL, thread_job1, NULL);
pthread_join(tid,NULL);
pthread_join(tid1,NULL);
exit(0);
}
在需要调试的地方打下断点,run 运行到断点处:
run 运行到断点处,info threads 可以查看被调试的线程:
使用 thread Id 切换线程号:
可以看到线程号从 2 变成了 1。
使用thread apply all bt 让所有线程打印堆栈信息:(bt是追踪命令)
最后执行 c (continue)到最后,调试结束:
➡️ GDB 调试多进程
GDB 是 linux 系统上常用的 C/C++ 调试工具,功能十分强大。对于较为复杂的系统,比如多进程系统,如何使用 GDB 调试呢?考虑下面这个三进程系统:
Proc2 是 Proc1 的子进程,Proc3 又是 Proc2 的子进程。如何使用 GDB 调试 proc2 或者 proc3 呢?
实际上,GDB 没有对多进程程序调试提供直接支持。例如,使用 GDB 调试某个进程,如果该进程 fork 了子进程,GDB 会继续调试该进程,子进程会不受干扰地运行下去。
如果你事先在子进程代码里设定了断点,子进程会收到 SIGTRAP 信号并终止。
那么该如何调试子进程呢?其实我们可以利用 GDB 的特点或者其他一些辅助手段来达到目的。此外,GDB 也在较新内核上加入一些多进程调试支持。
多进制调试命令
在 C 语言中创建多进程程序需要使用 fork 相关的一些函数,调用一次 fork 函数就会创建一个进程。
多进程调试时,我们需要对调试的进程和未调试的进程进行设置。下面介绍的一些命令是我们在调试时经常使用到的。
1. 设置调试进程 set follow-fork-mode
-
GDB 默认调试的是父进程,我们可以设置调试的进程,使用命令:
set follow-fork-mode <mode>其中
mode为设置调试的进程:可以是child,也可以是parent。当mode为parent时,程序在调用fork后调试父进程,子进程不会受到影响。当mode为child时,程序在调用fork后调试子进程,父进程不会受到影响。
2. 查看设置的调试进程 show
-
查看 GDB 中设置的
follow-fork-mode可以使用命令:show follow-fork-mode
3. 调试多进程 detach-on-fork
-
在 GDB 中调试多进程时,可以只调试一个进程,也可以同时调试两个进程,这个和 GDB 中的
detach-on-fork的设置有关,使用命令:set detach-on-fork <mode>mode可以为on,也可以为off。当mode为on时,表示程序只调试一个进程(可以是父进程、子进程),这是 GDB 的默认设置。当mode为off时,父子进程都在 GDB 的控制之下,其中一个进程正常的调试,另一个会被设置为暂停状态。
4. 查看detach-on-fork
-
查看 GDB 中设置的
detach-on-fork可以使用命令:show detach-on-fork
执行状态记录inferior结构
GDB 将每一个被调试程序的执行状态记录在一个名为
inferior的结构中。一般情况下一个inferior对应一个进程,每个不同的inferior有不同的地址空间。inferior有时候会在进程没有启动的时候就存在。
1. 查看 info
-
查看当前调试的所有的
inferior,使用命令:info inferiors当前调试的进程前有
*。
2. 切换 inferior
-
切换进程使用命令
inferior:inferior <num>表示切换到
id为num的inferior:inferior 2切换到 2 号进程。
实际操作
下面我们以以下多进程的程序为例,在 GDB 模式下测试各种命令。将以下代码保存在桌面上,命名为 demo.c,执行命令gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("fork()");
}
if(pid == 0)
{
printf("this is child,pid = %d\n",getpid());
}
else
{
printf("this is parent,pid = %d\n",getpid());
}
exit(0);
}
show follow-fork-mode
查看当前调试的 fork 模式,如下图,默认为父进程,如果想设置为子进程,可以使用 set follow-fork-mode child。
show detach-on-fork
查看 detach-on-fork 的模式。设置为 on 表示只调试父子进程中的一个,off 表示父子进程都在 GDB 的控制之下,其中一个进程正常调试另一个进程会被设置为暂停状态。
info inferiors
显示 GDB 调试的所有进程。inferior [进程编号] 可以切换到特定的 inferiors 进行调试。其中 * 代表正在调试的进程。
设置捕捉点
捕捉点是一种特殊类型的断点,用来在设置在某些事件发生时中断程序,使用 catch 命令可以捕获,当调用 fork 函数会产生中断。
实例
总的相关操作及调试信息如下:
(gdb) show follow-fork-mode //显示默认的 follow-fork-mode 配置
(gdb) show detach-on-fork //显示默认的 detach-on-fork 配置
(gdb) set follow-fork-mode child //设置
follow-fork-mode 为 child
(gdb) set detach-on-fork off //设置 detach-on-fork off 为 off
(gdb) catch fork //设置捕获点中断
(gdb) run //运行程序
(gdb) s //执行一行代码,如果有函数,进入函数,相当于step into 单步追踪
(gdb) info inferiors //显示程序运行的进程
(gdb) inferior 1 //切换到一号进程
(gdb) info inferiors
(gdb) c //执行第一个进程
(gdb) info inferiors
总结
本小节我们学习了以下知识点:
- GDB 调试多线程
- 查看线程的相关信息
- GDB 调试多进程
- 调试多进程实际操作
▶️ GDB 查看栈信息
当我们阅读代码和查找 BUG 时,往往有一个烦恼,就是我们不知道函数的调用顺序。而这些函数调用顺序对应我们理解程序结构,程序运行过程是很有帮助的。但是程序的调用过程往往是很复杂的,而且可能是多层嵌套,跨文件调用的。这时候如果靠人工去查找,这将是一件非常大工作量的事情。GDB 中有办法帮助我们做到查看函数调用的过程吗?本小节我们将会对此进行学习。
知识点
- 基础知识
- 显示栈帧信息
- 切换到其他栈帧
- 实例
基础知识
首先我们需要知道,函数调用信息存放在哪?只有知道函数调用信息,我们才能进行信息提取这一步。答案是,关于函数的信息都存放在栈中。
使用 GDB 调试程序时,当程序发生中断,我们首先应该知道程序在哪里产生中断以及产生中断的原因是什么? 函数发生调用时,相关的调试信息就已经产生,并且被存储在一块被称为栈帧的数据里。
栈帧是在调用栈的内存区域里分配的,是调用栈划分的连续的区块,简称为栈。
每个帧是一个函数调用另一个函数的相关数据,包含了传递给本地用函数的参数,这个函数的本地变量和这个函数的执行地址。
- 在函数开始的时候栈中只有一个帧,是
main函数的,这个帧称为初始帧或者是最外层的帧。 - 每当一个函数被调用,就产生一个新的栈帧。
- 当函数返回时,这个调用所属的帧就被销毁了。
- 如果调用的是递归函数,那么同一个函数就可能有多个帧。
- 当前正在执行的函数调用的帧成为最内层的帧,这是最近创建的帧,同时还有别的帧存在。
- 程序内部的
栈帧用地址标识,一个栈帧有许多的字节组成,每个字节都有自己的地址。
GDB 为所有现存的栈帧编号,从最内层帧 0 开始,1 是这个函数调用的帧,以此类推。这些编号并不真正存在于程序里,他们是由 GDB 分配,用于 GDB 的命令来区分栈帧。
显示栈帧信息
显示栈帧信息的命令主要有 frame 和 backtrace。
frame 命令
frame 的命令格式如下:
frame
使用 frame 命令会打印出当前调用栈的信息,这些信息包含:栈帧的层编号,当前的函数名,函数参数值,函数所在文件及行号,函数执行到的语句。命令可以缩写为 f。
backtrace 命令
backtrace 的命令格式如下:
backtrace <n>
- 不带参数:打印当前调用函数的栈帧信息,每个栈帧显示一行。
- 带参数:
n为正整数时,表示打印栈顶n层的栈信息;n为负整数时,那么表示打印栈底n层的栈信息。
info frame命令
如果我们想要获取更详细的当前栈帧层的信息,可以使用命令:
info frame
打印出的大多数都是运行时的内地址。
比如:函数地址,被调用的函数地址,当前函数是由什么样的语言写成的、函数参数地址及值、局部变量的地址。
❗ info 命令的其他使用方式
info 命令的其他使用方式:
| 命令 | 功能说明 |
|---|---|
| info registers | 查看当前寄存器的值。 |
| info args | 查看当前函数参数的值。 |
| info locals | 查看当前局部变量的值。 |
| info frame | 查看当前栈帧的详细信息。 |
| info variables | 查看程序中的变量符合。 |
| info functions | 查看程序中的函数符号。 |
切换到其他栈帧
切换到任意的栈帧 frame
切换到任意的栈帧使用 frame 相关的命令格式如下:
frame <n>
n 表示栈帧的标号,这个命令可以从一个堆栈帧转到另一个,并打印所选的堆栈帧。
(gdb) frame 3
切换到标号为 3 的栈帧。
从当前的栈帧层向上移动 up
命令格式:
up <n>
n 表示栈帧的标号,在堆栈里上移 n 帧,对于正数向外层的帧移动,更高编号的帧,存在更长的时间的帧。
(gdb) up 3
移动到编号为当前栈帧的编号加 3 的栈帧。
从当前的栈帧层向下移动 down
命令格式:
down <n>
n 表示栈帧的标号,在堆栈里下移 n 帧。对于正数 n,向内层的帧移动,更低编号的帧,新创建的帧。
(gdb) down 3
移动到编号为当前栈帧的编号减 3 的栈帧。
实例
将以下代码保存在桌面上,命名为 demo.c,执行命令gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
int sum(int n)
{
int ret = 0;
if( n > 0 )
{
ret = n + sum(n-1);
}
return ret;
}
int main()
{
int s = 0;
s = sum(10);
printf("sum = %d\n", s);
return 0;
}
设置断点:设置到递归结束标志的位置
(gdb) start
(gdb) break sum if n==0 //设置sum函数中, n==0 时的数据断点。
(gdb) info break //查看断点信息
查看函数调用过程
(gdb) continue
(gdb) backtrace //查看函数调用的顺序
分析函数调用过程
(gdb) next
(gdb) next
(gdb) info args //查看当前函数参数的值
(gdb) frame 7 //切换栈编号为7的上下文中
(gdb) info args //查看栈编号为7时函数参数的值
(gdb) info locals //查看当前局部变量ret的值
(gdb) info frame //查看当前栈帧的详细信息
(gdb) bt 2 //显示栈顶的两层的信息
(gdb) bt -2 //显示栈底的两层的信息
(gdb) frame 0
(gdb) up 1
(gdb) down 1
(gdb) info registers //查看当前寄存器的值
(gdb) info frame //查看当前栈帧的详细信息
Saved registers:
rbp at 0x7fffffffdf50, rip at 0x7fffffffdf58
(gdb) x /1wx 0x7fffffffdf50 //查看ebp地址中的值
(gdb) next
(gdb) next
(gdb) info args
(gdb) info registers //查看栈帧编号为1的寄存器值
(gdb) info locals
ret = 1 //计算结果
总结
本小节学习了以下知识点:
- 基础知识
- 显示栈帧信息
- 切换到其他栈帧
▶️ GDB实战
通过之前的学习,我们已经能够使用 GDB 最常见的用法了,本小节将带领大家使用 GDB 一步一步去调试一个 C 语言程序。本实验可能无法使用之前所学的全部命令,因为使用命令需要大家结合实际情况去判断,之前的教程已经把使用 GDB 命令的场景给大家介绍了,我相信在实际情况中大家也能自己很好的使用。
知识点
- GDB 学习实例
- GDB 命令使用
主要学习使用命令来调试代码,或者看函数调用信息,
实例
将以下代码保存在桌面上,命名为 demo.c,执行命令 gcc -ggdb demo.c -o demo,得到包含调试信息的二进制文件。执行 gdb demo 命令进入调试状态:
#include <stdio.h>
int add_range(int low, int high)
{
int i, sum;
for (i = low; i <= high; i++)
sum = sum + i;
return sum;
}
int main(void)
{
int result[100];
result[0] = add_range(1, 10);
result[1] = add_range(1, 100);
printf("result[0]=%d\nresult[1]=%d\n", result[0], result[1]);
return 0;
}
运行程序,结果如下:
从运行结果来看,这个答案肯定是错误的,因为我们完成的功能是计算 1 加到 10 和 1 加到 100 的和,打印出来的结果应该是 55 和 5050。
现在我们开始利用 GDB 对程序进行调试。
start 执行程序
首先用 start 命令开始执行程序:
(gdb) start
gdb 停在 main 函数之后的第一条语句处等待我们发命令,gdb 列出的这条语句是即将执行的下一条语句。
next 一条继续
我们可以用 next 命令(简写为 n)控制这些语句一条一条地执行:
(gdb) n
(gdb) n
(gdb) n
虽然程序正常打印,并且正常退出,但是并没有找到程序的问题所在。
step 进入函数单步追踪
因为错误不在 main 函数中而在 add_range 函数中,现在用 start 命令重新来过,这次用 step 命令(简写为 s)钻进 add_range 函数中去跟踪执行:
(gdb) start
(gdb) s
(gdb) s
这次停在了 add_range 函数中变量定义之后的第一条语句处。
bcaktrace 查看栈帧
在函数中有几种查看状态的办法,backtrace 命令(简写为 bt )可以查看函数调用的栈帧:
(gdb) bt
可见当前的 add_range 函数是被 main 函数调用的,main 传进来的参数是 low=1, high=10。main 函数的栈帧编号为 1,add_range 的栈帧编号为 0。
info查看局部变量
现在可以用 info 命令(简写为 i)查看 add_range 函数局部变量的值:
(gdb) i locals
frame 选择 1号栈帧
如果想查看 main 函数当前局部变量的值也可以做到,先用 frame 命令(简写为 f)选择 1 号栈帧然后再查看局部变量:
(gdb) f 1
(gdb) i locals
注意到 result 数组中有很多元素有杂乱无章的值,我们知道未经初始化的局部变量具有不确定的值。到目前为止一切正常。
print 打印
用 s 或 n 往下走几步,然后用 print 命令(简写为 p)打印出变量 sum 的值:
(gdb) s
(gdb) s
(gdb) s
(gdb) s
(gdb) p sum
第一次循环 i 是 1,第二次循环 i 是 2,加起来是 3。这里的 $1 表示 gdb 保存着这些中间结果,$ 后面的编号会自动增长,在命令中可以用 $1、$2、$3 等编号代替相应的值。
finish 运行到当前函数返回
由于我们本来就知道第一次调用的结果是正确的,再往下跟也没意义了,可以用 finish 命令让程序一直运行到从当前函数返回为止:
(gdb) finish
返回值是 55,当前正准备执行赋值操作,用 s 命令赋值,然后查看 result 数组:
(gdb) s
(gdb) p result
第一个值 55 确实赋给了 result 数组的第 0 个元素。
第二次调试函数
下面用 s 命令进入第二次 add_range 调用,进入之后首先查看参数和局部变量:
(gdb) s
(gdb) bt
(gdb) i locals
错误原因
由于局部变量 i 和 sum 没初始化,所以具有不确定的值,又由于两次调用是挨着的,i 和 sum 正好取了上次调用时的值。i 的初值不是 0 倒没关系,在 for 循环中会赋值为 0 的,但 sum 如果初值不是 0,累加得到的结果就错了。
修改错误 set
我们已经找到错误原因,可以退出 gdb 修改源代码了。如果我们不想浪费这次调试机会,可以在 gdb 中马上把 sum 的初值改为 0 继续运行,看看这一处改了之后还有没有别的 bug:
(gdb) set var sum=0
(gdb) finish
(gdb) n
(gdb) n
这样结果就对了,修改变量的值除了用 set 命令之外也可以用 print 命令,因为 print 命令后面跟的是表达式,可以自行尝试。
总结
本实验我们学习了以下知识点:
- GDB 学习实例
- GDB 命令使用
到这里我们关于 GDB 的入门课程就学习完毕了,关于 GDB 的内容有很多,本课程只介绍了关于 GDB 的常见使用方式。