GDB调试

正文

首先,编译程序时必须要加上-g参数。
简单总结一下GDB调试命令

gdb 可执行程序 // 使用调试模式
gdb 可执行程序 *.core // 进入调试模式,并查看错误信息

r/run // 运行程序
b/breakdown 文件名:行数 // 在某一行添加一个断点
p/print 变量名 // 打印变量值
watch 变量名 // 监控变量在程序运行过程中的变化
bt/backtrace // 查看栈帧信息
frame 帧号 // 进入指定栈帧

GDB(GNU Debugger)是UNIX及UNIX-like下的强大调试工具,可以调试ada,c,c++,asm,minimal,d,fortran.objective-c,go,java,pascal等语言。本文以C语言为例,介绍GDB启动调试的多种方式。

### 启动调试

对于C程序来说,要想使程序可被调试,需要在编译时加上 **-g** 参数,保留调试信息,否则不能使用GDB进行调试,但如果不是自己编译的程序,并不知道是否带有-g参数,如何判断一个文件是否带有调试信息呢?

答:可以直接对该文件使用gdb命令,如
```bash
$ gdb helloworld
Reading symbols from helloWorld...(no debugging symbols found)...done.

如果没有调试信息,会提示no debugging symbols found

如果是下面的提示:

Reading symbols from helloworld...done.

则可以进行调试。

也可以使用file命令查看程序是否可以调试

$ file helloworld
helloworld: stripped

如果显示的字符最后是stripped,则说明该文件的符号表信息和调试信息已被去除,不能使用gdb调试。

调试启动无参程序

例如:

$ gdb helloworld
(gdb)

然后输入run/r命令,即可运行程序

调试启动带参程序

假设有以下程序,启动时需要带参数

#include <stdio.h>
int main(int argc, char* argb[])
{
    if (1 >= argc)
    {
        printf("usage:hello name\n");
        return 0;
    }
    printf("Hello World %s\n", argv[1]);
    return 0;
}

这种情况应该怎么启动调试呢?

答:只需要在run/r命令的时候带上参数即可。或者使用set args命令,然后再用run/r启动:

$ gdb hello
(gdb) set args [要设置的参数]
(gdb) run/r

调试core文件

当程序core dump时,可能会产生core文件,它能够很大程度上帮助我们定位问题,但前提是系统没有限制core文件的产生,可以使用ulimit -c命令查看系统对core文件产生的限制:

$ ulimit -c
0

如果产生0,表明系统限制core文件产生个数为0,即即使程序出现core dump了,也不会产生core文件(产生路径一般在程序执行目录下或root目录下),为使core文件能够产生,需要使用如下命令:

$ ulimit -c unlimited
$ ulimit -c
unlimited

调试core文件的命令:

$ gdb 程序文件名 core文件名

调试已运行程序

如果程序已经运行了应该怎么进行调试呢?

答:首先使用ps命令找到进程id:

$ ps -ef | grep 进程名

或者

$ pidof 进程名

假设用上述两种方法获取到的进程id为20829,则可用下面的方式调试进程:

$ gdb
(gdb) attach 20829

也可以直接调试相关id进程

$ gdb hello 20829
$ gdb hello --pid 20829

接下来就可以开启调试了.

断点设置

为什么要设置断点

在介绍之前,我们首先需要了解,为什么需要设置断点。我们在指定位置设置断点之后,程序运行到该位置将会“暂停”。这个时候我们就可以对程序进行更多的操作,比如查看变量内容,堆栈情况等等,以帮助我们调试程序。

查看已设置的断点

info breakpoints
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004005fc in printNum2 at test.c:17
    breakpoint already hit 1 time
2       hw watchpoint  keep y                      a
    breakpoint already hit 1 time
    ignore next 3 hits

它将会列出所有已设置的断点,每一个断点都有一个标号,用来代表这个断点。例如,第2个断点是一个观察点,并且会忽略三次。

断点设置

断点设置有多种方式,分别应用于不同的场景。借助示例程序进行一一介绍:

#include<stdio.h>
void printNum(int a)
{
    printf("printNum\n");
    while(a > 0)
    {
    printf("%d\n",a);
    a--;
    }
}
void printNum2(int a,int num)
{
    printf("printNum\n");
    while(a > num && a>0)
    {
        printf("%d\n",a);
        a--;
    }
}
int div(int a,int b)
{
    printf("a=%d,b=%d\n",a,b);
    int temp = a/b;
    return temp;
}
int main(int argc,char *argv[])
{
    printNum2(12,5);
    printNum(10);
    div(10,0);
    return 0;
}

编译:

$ gcc -g -o test test.c

根据行号设置断点

b 9     # break可简写为b

或者

b test.c:9

根据函数名设置断点

将断点设置在函数入口处:

b printNum

这样的话,程序在调用到printNum()函数的时候就会停止运行

根据条件设置断点

假设程序某处发生崩溃,怀疑崩溃的原因时在某个地方出现了非期望的值,那么可以在这个地方打一个断点进行观察,当出现该非法值时,程序断住。使用下列命令来设置条件断点:

b test.c:23 if a == 0

当变量a等于0时,程序就会在第23行停下来,它和condition有着类似的作用,假设上面的断点号为1,那么:

condition 1 a == 0

会使得变量a等于0时,产生断点1。而实际上可以很方便的用来改变断点产生的条件,例如,之前设置a == 0时产生该断点,那么使用condition可以修改断点产生的条件。

根据规则设置断点

例如需要对所有调用**printNum()**函数都设置断点,可以使用下面的方式:

rbreak printNum*

所有以printNum开头的函数都设置了断点。

设置临时断点

假设某处的断点只想生效一次,那么可以设置临时断点,这样断点后面就不复存在了

tbreak test.c:10    # 在第10行设置临时断点

跳过多次设置断点

假如有某个地方,我们知道可能出错,但是前面30次都没有问题,虽然在该处设置了断点,但是想跳过前面30次,可以使用下面的方式:

ignore 1 30

其中,1是想要忽略的断点号,可以通过前面的方式查找到,30是需要跳过的次数,这样设置之后,会跳过前面30次,再次通过info breakpoints可以看到:

Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00000000004005e8 in printNum2 at test.c:16
    ignore next 30 hits

根据表达式值变化产生断点

有时候我们需要观察某个值或表达式,直到它什么时候发生变化了,这个时候我们可以借助watch命令。例如:

watch a

这个时候,让程序继续运行,如果a的值发生变化,则会打印相关内容,如:

Hardware watchpoint 2: a
Old value = 12
New value = 11

但是这里要特别注意的是,程序必须运行起来,否则会出现:

No symbol "a" in current context.

因为程序没有运行,当前上下文也就没有相关变量信息。

禁用或启动断点

有些断点暂时不想使用,但又不想删除,可以暂时禁用或启用。例如:

disable         # 禁用所有断点
disable bnum    # 禁用标号为bnum的断点
enable          # 启用所有断点
enable bnum     # 启用标号为bnum的断点
enable delete bnum      # 启动标号为bnum的断点,并且在此之后删除该断点

断点清除

断点清除主要用到cleardelete命令。常见使用如下:

clear           # 删除当前所有断点
clear function  # 删除函数名为function处的断点
clear filename:function # 删除文件filename中函数function处的断点
clear lineNum   # 删除行号为lineNum处的断点
clear filename:lineNum  #喊出文件filename中行号为lineNum处的断点
delete          # 删除所有断点
delete bNum     # 删除断点号为bNum的断点

变量查看

前言

在启动调试以及设置断点之后,就到了GDB调试中非常关键的一步—查看变量,查看运行结果是否符合预期。

在查看变量之前,需要先启动调试并设置断点,前文已经介绍过了。后面的内容都基于程序在某个位置已经断住。

普通变量查看

最常见的使用是使用print(可简写为p)打印变量内容。例如。打印基本类型、数组、字符数组等直接使用p 变量名即可:

(gdb) p a
$1 = 10
(gdb) p b
$2 = {1, 2, 3, 5}
(gdb) p c
$3 = "helloWorld"
(gdb)

当然很多时候,多个函数或者多个文件会有同一个变量名,这个时候可以在前面加上函数名或者文件名来区分:

(gdb) p 'testGdb.h'::a
$1 = 11
(gdb) p 'main':: b
$2 = {1, 2, 3, 5}
(gdb)

这里所打印的a值是我们定义在testGdb.h文件里的,而b值是main函数中的b。

打印指针指向内容

如果还是使用上面的方式打印指针指向的内容,那么打印出来的只是指针地址而已,例如:

(gdb) p d
$1 = (int* )0x602010
(gdb)

而如果想要打印指针指向的内容,需要解引用:

(gdb) p *d
$2 = 0
(gdb) p *d@10
$3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb)

从上面可以看到,使用 * 仅仅只能打印第一个值,如果要打印多个值,后面需要跟上 **@**并加上要打印的长度。或者 **@**后面跟上变量值:

(gdb) p *d@a
$2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
(gdb)

由于a的值为10,并且是作为整形指针数据长度,因此后面可以直接跟着a,也可以打印出所有内容。

同时,$可表示一个变量,而假设此时有一个链表lineNode,它有next成员代表下一个节点,则可使用下面方式不断打印链表内容:

(gdb) p *lineNode
# (这里显示lineNode 节点内容)
(gdb) p *$.next
# (这里显示lineNOde 节点下一个节点的内容)

如果想要查看前面数组的内容,可以将下标一个一个累加,还可以定义一个类似UNIX环境变量,例如:

(gdb) set $index=0
(gdb) p b[$index++]
$11 = 1
(gdb) p b[$index++]
$12 = 2
(gdb) p b[$index++]
$13 = 3

按照特定格式打印变量

对于简单的数据,print默认的打印方式已经足够,它会根据变量类型的格式打印出来,但是有时候这还不够,我们需要更多的格式控制。常见格式控制字符如下:

  • x按十六进制格式显示变量
  • d按十进制格式显示变量
  • u按十六进制格式显示无符号整型
  • o按八进制格式显示变量
  • t按二进制格式显示变量
  • a按十六进制格式显示变量
  • c按字符格式显示变量
  • f按浮点数格式显示变量

还是以实例来说明,正常方式打印字符数组c:

(gdb) p c
$18 = "hello world"

以十六进制格式打印:

(gdb) p/x c
$19 = {}
(gdb)

查看内存内容

**examine(简写为x)**可以用来查看内存地址中的值。语法如下:

x/[n][f][u] addr

其中:

  • n 表示要实现的内存单元书,默认值为1
  • f 表示要打印的格式,前面已经提到了格式控制字符
  • u 要打印的单元长度
  • addr 内存地址

单元类型常见有如下:

  • b 字节
  • h 半字,即双字节
  • w 字,即四字节
  • g 八字节

查看寄存器内容

(gdb)info registers
rax            0x0    0
rbx            0x0    0
rcx            0x7ffff7dd1b00    140737351850752
rdx            0x0    0
rsi            0x7ffff7dd1b30    140737351850800
rdi            0xffffffff    4294967295
rbp            0x7fffffffdc10    0x7fffffffdc10

单步调试

单步执行

next命令(简写为n)用于在程序断住后,继续执行下一条语句,假设已经启动调试并进入断点,如果要继续执行,就使用n来执行下一条语句,如果后面跟上数字num,则表示执行该命令num次,达到继续执行n行的效果。

单步进入-step

如果想跟踪函数内部的情况,可以使用step命令(简写为s),它可以单步跟踪到函数内部,但前提是该函数有调试信息并且有源码信息。如果没有该函数源码,需要调过该函数执行,可使用finish命令,继续执行后面的程序。如果没有函数调用,s的作用与n的作用并无差别,也是继续执行下一行。它后面也可以跟数字,表明要执行的次数。

继续执行到下一个断点-continue

一个程序中可能有多处断点,或者断点打在循环内,这个时候,想跳过这个断点,甚至跳过多次断点继续执行该怎么做呢?可以使用continue(简写为c)命令,它会继续执行程序,直到再次遇到断点处

继续运行到指定位置-until

假如程序在25行停住了,现在想要运行到29行停住,就可以使用until(简写为u) 命令

跳过执行-skip


标题:GDB调试
作者:staymeloo7
联系方式:staycoolsun@gmail.com

    评论
    0 评论
avatar

取消