一、前言
在学习 C/C++、Linux 编程、嵌入式开发、ROS 开发或者操作系统相关课程时,很多同学都会遇到一个问题:
程序能编译,但是运行结果不对;
程序运行一半突然崩溃;
出现 Segmentation fault,却不知道错在哪里。
如果只靠printf一行一行打印变量,虽然也能排查问题,但效率很低。尤其当程序结构复杂、函数调用层级很多时,单纯依赖打印会变得非常麻烦。
这时就需要使用专业的调试工具:GDB。
GDB 是 Linux 环境下非常常用的调试器,它可以让程序在指定位置暂停,查看变量值,单步执行代码,进入函数内部,查看函数调用栈,还能定位段错误等问题。
本文将从零开始,带你一步一步学习 GDB 的基本使用方法。
二、GDB 是什么?
GDB 的全称是GNU Debugger,中文通常叫做 GNU 调试器。
它主要用于调试 C、C++ 等程序。
我们可以这样理解:
平时运行程序时,程序是从头到尾连续执行的,中间发生了什么我们看不到。而使用 GDB 后,我们可以控制程序的执行过程,让它:
- 在某一行代码处停下来;
- 一行一行地执行;
- 查看变量当前的值;
- 查看函数是如何调用的;
- 查看程序崩溃在哪一行;
- 修改程序运行过程中的变量值;
- 分析段错误、死循环、数组越界等问题。
简单来说:
GDB 的作用就是让程序“慢下来”,让我们看清楚程序每一步到底做了什么。
三、安装 GDB
如果你使用的是 Ubuntu 或 Debian 系统,可以使用下面命令安装:
sudoaptupdatesudoaptinstallgdb安装完成后,查看版本:
gdb--version如果能看到类似下面的信息,说明安装成功:
GNU gdb(Ubuntu 版本号)Copyright(C)Free Software Foundation, Inc.四、准备一个测试程序
为了方便学习,我们先写一个简单的 C 程序。
创建一个文件夹:
mkdirgdb_democdgdb_demo新建源文件:
vimmain.c如果不会使用vim,也可以用nano:
nanomain.c输入下面代码:
#include<stdio.h>intadd(inta,intb){intresult=a+b;returnresult;}intmain(){intx=10;inty=20;intsum=add(x,y);printf("sum = %d\n",sum);return0;}这段代码很简单,主要功能是:
- 在
main函数中定义两个整数x和y; - 调用
add函数计算两个数的和; - 将结果保存到
sum; - 最后打印结果。
五、编译程序:一定要加-g
普通编译命令是:
gcc main.c-omain这样虽然可以生成可执行文件,但不适合调试。
如果要使用 GDB 调试,编译时必须加上-g参数:
gcc-gmain.c-omain这里的-g表示加入调试信息。
调试信息中包含:
- 源代码文件名;
- 代码行号;
- 函数名;
- 变量名;
- 类型信息。
如果没有-g,GDB 仍然可以打开程序,但是调试体验会很差,可能看不到源码,也不能方便地查看变量。
为了让调试结果更准确,建议初学时使用:
gcc-g-O0main.c-omain其中:
-g表示加入调试信息-O0表示关闭编译优化为什么要关闭优化?
因为编译器优化后,代码的实际执行顺序可能和源代码不完全一致,某些变量也可能被优化掉。对于初学者来说,关闭优化更容易理解程序执行过程。
运行程序:
./main输出结果:
sum=30六、启动 GDB
使用下面命令启动 GDB:
gdb ./main进入 GDB 后,会看到类似下面的提示:
(gdb)这说明你已经进入了 GDB 的命令环境。
在这个环境中,我们输入的不再是普通 Linux 命令,而是 GDB 的调试命令。
七、GDB 调试的基本流程
GDB 的基本调试流程一般是:
1. 启动 GDB 2. 设置断点 3. 运行程序 4. 程序停在断点处 5. 单步执行代码 6. 查看变量 7. 继续运行或退出下面我们一步一步来操作。
八、设置断点
断点的作用是:
让程序运行到指定位置时自动暂停。
例如我们想让程序一进入main函数就暂停,可以输入:
break main也可以使用简写:
b mainGDB 可能会显示:
Breakpoint1at 0x0000000000001149:filemain.c, line10.这句话表示:
- 成功设置了一个断点;
- 断点编号是 1;
- 断点位置在
main.c文件; - 对应源代码第 10 行。
除了按函数名设置断点,也可以按行号设置断点。
例如:
break 14或者:
b main.c:14表示在main.c的第 14 行设置断点。
九、查看源码
在 GDB 中可以使用list命令查看源码:
list简写为:
l如果想查看main函数附近的代码:
list main如果想查看第 12 行附近的代码:
list 12GDB 会显示类似:
89intmain()10{11intx=10;12inty=20;1314intsum=add(x,y);1516printf("sum = %d\n",sum);17这样就可以确认断点位置是否正确。
十、运行程序
设置好断点后,输入:
run也可以简写:
r程序开始运行,并在main函数处停下来。
你可能会看到:
Breakpoint1, main()at main.c:1111int x=10;这句话很重要,意思是:
程序现在停在
main.c第 11 行,这一行代码即将执行,但还没有执行。
也就是说,此时int x = 10;还没有真正执行。
十一、单步执行:next 命令
如果想让程序执行当前这一行,然后停到下一行,可以使用:
next简写为:
n例如当前停在:
intx=10;输入:
n程序会执行这一行,然后停到下一行:
inty=20;这个时候,变量x已经被赋值为 10。
十二、查看变量:print 命令
查看变量值使用:
print 变量名简写为:
p 变量名例如查看x:
p x输出可能是:
$1=10这里的$1是 GDB 给这次输出结果起的编号,不是变量名。
继续执行一行:
n然后查看y:
p y输出:
$2=20查看sum:
p sum如果此时sum那一行还没有执行,它的值可能是不确定的。因为变量虽然已经声明,但还没有完成赋值。
所以要记住一个原则:
GDB 显示的当前行通常是“即将执行的行”,不是“已经执行完的行”。
十三、进入函数内部:step 命令
现在程序会执行到这一行:
intsum=add(x,y);这里调用了add函数。
如果使用:
nGDB 会把这一行整体执行完,不会进入add函数内部。
如果想进入add函数中查看执行过程,需要使用:
step简写为:
s当程序停在:
intsum=add(x,y);输入:
s程序会进入add函数:
intadd(inta,intb){intresult=a+b;returnresult;}此时可以查看函数参数:
p a p b输出:
$3=10$4=20说明main函数中的x和y已经传给了add函数的参数a和b。
十四、next 和 step 的区别
初学 GDB 时,很多人会分不清next和step。
它们的区别如下:
| 命令 | 简写 | 作用 |
|---|---|---|
| next | n | 执行下一行,遇到函数不会进入 |
| step | s | 执行下一步,遇到函数会进入函数内部 |
举例:
intsum=add(x,y);如果使用:
nGDB 会直接执行完整个add(x, y),然后停到下一行。
如果使用:
sGDB 会进入add函数内部,让你看到函数里面是怎么执行的。
简单记忆:
next:看表面 step:进里面十五、跳出当前函数:finish 命令
当我们进入add函数后,如果不想一行一行执行了,可以使用:
finish这个命令的作用是:
执行完当前函数,并回到调用这个函数的位置。
例如在add函数中输入:
finishGDB 可能显示:
Run tillexitfrom#0 add (a=10, b=20) at main.c:6Value returned is$5=30这表示add函数执行结束,返回值是 30。
十六、继续运行:continue 命令
如果程序停在某个断点,或者单步执行过程中暂停了,你想让程序继续运行,可以输入:
continue简写:
c程序会继续运行,直到:
- 遇到下一个断点;
- 程序正常结束;
- 程序发生错误;
- 用户手动中断。
在当前例子中,如果没有其他断点,输入:
c程序会继续执行并输出:
sum=30然后程序结束。
十七、查看所有断点
查看当前设置了哪些断点,可以使用:
info breakpoints也可以简写:
info b显示结果类似:
Num Type Disp Enb Address What1breakpoint keep y 0x0000000000001149inmain at main.c:11其中:
| 字段 | 含义 |
|---|---|
| Num | 断点编号 |
| Type | 类型 |
| Enb | 是否启用 |
| What | 断点位置 |
十八、删除断点
删除指定断点:
delete 1也可以简写:
d 1这里的1是断点编号。
如果想删除所有断点:
deleteGDB 会询问是否确认删除。
十九、禁用和启用断点
有时我们不想删除断点,只是暂时不用它,可以禁用断点:
disable 1重新启用:
enable 1这样做的好处是,调试复杂程序时不需要频繁删除和重新设置断点。
二十、自动显示变量:display 命令
如果每执行一步都要手动输入:
p x会比较麻烦。
这时可以使用display命令,让 GDB 每次暂停时自动显示变量值。
例如:
display x display y display sum之后每次执行:
nGDB 都会自动显示这些变量的值。
查看已经设置的自动显示项:
info display取消某个自动显示项:
undisplay 1其中1是 display 编号。
二十一、查看局部变量
如果当前函数里有很多局部变量,一个一个print会比较麻烦。
可以使用:
info locals这个命令会显示当前函数中的所有局部变量。
例如在main函数中,可能会显示:
x=10y=20sum=30查看当前函数参数:
info args例如在add函数中,可能会显示:
a=10b=20二十二、查看函数调用栈:backtrace 命令
函数调用栈可以帮助我们知道:
当前代码是从哪些函数一步一步调用过来的。
使用命令:
backtrace简写:
bt如果当前程序停在add函数中,输入:
bt可能会看到:
#0 add (a=10, b=20) at main.c:5#1 main () at main.c:14这表示:
- 当前正在执行的是
add函数; add函数是被main函数调用的;- 调用位置在
main.c第 14 行。
调用栈在调试复杂程序时非常重要,尤其是程序崩溃时,可以通过bt快速看到崩溃是从哪里一路调用过来的。
二十三、切换栈帧:frame 命令
当使用bt查看调用栈后,可以使用frame命令切换到不同的函数调用层级。
例如:
#0 add (a=10, b=20) at main.c:5#1 main () at main.c:14如果当前在#0,也就是add函数中。
切换到main函数:
frame 1简写:
f 1然后可以查看main函数中的变量:
p x p y p sum如果想回到add函数:
frame 0二十四、调试段错误 Segmentation fault
GDB 最常用的场景之一,就是排查段错误。
下面写一个会产生段错误的程序。
创建文件:
vimseg.c输入代码:
#include<stdio.h>intmain(){int*p=NULL;*p=100;printf("value = %d\n",*p);return0;}编译:
gcc-g-O0seg.c-oseg直接运行:
./seg程序会报错:
Segmentation fault这说明程序访问了非法内存。
接下来用 GDB 调试:
gdb ./seg运行:
runGDB 会提示:
Program received signal SIGSEGV, Segmentation fault. main()at seg.c:77*p=100;这说明程序在第 7 行崩溃:
*p=100;查看指针p:
p p输出:
$1=(int *)0x00x0就是空地址,也就是NULL。
所以错误原因是:
指针 p 是空指针,却对它进行了解引用操作。
错误代码:
int*p=NULL;*p=100;正确写法之一:
intvalue=0;int*p=&value;*p=100;完整修改后:
#include<stdio.h>intmain(){intvalue=0;int*p=&value;*p=100;printf("value = %d\n",*p);return0;}二十五、调试数组越界问题
再看一个常见错误:数组越界。
创建文件:
vimarray.c输入代码:
#include<stdio.h>intmain(){intarr[3]={1,2,3};for(inti=0;i<=3;i++){printf("arr[%d] = %d\n",i,arr[i]);}return0;}编译:
gcc-g-O0array.c-oarray使用 GDB:
gdb ./array设置断点:
b main运行:
r单步执行:
n n当进入循环后,可以查看i:
p i查看数组:
p arr查看指定元素:
p arr[0] p arr[1] p arr[2] p arr[3]数组定义是:
intarr[3]={1,2,3};它只有 3 个元素,下标分别是:
arr[0] arr[1] arr[2]但是循环条件写成了:
i<=3当i等于 3 时,会访问:
arr[3]这就是数组越界。
正确写法应该是:
for(inti=0;i<3;i++)所以这类问题的调试思路是:
- 在循环处设置断点;
- 单步执行;
- 查看循环变量;
- 查看数组下标;
- 判断是否访问了非法位置。
二十六、调试死循环
死循环也是常见问题。
创建文件:
vimloop.c输入代码:
#include<stdio.h>intmain(){inti=0;while(i<5){printf("i = %d\n",i);}return0;}编译:
gcc-g-O0loop.c-oloop运行:
./loop你会发现程序一直输出:
i=0i=0i=0程序不会停止。
这时用 GDB 调试:
gdb ./loop运行:
run如果程序一直执行,可以按:
Ctrl + C这会让 GDB 暂停正在运行的程序。
暂停后输入:
bt查看程序当前停在哪里。
再输入:
list查看附近代码。
可以看到循环部分:
while(i<5){printf("i = %d\n",i);}问题是变量i一直没有变化,所以i < 5永远成立。
正确写法:
while(i<5){printf("i = %d\n",i);i++;}二十七、条件断点
如果程序中有一个循环执行很多次,我们不想每次循环都停,只想在某个条件满足时停,可以使用条件断点。
例如:
#include<stdio.h>intmain(){for(inti=0;i<10;i++){printf("i = %d\n",i);}return0;}编译:
gcc-g-O0condition.c-ocondition进入 GDB:
gdb ./condition如果想让程序在i == 5时停下,可以设置条件断点:
b 7 if i == 5这里假设第 7 行是:
printf("i = %d\n",i);运行:
r程序会在i等于 5 的时候暂停。
条件断点适合调试:
- 循环次数很多的程序;
- 某个变量达到特定值才出错的程序;
- 数组处理、链表遍历、数据查找等场景。
二十八、监视变量变化:watch 命令
有时候我们会遇到这种问题:
某个变量的值突然变错了,但不知道是哪一行代码改坏的。
这时可以使用watch命令。
例如:
watch sum表示监视变量sum。
只要sum的值发生变化,程序就会自动暂停。
例如:
intsum=0;sum=10;sum=20;如果设置了:
watch sum当sum被修改时,GDB 会停下来,并告诉你变量旧值和新值。
这对排查变量被意外修改的问题非常有用。
二十九、修改变量值
GDB 不仅能查看变量值,还能在程序运行过程中修改变量。
例如当前程序中:
intx=10;inty=20;在 GDB 中可以输入:
set var x = 100然后查看:
p x输出:
$1=100这说明变量x已经被修改了。
这种功能适合测试不同分支,比如:
if(score>=60){printf("pass\n");}else{printf("fail\n");}你可以在 GDB 中直接修改:
set var score = 59或者:
set var score = 90这样不用反复改代码、重新编译,就可以测试不同情况。
三十、调试带命令行参数的程序
有些程序运行时需要参数,例如:
./main hello world下面写一个示例程序。
创建文件:
vimargs.c输入:
#include<stdio.h>intmain(intargc,char*argv[]){printf("argc = %d\n",argc);for(inti=0;i<argc;i++){printf("argv[%d] = %s\n",i,argv[i]);}return0;}编译:
gcc-g-O0args.c-oargs普通运行:
./args hello world使用 GDB 调试时,有两种传参方式。
第一种,直接在run后面加参数:
run hello world第二种,先设置参数:
set args hello world run查看当前参数:
show args三十一、查看内存
GDB 还可以查看内存内容,常用命令是:
xx是 examine 的意思,表示检查内存。
常见格式:
x/数量格式单位 地址例如:
x/4xw &x含义如下:
| 部分 | 含义 |
|---|---|
| x | examine,查看内存 |
| /4 | 查看 4 个单位 |
| x | 用十六进制显示 |
| w | word,4 字节 |
| &x | 变量 x 的地址 |
常见显示格式:
| 格式 | 含义 |
|---|---|
| x | 十六进制 |
| d | 有符号十进制 |
| u | 无符号十进制 |
| c | 字符 |
| s | 字符串 |
| i | 汇编指令 |
例如查看变量地址:
p &x查看变量附近的内存:
x/4xw &x查看字符串:
x/s str查看数组中的 10 个整数:
x/10dw arr这里:
10 表示查看 10 个 d 表示十进制 w 表示每个单位 4 字节三十二、查看汇编代码
如果学习操作系统、嵌入式、汇编或底层调试,可以使用 GDB 查看汇编代码。
反汇编当前函数:
disassemble反汇编指定函数:
disassemble main显示当前指令:
x/i $pc其中$pc表示当前程序计数器,也就是 CPU 正在执行的位置。
如果想让 GDB 显示源码窗口,可以使用:
layout src显示汇编窗口:
layout asm退出这个界面:
Ctrl + X 然后按 A也就是先按住Ctrl再按X,松开后再按A。
三十三、常见 GDB 报错和解决方法
1. No debugging symbols found
如果启动 GDB 时看到:
No debugging symbols found说明编译时没有加-g。
错误编译方式:
gcc main.c-omain正确编译方式:
gcc-g-O0main.c-omain2. No symbol table is loaded
这个问题通常也是因为没有调试信息。
解决方法:
gcc-g-O0main.c-omain gdb ./main3. 程序没有停在断点
可能原因有:
- 断点所在代码根本没有被执行;
- 没有加
-g; - 编译时开启了优化;
- GDB 加载的不是刚刚编译的程序。
建议重新编译:
gcc-g-O0main.c-omain4. Cannot access memory at address 0x0
这个错误通常和空指针有关。
例如:
int*p=NULL;*p=10;解决方法是:在使用指针前,确保它指向有效地址。
5. Segmentation fault
段错误常见原因包括:
- 空指针解引用;
- 数组越界;
- 使用已经释放的内存;
- 栈溢出;
- 字符串越界写入;
- 访问非法地址。
遇到段错误时,最推荐的做法是:
gdb ./程序名然后:
run bt通常可以直接定位到出错位置。
三十四、GDB 常用命令速查表
| 命令 | 简写 | 作用 |
|---|---|---|
| gdb ./main | 无 | 启动 GDB |
| break main | b main | 在 main 函数设置断点 |
| break 10 | b 10 | 在第 10 行设置断点 |
| run | r | 运行程序 |
| next | n | 单步执行,不进入函数 |
| step | s | 单步执行,进入函数 |
| continue | c | 继续运行 |
| print x | p x | 查看变量 x |
| display x | 无 | 每次暂停自动显示 x |
| info locals | 无 | 查看局部变量 |
| info args | 无 | 查看函数参数 |
| backtrace | bt | 查看函数调用栈 |
| frame 1 | f 1 | 切换到第 1 层栈帧 |
| finish | 无 | 执行完当前函数并返回 |
| info breakpoints | info b | 查看所有断点 |
| delete 1 | d 1 | 删除编号为 1 的断点 |
| disable 1 | 无 | 禁用编号为 1 的断点 |
| enable 1 | 无 | 启用编号为 1 的断点 |
| watch x | 无 | 监视变量 x 的变化 |
| set var x=10 | 无 | 修改变量 x 的值 |
| list | l | 查看源码 |
| quit | q | 退出 GDB |
| 最后再记住一句话: |
GDB 调试的核心不是背命令,而是看清楚程序每一步执行到了哪里、变量发生了什么变化、错误是从哪一步开始出现的。
只要掌握了这个思路,后面无论是调试 C/C++ 程序、Linux 程序、嵌入式程序,还是 ROS 工程,GDB 都会成为非常有用的工具。