一、前言在学习 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也可以用nanonanomain.c输入下面代码#includestdio.hintadd(inta,intb){intresultab;returnresult;}intmain(){intx10;inty20;intsumadd(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表示加入调试信息。调试信息中包含源代码文件名代码行号函数名变量名类型信息。如果没有-gGDB 仍然可以打开程序但是调试体验会很差可能看不到源码也不能方便地查看变量。为了让调试结果更准确建议初学时使用gcc-g-O0main.c-omain其中-g表示加入调试信息-O0表示关闭编译优化为什么要关闭优化因为编译器优化后代码的实际执行顺序可能和源代码不完全一致某些变量也可能被优化掉。对于初学者来说关闭优化更容易理解程序执行过程。运行程序./main输出结果sum30六、启动 GDB使用下面命令启动 GDBgdb ./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{11intx10;12inty20;1314intsumadd(x,y);1516printf(sum %d\n,sum);17这样就可以确认断点位置是否正确。十、运行程序设置好断点后输入run也可以简写r程序开始运行并在main函数处停下来。你可能会看到Breakpoint1, main()at main.c:1111int x10;这句话很重要意思是程序现在停在main.c第 11 行这一行代码即将执行但还没有执行。也就是说此时int x 10;还没有真正执行。十一、单步执行next 命令如果想让程序执行当前这一行然后停到下一行可以使用next简写为n例如当前停在intx10;输入n程序会执行这一行然后停到下一行inty20;这个时候变量x已经被赋值为 10。十二、查看变量print 命令查看变量值使用print 变量名简写为p 变量名例如查看xp x输出可能是$110这里的$1是 GDB 给这次输出结果起的编号不是变量名。继续执行一行n然后查看yp y输出$220查看sump sum如果此时sum那一行还没有执行它的值可能是不确定的。因为变量虽然已经声明但还没有完成赋值。所以要记住一个原则GDB 显示的当前行通常是“即将执行的行”不是“已经执行完的行”。十三、进入函数内部step 命令现在程序会执行到这一行intsumadd(x,y);这里调用了add函数。如果使用nGDB 会把这一行整体执行完不会进入add函数内部。如果想进入add函数中查看执行过程需要使用step简写为s当程序停在intsumadd(x,y);输入s程序会进入add函数intadd(inta,intb){intresultab;returnresult;}此时可以查看函数参数p a p b输出$310$420说明main函数中的x和y已经传给了add函数的参数a和b。十四、next 和 step 的区别初学 GDB 时很多人会分不清next和step。它们的区别如下命令简写作用nextn执行下一行遇到函数不会进入steps执行下一步遇到函数会进入函数内部举例intsumadd(x,y);如果使用nGDB 会直接执行完整个add(x, y)然后停到下一行。如果使用sGDB 会进入add函数内部让你看到函数里面是怎么执行的。简单记忆next看表面 step进里面十五、跳出当前函数finish 命令当我们进入add函数后如果不想一行一行执行了可以使用finish这个命令的作用是执行完当前函数并回到调用这个函数的位置。例如在add函数中输入finishGDB 可能显示Run tillexitfrom#0 add (a10, b20) at main.c:6Value returned is$530这表示add函数执行结束返回值是 30。十六、继续运行continue 命令如果程序停在某个断点或者单步执行过程中暂停了你想让程序继续运行可以输入continue简写c程序会继续运行直到遇到下一个断点程序正常结束程序发生错误用户手动中断。在当前例子中如果没有其他断点输入c程序会继续执行并输出sum30然后程序结束。十七、查看所有断点查看当前设置了哪些断点可以使用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函数中可能会显示x10y20sum30查看当前函数参数info args例如在add函数中可能会显示a10b20二十二、查看函数调用栈backtrace 命令函数调用栈可以帮助我们知道当前代码是从哪些函数一步一步调用过来的。使用命令backtrace简写bt如果当前程序停在add函数中输入bt可能会看到#0 add (a10, b20) at main.c:5#1 main () at main.c:14这表示当前正在执行的是add函数add函数是被main函数调用的调用位置在main.c第 14 行。调用栈在调试复杂程序时非常重要尤其是程序崩溃时可以通过bt快速看到崩溃是从哪里一路调用过来的。二十三、切换栈帧frame 命令当使用bt查看调用栈后可以使用frame命令切换到不同的函数调用层级。例如#0 add (a10, b20) 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 faultGDB 最常用的场景之一就是排查段错误。下面写一个会产生段错误的程序。创建文件vimseg.c输入代码#includestdio.hintmain(){int*pNULL;*p100;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*p100;这说明程序在第 7 行崩溃*p100;查看指针pp p输出$1(int *)0x00x0就是空地址也就是NULL。所以错误原因是指针 p 是空指针却对它进行了解引用操作。错误代码int*pNULL;*p100;正确写法之一intvalue0;int*pvalue;*p100;完整修改后#includestdio.hintmain(){intvalue0;int*pvalue;*p100;printf(value %d\n,*p);return0;}二十五、调试数组越界问题再看一个常见错误数组越界。创建文件vimarray.c输入代码#includestdio.hintmain(){intarr[3]{1,2,3};for(inti0;i3;i){printf(arr[%d] %d\n,i,arr[i]);}return0;}编译gcc-g-O0array.c-oarray使用 GDBgdb ./array设置断点b main运行r单步执行n n当进入循环后可以查看ip 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]但是循环条件写成了i3当i等于 3 时会访问arr[3]这就是数组越界。正确写法应该是for(inti0;i3;i)所以这类问题的调试思路是在循环处设置断点单步执行查看循环变量查看数组下标判断是否访问了非法位置。二十六、调试死循环死循环也是常见问题。创建文件vimloop.c输入代码#includestdio.hintmain(){inti0;while(i5){printf(i %d\n,i);}return0;}编译gcc-g-O0loop.c-oloop运行./loop你会发现程序一直输出i0i0i0程序不会停止。这时用 GDB 调试gdb ./loop运行run如果程序一直执行可以按Ctrl C这会让 GDB 暂停正在运行的程序。暂停后输入bt查看程序当前停在哪里。再输入list查看附近代码。可以看到循环部分while(i5){printf(i %d\n,i);}问题是变量i一直没有变化所以i 5永远成立。正确写法while(i5){printf(i %d\n,i);i;}二十七、条件断点如果程序中有一个循环执行很多次我们不想每次循环都停只想在某个条件满足时停可以使用条件断点。例如#includestdio.hintmain(){for(inti0;i10;i){printf(i %d\n,i);}return0;}编译gcc-g-O0condition.c-ocondition进入 GDBgdb ./condition如果想让程序在i 5时停下可以设置条件断点b 7 if i 5这里假设第 7 行是printf(i %d\n,i);运行r程序会在i等于 5 的时候暂停。条件断点适合调试循环次数很多的程序某个变量达到特定值才出错的程序数组处理、链表遍历、数据查找等场景。二十八、监视变量变化watch 命令有时候我们会遇到这种问题某个变量的值突然变错了但不知道是哪一行代码改坏的。这时可以使用watch命令。例如watch sum表示监视变量sum。只要sum的值发生变化程序就会自动暂停。例如intsum0;sum10;sum20;如果设置了watch sum当sum被修改时GDB 会停下来并告诉你变量旧值和新值。这对排查变量被意外修改的问题非常有用。二十九、修改变量值GDB 不仅能查看变量值还能在程序运行过程中修改变量。例如当前程序中intx10;inty20;在 GDB 中可以输入set var x 100然后查看p x输出$1100这说明变量x已经被修改了。这种功能适合测试不同分支比如if(score60){printf(pass\n);}else{printf(fail\n);}你可以在 GDB 中直接修改set var score 59或者set var score 90这样不用反复改代码、重新编译就可以测试不同情况。三十、调试带命令行参数的程序有些程序运行时需要参数例如./main hello world下面写一个示例程序。创建文件vimargs.c输入#includestdio.hintmain(intargc,char*argv[]){printf(argc %d\n,argc);for(inti0;iargc;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含义如下部分含义xexamine查看内存/4查看 4 个单位x用十六进制显示wword4 字节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*pNULL;*p10;解决方法是在使用指针前确保它指向有效地址。5. Segmentation fault段错误常见原因包括空指针解引用数组越界使用已经释放的内存栈溢出字符串越界写入访问非法地址。遇到段错误时最推荐的做法是gdb ./程序名然后run bt通常可以直接定位到出错位置。三十四、GDB 常用命令速查表命令简写作用gdb ./main无启动 GDBbreak mainb main在 main 函数设置断点break 10b 10在第 10 行设置断点runr运行程序nextn单步执行不进入函数steps单步执行进入函数continuec继续运行print xp x查看变量 xdisplay x无每次暂停自动显示 xinfo locals无查看局部变量info args无查看函数参数backtracebt查看函数调用栈frame 1f 1切换到第 1 层栈帧finish无执行完当前函数并返回info breakpointsinfo b查看所有断点delete 1d 1删除编号为 1 的断点disable 1无禁用编号为 1 的断点enable 1无启用编号为 1 的断点watch x无监视变量 x 的变化set var x10无修改变量 x 的值listl查看源码quitq退出 GDB最后再记住一句话GDB 调试的核心不是背命令而是看清楚程序每一步执行到了哪里、变量发生了什么变化、错误是从哪一步开始出现的。只要掌握了这个思路后面无论是调试 C/C 程序、Linux 程序、嵌入式程序还是 ROS 工程GDB 都会成为非常有用的工具。