GDB是一个由GNU开源组织发布的、UNIX/LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。
在实际应用中,有两种调试方法:在线调试和离线调试。
离线调试适用于开发测试环境,可以自由启停进程,设置断点;在线调试一般用于现场问题分析,不能随便启停进程,对于技术要求较高。
若想执行gdb调试,在Makefile文件中需要增加编译调试选项-g,例如:
gdb dup_file.c –o dum_file_elf –g –lpthread
说明:-g选项的作用是在可执行文件(ELF)中加入源代码的相关信息,比如ELF中第几条机器指令对应源代码的行数。但不是把整个源文件嵌入到可执行文件中,所以在调试时必须保证gdb能找到源文件。
-g完整格式是-glevel,其中,level中指定了调试信息中包含了调试信息的多少,默认的是2,level=1最少,level=3最多。
例如:
readelf -S helloWorld|grep debug
注:helloWorld为文件名,如果没有任何debug信息,则不能被调试。
下面的情况也是不可调试的:
file helloWorld
helloWorld: (省略前面内容) stripped
注:如果最后是stripped,则说明该文件的符号表信息和调试信息已被去除,不能使用gdb调试。但是not stripped的情况并不能说明能够被调试。
在开发中可以将源码和可执行文件拷贝到某一目录下,使用gdb启动进程进行调试,也可以不拷贝源码和可执行文件,使用NFS挂载到编译环境执行调试;在现场环境中使用ps获取进程的pid,然后gdb –p pid执行在线调试。
离线调试:
gdb 进程名
gdb –tui 进程名
在线调试:
ps –A | grep 进程名
gdb –p pid/gdb attach pid
说明:使用-tui参数可以将调试窗口分为两部分:上面是源码,下面是调试信息,使用Ctrl+n/Ctrl+p或者方向键进行翻页。
带参数调试:
1、启动的时候带上参数
gdb --args xxx 参数
2、启动之后 run 带上参数
# gdb xxx
(gdb)run 参数
3、启动之后 set args 设置参数
# gdb xxx
(gdb)set args 参数
core文件调试
当程序core dump时,可能会产生core文件,它能够很大程序帮助我们定位问题。但前提是系统没有限制core文件的产生。可以使用命令limit -c查看:
$ ulimit -c
如果结果是0,即便程序core dump了也不会有core文件留下。我们需要让core文件能够产生:
ulimit -c unlimied #表示不限制core文件大小
ulimit -c 10 #设置最大大小,单位为块,一块默认为512字节
上面两种方式可选其一。第一种无限制,第二种指定最大产生的大小。
针对生成core文件进行调试,可以采用在线加载和离线加载的方式,如下:
gdb 可执行文件 core文件
注:有时候使用p打印调试信息不完整或者不便于阅读,可以使用set print elelent 0和setprint pretty on设置。
handle命令
handle SIGUSR1 nostop noprint
handle SIGUSR2 nostop noprint
handle SIGPIPE nostop noprint
handle SIGALARM nostop
handle SIGHUP nostop
handle SIGTERM nostop noprint
注:设置GDB调试时对信号的相关动作。
打断点还是比较有技巧的,虽然有很多打断点的方法,但是实际调试中一般就使用以下几种:
函数打断点:b 函数名
某一行打断点:b 源文件:行号
条件断点:
break 断点 if 条件
continue 断点编号(执行一次表示设定,再次执行表示取消)
continue 断点编号 条件
注:条件断点非常有用,实际调试中往往需要调试特定场景下函数调用关系,此时就需要设置断点触发的条件。
查看断点:info breakpoint/info break/info b
删除断点:delete 断点号/delete(删除所有断点)
禁用/开启断点:disable/enable breakpoint
ignore:
断点条件的一个特殊用法是,程序只有在到达断点一定次数之后才会停止,此时可以使用指令:
ignore 断点编号 次数
ignore 2 10触发断点10次后才会停止,每次触发断点count自动减1
说明:打完断点是不是执行continue就可以等待着运行到断点了呢?不一定,有时候断点处代码的执行需要外部出发,比如web发送特定消息后才可以触发执行,如果一直等待没有消息出发永远执行不到断点处,此时就需要结合自己的业务逻辑,手动设置出发条件。
执行程序的方法有两种:一种是从main函数开始执行逐步分析,一种是执行到断点处。
重新运行:r/run
继续执行:c/continue
单步执行:n/next/next N(执行N次next)
单步进入:step(遇到函数进入函数内部,退出函数时使用finish)
结束函数:finish
强制返回:return(忽略当前未执行的部分,强制返回)
(gbd) backstrace/bt
有时候跳转的次数太多,不知道具体调用的层级关系了,可以使用bt查看堆栈,该命令会产生一张列表,包含着运行过程和相关的参数。
设置变量:set 变量=表达式
在调试的时候,有时候需要设置一些假数据查看对应输出,比如根据布尔值查看流程执行情况,此时就需要在执行到指定位置时手动设置一下数据的取值。
监控变量:
watch 变量 (数值改变时暂停运行)
awatch <表达式> (被访问或改变时暂停运行)
rwatch <表达式> (被访问时暂停运行)
有时候我们需要观察一个变量的变化过程,比如一个全局变量如何初始化,如何调用的,这就需要使用watch监控变量。
变量类型:
ptype var 变量类型
whatis var 显示一个变量var的类型
打印变量/表达式:
打印变量:p 变量
打印字符/表达式:p “%s”,字符/表达式
格式化输出:p/格式控制符 打印内容
说明:
gdb可支持的变量显示格式有:
x:按16进制格式显示变量
d:按10进制格式显示变量
u:按16进制格式显示无符号整型
o:按8进制格式显示变量
t:按2进制格式显示变量
c:按字符格式显示变量
f:按浮点数格式显示变量
也可以使用x(Examination)来打印需要显示的字符信息,格式如下:
x/格式 地址
格式(可选)一般是NFU:
1、N表示重复次数(表示显示内存的长度,也就是说从当前向后显示几个地址的内容)
2、F表示显示格式
3、U表示单位(b:字节,h:半字[2字节],w:字[4字节,默认],g:双字[8字节])。表示多少个字节作为一个值取出来,如果不指定的话,GDB默认是1个byte,当我们指定了字节长度后,GDB会从指定内存的地址开始,读取指定字节,并把其作为一个值取出来。
参数u可使用下面字符代替:
b:表示单字节
h:表示双字节
w:表示四字节
g:表示八字节
disassemble
可以使用反汇编的指令disassemble去探究究竟在函数中发生了哪些操作,具体如下:
1、disassemble
2、disassemble 程序计数器
3、disassemble 开始地址 结束地址
格式1表示反汇编当前整个函数,格式2表示反汇编计数器所在函数的整个函数,格式3表示反汇编从开始地址到结束地址的部分。
call
强制调用函数:call 表达式
q/quit
在执行到断点后,采用q/quit指令退出。
detach-on-fork
该属性决定了gdb是同时调试父子进程,还是在fork了子进程之后,将子进程分离出去。
on:子进程(或者父进程,取决于gdb在初始时,要调试的进程,也就是follow-fork-mode的值)
off:同时调试父子进程,一个进程处于被调试的状态,而另一个则被gdb挂起
设置:set detach-on-fork on/off
follow-fork-mode
该属性决定了gdb在进程调用fork之后的行为。
set follow-fork-mode parent:默认情况下,在调用fork之后,gdb选择跟随(也就是调试)父进程,而子进程则在处于运行的状态(此时父进程处于阻塞的状态)。
set follow-fork-mode child:fork之后gdb选择调试子进程,而父进程处于运行的状态。
查看当前调试的进程:info inferiors
查看线程:info threads
注:输出信息前面有“*”表示调试的当前线程(一般thread切换线程后查看)。
有的程序会在运行过程中主线程创建多个子线程,所以前后执行info threads显示的线程数是会动态变化的。
查看所有线程堆栈:thread apply all bt
查看指定线程堆栈:thread apply thread1 thread2... bt
切换线程:thread N
注:通过打印counter,可以看到多个线程都是在运行的,如果想要让其他线程处于停止状态,只有当前调试的线程执行,可以采用set scheduler-locking on。
阻塞其他线程,仅调试当前线程工作:
set scheduler-locking [on|off|step]
运行指定线程并允许其他线程并行执行:
thread apply N command
对于C语言开发,必须熟练使用gdb进行调试,这可以帮助我们快速定位问题并解决问题,在开发中可以帮助我们及时找到测试出现的问题,在现场问题中如果日志打印不是很充分,日志信息量不够的情况下,gdb调试显得非常重要。
在实际应用中,我们通常是利用gdb分析core文件,这就需要结合寄存器,汇编,内存相关知识综合分析,后面会详细介绍相关分析技巧。