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种方式

按常用方式排列如下:

  1. gdb filename corefile

    调试core文件。使用场景:程序发生段错误,通过core文件定位异常情况所在位置。

  2. gdb -p pid|tid

    调试正在运行中的进程或者线程。使用场景:1. mwareserver进程正在运行,用GDB挂载到进程中查看某些全局变量的值;2. mwareserver中某个线程阻塞了,用gdb可以查看它阻塞的位置来分析原因。

  3. gdb filename

    直接调试目标程序。使用场景:1. 学习开源代码时通过GDB调试更快地理解程序的架构和执行逻辑;2. 调试自己代码Demo中的bug。

3.2 GDB实用功能设置
  1. 日志功能

有时候输出信息太多,直接显示不方便看,则可以启用GDB的日志功能。

(gdb) set logging on,默认会将日志输出到gdb.txt文件中。

输出文件也可以进行指定,(gdb) set logging file <file name>,然后再执行set logging on开启日志功能。

  1. 结构体格式化输出

默认情况下,gdb以一种“紧凑的方式打印结构体。set print pretty on,打开这个选项,GDB显示结构体时会比较漂亮,当结构体较大时这个功能非常有帮助。

3.3 GDB常用命令
命令名称 命令缩写 命令说明
backtrace bt 查看当前线程的调用堆栈
print 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;
  1. 查看结构体stRect的内容

(gdb) p stRect

  1. 查看结构体stRect中ulWidth的值

(gdb) p stRect.ulWidth 或者 p pstRect->ulWidth

  1. 查看结构体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);
  1. 查看数组aulArray中第5个元素的值

    (gdb) p aulArray[4]

  2. 查看数组aulArray全部的元素

    (gdb) p aulArray

  3. 查看动态数组pulArrayCopy全部的元素

    (gdb) p *pulArrayCopy@MAX_SIZE

print输出格式:

​ print默认是根据表达式的类型自动显示的,在大多数情况下都可以工作的很好,但是在print中可以指定输出格式得到个性化得显示结果。

  1. p/x i,以十六进制打印变量i的值。

  2. p/d|u i,以有符号|无符号十进制整数打印。

  3. p/c cEnd,以十进制和字符的方式显示。

  4. p/f fPos,以浮点数的方式显示。

  5. 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字节)。

  1. 查看数组aulArray中第5个元素的值

    (gdb) x/u aulArray[4]等价于(gdb) x/1uw aulArray[4]

  2. 查看数组aulArray全部的元素

    (gdb) x/10u aulArray或者(gdb) x/10uw aulArray

  3. 查看字符串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

  1. info locals,查看当前栈帧当中全部的局部变量的数值。
  2. info threads,查看当前进程中运行的所有线程的信息,若需要查看某个线程的栈帧,则执行thread n切换到编号为n的线程,然后执行bt命令即可查看栈帧。
  3. info args,查看当前函数的参数值。

7. dump

将指定的一段地址(从start_addr 到end_addr)中的内存数据写入名为filename 的文件中。

dump memory filename start_addr end_addr

4. 参考资料

  1. GNU GDB调试手册
  2. Linux GDB调试指南
  3. 100个gdb小技巧
  4. 《Debug.Hacks中文版_深入调试的技术和工具》