一、实验准备
在 Linux 环境软件开发中,GDB 是调试 C 和 C++ 程序的主要工具。本次实验围绕着GDB常用的调试操作进行。
1、设置断点的意义
当我们想查看变量内容,堆栈情况等等,可以指定断点。程序执行到断点处会暂停执行。break 命令用来设置断点,缩写形式为b。设置断点后,以便我们更详细的跟踪断点附近程序的执行情况。
2、C源代码准备
//test.c
#include <stdio.h>void judge(int num){if ((num & 1) == 0){printf("%d is even\n",num);return;}else{printf("%d is odd\n",num);return;}
}int main(int argc, char *argv[]){judge(0);judge(1);judge(4);return 0;
}
注意:要调试C/C++的程序,首先在编译时,要使用gdb调试程序,在使用gcc编译源代码时必须加上“-g”参数。保留调试信息,否则不能使用GDB进行调试。
gcc -g test.c -o test
二、实验内容
1、通过行号设置断点
break [行号] 或简写为 b [行号]
break 行号,断点设置在该行开始处,注意:该行代码未被执行
如果有多个源文件的话,也可以使用“文件名:行号”的形式设置断点。示例如下:
上图中的(gdb) b test.c:18是设置了断点。断点的位置是test.c文件的18行。使用r命令执行脚本时,当运行到18行时就会暂停。注意:该行代码未被执行。
2、通过函数设置断点
break [函数名] 或简写为 b [函数名]
break 函数名,断点设置在该函数的开始处,断点所在行未被执行:
如果按上面的方法设置断点后,每次执行到断点位置都会暂停。然而,有时候我们只想在指定条件下才暂停。这时候可以根据条件设置断点。设置条件断点的形式,就是在设置断点的基本形式后面增加if条件。示例如下:
b 6 if num > 0
此时,只有当num>0时,程序才会在第6行断住。
3、查看断点信息
info breakpoints 或简写为 info b
可以使用info breakpoints查看断点的相关信息。包含都设置了哪些断点,断点被命中的次数等信息。示例如下:
使用info breakpoints命令后,将会列出所有已设置的断点,每一个断点都有一个标号,用来代表这个断点。
4、删除断点
delete breakpoint 或简写为 del breakpoint
对于不再使用的断点我们可以将其删除。删除的命令格式为 delete breakpoint 断点编号。info breakpoint命令显示结果中的num列就是编号。删除断点的示例如下:
(看样子还不能简写为b,哈哈),再来看一种有多个断点的情况
不指定断点编号的话,默认删除全部断点(会有confirm提示的,别担心hh~)
5、查看源码
断点设置完后,当程序运行到断点处就会暂停。暂停的时候,我们可以查看断点附近的代码。查看代码的命令是list,缩写形式为l(L的小写字母)。
因为上面设置的断点是judge(1),所以list会展示出judge(1)附近的代码,再次执行list,会将后面的代码展示出来,直到代码全部展示完毕。ps:我数了一下,应该是每次展示附近的10行代码。
6、指定行号查看代码
list first,last 或简写为 l first,last # 小写的L
7、列出指定文件的源码
前面执行list命令时,默认列出test.c的源码,如果想要看指定文件的源码呢?可以使用下面的指令
list 文件名+行号/函数名
断点附近的代码了解之后,这时候就可以使用单步执行一条一条语句的去执行。可以随时查看执行后的结果。接下来你可能会想知道程序运行的一些情况,就需要查看变量的值。下面介绍单步调试与设置变量。
8、单步调试
单步执行有两个命令,分别是step和next。我们可能打了多处断点,或者断点打在循环内,这个时候,可以使用continue命令。这三个命令的区别在于:
1、next命令(可简写为n)用于在程序断住后,继续执行下一条语句。
2、step命令(可简写为s),它可以单步跟踪到函数内部。
3、continue命令(可简写为c)或者fg,它会继续执行程序,直到再次遇到断点处。
单步进入-step
step 一条语句一条语句的执行。它有一个别名s。它可以单步跟踪到函数内部。详细来讲,step 就是单步执行,遇到子函数就进入并且继续单步执行;在其他调试其中相当于step-into命令,作用是移动到下一个可执行的代码行。如果当前行是一个函数调用,则调试器进入函数并停止在函数体的第一行。step可以帮助初步揭开代码位置的谜团,例如:函数调用和函数本身可能在不同的文件中。
我在第18行打了断点,然后执行run命令,到18行断点处停下,然后用step命令进入judge函数内部,step 就是单步执行,遇到子函数就进入并且继续单步执行,比如在源程序第6行没有函数调用,只是一个if判断,所以执行step就会继续向下走,而如果该行有函数调用,比如printf,继续执行step就会深入到printf函数里面,如果我们想从printf库函数中跳出来,可以执行finish命令。
finish就是单步执行到子函数内时,用step out就可以执行完子函数余下部分,并返回到上一层函数。在其他调试器中相当于step-out,作用是在栈中前进到到下一层,并在调用函数的下一行停止。
单步执行-next
next命令(可简写为n)用于在程序断住后,继续执行下一条语句。详细来讲,next 是在单步执行时,在函数内遇到子函数时不会进入子函数内单步执行,而是将子函数整个执行完再停止,也就是把子函数整个作为一步。在其他调试器中相当于step-over,作用是在同一个调用栈层中移动到下一个可执行的代码行。调试器不会进入函数体。如果当前行是函数的最后一行,则,next将进入下一个栈层,并在调用函数的下一行停止。
由上图我们可以看出,next指令不会进入judge函数内部,而是将子函数judge整个执行完再停止,也就是把子函数整个作为一步。
继续执行到下一个断点-continue
我们可能打了多处断点,或者断点打在循环内,若想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue命令。它的作用就是从暂停处继续执行。命令的简写形式为c。
我把断点打在了子函数judge,函数体的开始位置,因为在主函数中要三次调用子函数judge,相当于打了三个断点,执行continue命令后,就会跳到下一处的断点位置。
跳过执行–skip
在下图可以看到,使用skip之后,将不会进入judge函数。好处就是skip可以在step时跳过一些不想关注的函数或者某个文件。
可以看到,再使用skip之后,使用step将不会进入judge函数。
- skip delete [num] 删除skip
- skip enable [num] 使能skip
- skip disable [num] 去使能skip
其中num是前面通过info skip看到的num值,上面可以带或不带该值,如果不带num,则针对所有skip,如果带上了,则只针对某一个skip。
为方便在终端查看下面讲述的内容,我们换一个源程序文件(如下所示)
#include<stdio.h>
#include<stdlib.h>int main( int argc , char *argv[] )
{int a = 1;int i = 0;int b[3] = {0,1,2};for(i = 0; i < 3;i++)b[i] = b[i] + 1;printf("%d\n",a);int *p;p = b;printf("%d\n",p[0]);return 0;
}
编译产生可执行文件main:
gcc -g main.c -o main
9、查看变量
打印基本类型变量,数组
上面讲述了如何设置断点,查看断点附近的代码,并可以单步执行和继续执行。接下来可能会想知道程序运行的一些情况,如查看变量的值。此时我们可以使用print命令,以帮助我们进一步定位问题。
print [变量名] 或简写为 p [变量名]
若多个函数或者多个文件有同一个变量名,这个时候可以在前面加上函数名或者文件名来区分:
打印指针指向内容
如果还是使用上面的方式打印指针指向的内容,那么打印出来的只是指针地址,例如:
而如果想要打印指针指向的内容,需要解引用:
从上面可以看到,仅仅使用*只能打印第一个值,如果要打印多个值,后面跟上@并加上要打印的长度。
另外值得一提的是,$可表示上一个变量,而假设此时有一个链表linkNode,它有next成员代表下一个节点,则可使用下面方式不断打印链表内容:
(gdb) p *linkNode
( 这里显示linkNode节点内容 )
(gdb) p *$.next
( 这里显示linkNode节点下一个节点的内容 )
10、按照特定格式打印变量
对于简单的数据,print默认的打印方式已经足够了,它会根据变量类型的格式打印出来,但是有时候这还不够,我们需要更多的格式控制。常见格式控制字符如下:
- x 按十六进制格式显示变量。
- d 按十进制格式显示变量。
- u 按十六进制格式显示无符号整型。
- o 按八进制格式显示变量。
- t 按二进制格式显示变量。
- a 按十六进制格式显示变量。
- c 按字符格式显示变量。
- f 按浮点数格式显示变量。
如果我们要查看b数组的十六进制和二进制格式打印,根据上面的规则得到:
查看内存内容
使用examine命令(简写为x)来查看内存地址中的值。x命令的语法如下:
x/[n][f][u] addr
其中:
- n 表示要显示的内存单元数,默认值为1
- f 表示要打印的格式,前面已经提到了格式控制字符
- u 要打印的单元长度
- addr 内存地址
单元类型常见有如下:
- b 字节
- h 半字,即双字节
- w 字,即四字节
- g 八字节
我们通过一个实例来看,假如我们要把int变量a按照二进制方式打印,并且打印单位是一字节:
查看寄存器内容
info registers
以上是对GDB调试做了简单的实验总结,本实验涉及到GDB调试的常见用法,了解这些之后能够使用GDB定位大部分问题。但是GDB的使用远不止如此,当遇到更加复杂的情况时,可以再去学习更多相关操作。
本次实验的完成参考了许多其他博主的博客,笔者在此向各位致谢,感谢各位精彩的分享,让我受益匪浅。
参考链接:
GDB调试指南(入门,看这篇够了)_程序猿编码的博客-CSDN博客
GDB调试入门指南 - 知乎
【Linux】GDB调试教程(新手小白)_gdb p_爪可摘星辰的博客-CSDN博客
gdb中查看内存方法总结_gdb看的是物理内存还是虚拟内存_angus_monroe的博客-CSDN博客
https://www.cnblogs.com/J1ac/p/9113669.html