一文详解GDB debug C/C++程序
GDB是一个由GNU开源组织发布的、UNIX/LINUX 操作系统下的、基于命令行的、功能强大的程序调试工具。GDB 支持断点、单步执行、打印变量、观察变量、查看寄存器、查看堆栈等调试手段。在 Linux 环境软件开发中,GDB 是主要的调试工具,用来调试 C 和 C++ 程序(也支持 go 等其他语言)。
本文全面介绍了GDB在Linux上的安装、使用以及基本调试命令和操作,并通过一些简单的案例来演示GDB如何调试C/C++程序。
🎬个人简介:一个全栈工程师的升级之路!
📋个人专栏:问题处理专栏
🎀CSDN主页 发狂的小花
🌄人生秘诀:学习的本质就是极致重复!
目录
1 GDB概述与安装
1.1 GDB简介
GDB(GNU Debugger)是GNU开源组织发布的一个强大的UNIX下的程序调试工具。
GDB主要帮助你进行下面四个方面的功能:
1. 启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
2. 可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)。
3. 当程序被停住时,可以检查此时你的程序中所发生的事。
4. 动态的改变你程序的执行环境。
1.2 GDB历史与发展
GDB最初由Stallman编写,作为GNU项目的一部分,用于调试C语言程序。
随着时间的推移,GDB的功能不断扩展,支持的语言也越来越多,包括C、Objective-C、Fortran、Ada等。
GDB的版本也在不断更新,每个新版本都增加了新的特性和改进了性能。
1.3 安装与配置
安装:在大多数Linux发行版中,GDB都是默认安装的。如果没有安装,可以通过包管理器(如apt、yum等)进行安装。
sudo apt install gdb
配置:在使用GDB之前,需要进行一些基本的配置。例如,设置断点、观察点、命令历史记录等。这些配置可以通过编辑~/.gdbinit文件来实现。
另外,还需要确保被调试的程序是带有调试信息的。在编译程序时,需要使用-g选项来生成调试信息。
例如,使用gcc编译器编译程序时,可以使用以下命令:
gcc -g test.c -o test
然后使用gdb执行程序即可进行gdb调试,如下:
gdb test
2 基本调试命令与操作
常用调试指令
-g: 使用该参数编译可以执行文件,得到调试表。
gdb ./a.out
list: list 1 列出源码。根据源码指定 行号设置断点。
b: b 20 在 20 行位置设置断点。
run/r: 运行程序
n/next: 下一条指令(会越过函数)
s/step: 下一条指令(会进入函数)
p/print: p i 查看变量的值。
continue:继续执行断点后续指令。
finish:结束当前函数调用。
quit:退出 gdb 当前调试。
set args: 设置 main 函数命令行参数 (在 start、 run 之前)
run 字串 1 字串 2 ...: 设置 main 函数命令行参数
info/i b: 查看断点信息表
b 20 if i = 5: 设置条件断点。
ptype:查看变量类型。
bt:列出当前程序正存活着的栈帧。
frame: 根据栈帧编号,切换栈帧。
display:设置跟踪变量
undisplay:取消设置跟踪变量。 使用跟踪变量的编号
2.1 启动GDB并加载程序
有两种方式:
(1)gdb + 可执行程序
在终端中输入 `gdb` 命令即可启动 GDB 调试器,gdb ./test。
(2)gdb + 源文件
gdb test.c ,需要在进入后执行file test加载符号表后才能进行调试。
2.2 设置断点
(1)break 命令
使用 `break` 命令在指定位置设置断点,例如 `break function_name` 或 `break filename:linenumber`。
(2)tbreak 命令
使用 `tbreak` 命令设置临时断点,程序执行到该断点时暂停,但断点只生效一次。
2.3 单步执行与进入函数
(1)step 命令
使用 `step` 命令单步执行程序,遇到函数调用时会进入函数内部。
(2)next 命令
使用 `next` 命令单步执行程序,遇到函数调用时不会进入函数内部,而是直接执行下一行代码。
2.4 查看变量值与表达式求值
(1)print 命令
使用 `print` 命令查看变量值或表达式求值,例如 `print variable_name` 或 `print expression`。
(2)ptype 命令
使用 `ptype` 命令查看变量类型,例如 `ptype variable_name`。
2.5 继续执行到下一个断点或程序结束
(1)continue 命令
使用 `continue` 命令继续执行程序,直到遇到下一个断点或程序结束。
(2)until 命令
使用 `until` 命令执行到指定位置,例如 `until function_name` 或 `until filename:linenumber`。
(3)finish 命令
使用 `finish` 命令执行完当前函数并返回到调用该函数的位置。
3 高级调试技巧与方法
3.1 多线程调试支持
(1)线程信息查看
使用`info threads`命令查看当前进程中的所有线程信息。
(2)线程切换
使用`thread <n>`命令切换到指定编号的线程,其中`<n>`为线程编号。
(3)线程断点设置
可以在特定线程上设置断点,以便仅在该线程执行时触发。
3.2 核心转储文件分析
(1)核心转储文件生成
当程序崩溃时,操作系统可以生成核心转储文件,记录程序崩溃时的内存状态。
(2)GDB加载核心转储文件
使用`gdb <executable> <core file>`命令加载核心转储文件。
(3)分析核心转储文件
通过GDB的命令行接口,可以检查程序崩溃时的堆栈信息、变量值等,以定位问题原因。
3.3 条件断点与临时断点设置
(1)条件断点设置
可以在设置断点时添加条件,以便仅在满足特定条件时触发断点。例如,`break <location> if <condition>`。
(2)临时断点设置
使用`tbreak <location>`命令设置临时断点,该断点在触发一次后自动删除。
(3)断点命令列表
可以为断点设置命令列表,当断点触发时自动执行这些命令。例如,`commands <breakpoint number>`。
3.4 远程调试配置与实现
(1)远程调试原理
远程调试允许在本地机器上运行GDB,而目标程序在远程机器上执行。通过网络连接,GDB可以发送控制命令并接收目标程序的执行状态。
(2)远程调试配置
需要在本地和远程机器上分别配置GDB服务器和客户端。在远程机器上启动GDB服务器,并在本地机器上配置GDB客户端连接到远程服务器。
(3)远程调试实现
在本地机器上使用`target remote <host>:<port>`命令连接到远程GDB服务器。然后,可以像在本地调试一样设置断点、单步执行等操作。
4 调试过程中常见问题及解决方案
4.1 符号表问题
(1)符号表缺失
在编译时未开启-g选项,导致调试信息未被包含在可执行文件中。解决方法是在编译时添加-g选项。
(2)符号表不匹配
源代码与编译生成的符号表不匹配,可能是由于源代码被修改或重新编译导致。解决方法是确保源代码与符号表一致,或重新编译生成新的符号表。
4.2 内存泄漏检测与定位
(1)内存泄漏现象
程序在运行过程中不断占用内存,且无法释放。这可能是由于动态分配的内存未被正确释放导致。
(2)内存泄漏检测工具
使用Valgrind等内存泄漏检测工具可以帮助定位内存泄漏问题。这些工具可以检测出哪些内存块被分配但未释放。
(2)内存泄漏定位方法
通过分析内存泄漏检测工具的报告,结合源代码,可以定位到具体的内存泄漏位置。修复方法包括确保在使用完动态分配的内存后及时释放,以及检查是否存在野指针等问题。
4.3 段错误处理及原因分析
(1)段错误现象
程序在运行过程中突然崩溃,并报告“段错误”或“Segmentation fault”。这通常是由于访问了非法内存地址导致。
(2)段错误原因分析
可能的原因包括解引用空指针、越界访问数组、非法内存访问等。通过分析崩溃时的堆栈信息,可以定位到导致段错误的代码位置。
(3) 段错误处理方法
根据段错误的原因,采取相应的处理措施。例如,对于解引用空指针的问题,需要确保在使用指针前进行非空检查;对于越界访问数组的问题,需要检查数组索引是否越界,并确保数组长度足够。
4.4 性能优化建议
(1)优化算法
针对程序中耗时的算法进行优化,例如使用更高效的算法或数据结构,减少不必要的计算或内存访问。
(2)减少I/O操作
减少不必要的文件读写或网络传输操作,例如通过缓存数据或使用更高效的数据传输方式来提高I/O性能。
(3)多线程/并行计算
利用多线程或并行计算技术,将程序中的计算任务分配到多个处理器核心上执行,从而提高程序的执行效率。
(4)优化编译器选项
调整编译器的优化选项,例如开启O2或O3优化级别,可以让编译器自动进行代码优化,提高程序的执行效率。
5 调试示例
5.1 一个简单示例
#include <stdio.h>
void log(int i)
{
printf(" i = %d \n",i);
}
int main(void)
{
int i = 0;
int arr[3] = {3,4,13};
printf("hello gdb\n");
for (i = 0; i < 4; i++) {
log(arr[i]);
}
}
编译:gcc -g -o test
调试运行:gdb test 得到如下:
list/l n 从第n行开始显示源程序、一般显示10 行,后续继续输入list/l,就可以显示后面的程序
break/b n ,在第n行设置断点,这一行不会被执行
run/r 运行程序
按 next/n 或者step/s 继续向下执行
next/n :下一个,调用函数就开始运行
step/s :单步,会进入调用的函数 要注意的是,如果是系统函数,按 s 就出不来了,这时用 until+行号直接执行到行号处
s执行进入系统函数出不来,使用until +行数出来
print/p i 查看 i 变量的值
continue 继续运行直到结束,得到exited normally,表示正常退出
finish 结束当前函数的调用
上述的代码中有个很明显的数组越界,当执行continue时,可以清晰的看到打印出一个异常的i值。
5.2 gdb调试段错误
这段代码有明显的段错误,p[1]不能赋值
#include <stdio.h>
void log(int i)
{
printf(" i = %d \n",i);
}
int main(void)
{
int i = 0;
int arr[3] = {3,4,13};
char *p = "Tom";
p[1] = 'l';
printf("hello gdb\n");
for (i = 0; i < 3; i++) {
log(arr[i]);
}
}
编译:gcc test.c -o test
调试:gdb test 得到
run 运行,可以很容易查到段错误的位置
5.3 gdb 调试函数传参
#include <stdio.h>
void log(int i)
{
printf(" i = %d \n",i);
}
int main(int argc, char *argv[])
{
int i = 0;
int arr[3] = {3,4,13};
printf("main addr = %d \n",argc);
printf("main函数 参数1: %s 参数2: %s \n",argv[0],argv[1]);
printf("hello gdb\n");
for (i = 0; i < 3; i++) {
log(arr[i]);
}
}
编译:gcc -g test.c -o test
调试:gdb test 得到
set args string1 string2 ...给主函数传入参数
或者 gdb test后使用run testHUHUHU 得到
5.4 断点测试
#include <stdio.h>
void log(int i)
{
printf(" i = %d \n",i);
}
int main(void)
{
int i = 0;
int arr[3] = {3,4,13};
printf("hello gdb\n");
for (i = 0; i < 3; i++) {
log(arr[i]);
}
}
编译,gdb调试后得到:
设置断点和条件断点
backtrace/bt 命令是列出当前堆栈中的所有帧。执行到13行时,栈上只有一帧,编号为0,属于 main 函数。
继续执行,当执行到新的函数时,用bt来查看此时的栈帧信息,下面的图可以看到i=2时,进入了断点2,此时进入log函数调用且暂停运行,因此bt显示有两个栈帧。
从下面输出结果,我们能够看出,有两个栈帧,第1帧属于 main 函数,第0帧属于 log 函数。 每个栈帧都列出了该函数的参数列表。从上面我们可以看出,main 函数没有参数,而 log 函数有参数,并且显示了其参数的值。
有一点我们可能比较迷惑,在第一次执行backtrace的时候,main 函数所在的栈帧编号为0,而第二次执行的时候,main 函数的栈帧为1,而 log 函数的栈帧为0,这是因为栈的增长方向是向下的。
info b/break 查看断点信息
frame 用来查看当前栈帧,栈帧用来存储函数的变量值等信息,默认情况下,GDB 总是位于当前正在执行函数对应栈帧的上下文中。
由于当前正在执行log函数,因此gdb位于第0帧的上下文中,使用p i可以查看变量i的值,使用ptype i 可以查看变量的类型,frame [num]可以切换栈帧
display可以设置跟踪变量
display 命令用于调试阶段查看某个变量或表达式的值,使用 display 命令查看变量或表达式的值,每当程序暂停执行(例如单步执行)时,GDB 调试器都会自动帮我们打印出来
undisplay:取消设置跟踪变量。 使用跟踪变量的编号
5.5 gdb 调试核心转存(coredump)
Coredump,也被称为核心转储,是在进程突然崩溃时捕获的内存快照。当程序发生异常且在进程内部未被捕获时,操作系统会将进程此时的内存、寄存器状态和运行堆栈等信息保存在一个文件中。
这个文件是二进制格式,可以使用gdb、elfdump、objdump等工具或者在Windows系统下的windebug、Solaris系统下的mdb来打开并分析其具体内容。
通过使用ulimit -c命令,可以设置core文件的大小。如果该值为0,则不会产生core文件;如果该值过小,也不会生成core文件,因为core文件通常较大。
使用ulimit -c unlimited命令可以设置为无限制大小,这样在任何情况下都会产生core文件。
要查看当前的core文件限制,可以使用ulimit -a命令。
如下图,执行后此时可以保存核心转存的内存快照:
一个例子:(典型的释放了常量区的内存,运行汇报coredumped)
#include "stdio.h"
#include "stdlib.h"
void dumpCrash()
{
char *ptr = "Hello";
free(ptr);
}
int main()
{
dumpCrash();
return 0;
}
命令端:ulimit -c unlimited
编译:gcc -g test.c -o coredump
执行:./coredump
调试:gdb coredump 后得到
r 和bt后,得到
由上述我们可以很容易的找到coredump的位置,进一步推断coredump的原因
当然,一般系统的coredump的场景远比这个复杂,但是逻辑都是一样的,我们需要先找到coredump的位置,再结合代码以及core文件推测coredump的原因。
🌈我的分享也就到此结束啦🌈
如果我的分享也能对你有帮助,那就太好了!
若有不足,还请大家多多指正,我们一起学习交流!
📢未来的富豪们:点赞👍→收藏⭐→关注🔍,如果能评论下就太惊喜了!
感谢大家的观看和支持!最后,☺祝愿大家每天有钱赚!!!欢迎关注、关注!
更多推荐
所有评论(0)