1. 什么是GDB?
GDB主要用来调试C/C++程序,它允许我们在执行程序时查看程序的行为和内存信息,以及帮助我们在程序崩溃时查看它之前进行了什么操作。
GDB的命令行调试要远比IDE的功能高级的多,如果只是通常的设置断点,监测变量什么的可能IDE比较方便,但是一旦上升到使用那些高级点的调试技巧,反而GDB在命令行的模式下更为的方便和高效,起码启动速度和响应速度会高很多。
2. 调试必备知识
2.1 什么是core文件?
core 文件是大多数UNIX 系统实现的一种特性,当进程崩溃时,操作系统会将进程当前的内存映像和一部分相关的调试信息写入core 文件,方便开发者后面对问题进行定位。
2.2 开启或关闭core文件的生成
使用ulimit -c
命令可以查看当前的内核转储功能是否有效。
-c
选项表示内核转储文件的大小限制,如果执行完后它的大小为0,表示内核转储不会生效,即使程序出现段错误,也不会有core文件生成。
开启内核转储:
ulimit -c unlimited
这个命令的意思是不限制内核转储文件的大小。设为无限制之后,发生问题时进程的内存就可以全部转储到内核转储文件中。
2.3 设置/查看Core文件的存储路径和命名规则
cat /proc/sys/kernel/core_pattern
执行该命令便可知道core文件存储路径和它的命名规则。假设执行完该命令,输出内容如下所示:
/mnt/sdcard/core-%e-%p-%t
该内容说明core文件生成后将被存放到SD卡目录下,并且还说明了命名格式,其中格式的具体含义如下:
%e:可执行文件名
%p:被转储的进程/线程ID
%t:转储时刻
2.4 调试信息与调试原理
一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。
使用gcc编译程序时,如果加上**-g**选项即可在编译后的程序中保留调试符号信息。举个例子,以下命令将生成一个带调试信息的程序 hello_server。
gcc -g -o hello_server hello_server.c
2.5 编译器优化对调试的影响
编译器的程序优化选项一般有五个级别,从 O0~O4 ( 注意第一个O0 ,是字母O加上数字0), O0表示不优化,从 O1~O4 优化级别越来越高,O4最高。
如果在编译代码时开启编译器优化选项,那么在实际调试过程中可能和实际代码存在差异,例如:在代码中定义了某个全局变量,但是它在任何地方都没有被使用,编译器就会将它从代码中优化掉,那么在使用GDB调试时,就无法查看该变量的任何信息。
相机中的代码被编译时开启了编译器优化选项-O2
,因此编译器在编译时会对代码进行优化,在使用GDB调试时,可能存在有些变量的值等于 <optimized out>
,说明它已经被优化掉了。
3. 启动GDB调试
3.1 使用GDB调试程序的3种方式
按常用方式排列如下:
-
gdb filename corefile
调试core文件。使用场景:程序发生段错误,通过core文件定位异常情况所在位置。
-
gdb -p pid|tid
调试正在运行中的进程或者线程。使用场景:1. mwareserver进程正在运行,用GDB挂载到进程中查看某些全局变量的值;2. mwareserver中某个线程阻塞了,用gdb可以查看它阻塞的位置来分析原因。
-
gdb filename
直接调试目标程序。使用场景:1. 学习开源代码时通过GDB调试更快地理解程序的架构和执行逻辑;2. 调试自己代码Demo中的bug。
3.2 GDB实用功能设置
- 日志功能
有时候输出信息太多,直接显示不方便看,则可以启用GDB的日志功能。
(gdb) set logging on
,默认会将日志输出到gdb.txt文件中。
输出文件也可以进行指定,(gdb) set logging file <file name>
,然后再执行set logging on
开启日志功能。
- 结构体格式化输出
默认情况下,gdb以一种“紧凑的方式打印结构体。set print pretty on
,打开这个选项,GDB显示结构体时会比较漂亮,当结构体较大时这个功能非常有帮助。
3.3 GDB常用命令
命令名称 | 命令缩写 | 命令说明 |
---|---|---|
backtrace | bt | 查看当前线程的调用堆栈 |
p | 打印变量或寄存器值 | |
examine | x | 查看内存地址中的值 |
frame | f | 切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定 |
up/down | / | 向上/向下切换当前调用线程的堆栈 |
info | i | 查看断点 / 线程等信息 |
dump | / | 将指定的一段地址中的内存数据写入文件中 |
1. backtrace
bt,查看当前线程的调用堆栈,即函数间的调用关系。
2. print
p,查看变量的值。
例1:
typedef struct rect
{
ULONG ulWidth;
ULONG ulHeight;
}Rect;
Rect stRect = {1920, 1080};
Rect *pstRect = &stRect;
- 查看结构体stRect的内容
(gdb) p stRect
- 查看结构体stRect中ulWidth的值
(gdb) p stRect.ulWidth
或者 p pstRect->ulWidth
- 查看结构体stRect的地址
(gdb) p &stRect
例2:
#define MAX_SIZE 10
ULONG i = 0;
ULONG aulArray[MAX_SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
ULONG *pulArrayCopy = malloc(MAX_SIZE);
for (i = 0; i < MAX_SIZE; i++)
{
pulArrayCopy[i] = aulArray[i];
}
free(pulArrayCopy);
-
查看数组aulArray中第5个元素的值
(gdb) p aulArray[4]
-
查看数组aulArray全部的元素
(gdb) p aulArray
-
查看动态数组pulArrayCopy全部的元素
(gdb) p *pulArrayCopy@MAX_SIZE
print输出格式:
print默认是根据表达式的类型自动显示的,在大多数情况下都可以工作的很好,但是在print中可以指定输出格式得到个性化得显示结果。
-
p/x i
,以十六进制打印变量i的值。 -
p/d|u i
,以有符号|无符号十进制整数打印。 -
p/c cEnd
,以十进制和字符的方式显示。 -
p/f fPos
,以浮点数的方式显示。 -
p/s pHelloStr
,以字符串的方式显示pHelloStr。
3. examine
x,查看内存地址中的值。它与print作用类似,但适用性更广。
x/nfu addr
,x命令有三个可选参数。
n:显示数目。
f:显示格式,与print格式类似,默认以十六进制显示。
u:显示单位字节大小,b(bytes,1字节)、h(半字,2字节)、w(字,4字节)、g(gaint字,8字节)。
-
查看数组aulArray中第5个元素的值
(gdb) x/u aulArray[4]
等价于(gdb) x/1uw aulArray[4]
-
查看数组aulArray全部的元素
(gdb) x/10u aulArray
或者(gdb) x/10uw aulArray
-
查看字符串pHelloStr内容
char acHelloStr[10] = "Hello gdb!";
(gdb) x/10c acHelloStr
或者(gdb) x/s acHelloStr
。
examine命令相较于print命令适用性更广的地方在于它能够直接查看内存地址中的内容。
假设已知数组aulArray的地址为0x7fffffffde30,数组大小为10,若想查看数组元素的值,那么就可以这样查看:
x/10uw 0x7fffffffde30
或者p *0x7fffffffde30@10
。
4. frame
查看指定的栈帧,用法如:f n
,查看编号为n的栈帧。
5. up/down
基于当前的栈帧向上或者向下移动栈帧。
6. info
info locals
,查看当前栈帧当中全部的局部变量的数值。info threads
,查看当前进程中运行的所有线程的信息,若需要查看某个线程的栈帧,则执行thread n
切换到编号为n的线程,然后执行bt命令即可查看栈帧。info args
,查看当前函数的参数值。
7. dump
将指定的一段地址(从start_addr 到end_addr)中的内存数据写入名为filename 的文件中。
dump memory filename start_addr end_addr
4. 参考资料
- GNU GDB调试手册
- Linux GDB调试指南
- 100个gdb小技巧
- 《Debug.Hacks中文版_深入调试的技术和工具》