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的断点,并且在此之后删除该断点
断点清除
断点清除主要用到clear和delete命令。常见使用如下:
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) 命令