准备工作
开启core, 采集程序崩溃的状态
首先你跟着我做开启core崩溃状态采集. 可以通过ulimit -c
查看,如果是0
表示没有开启. 开启按照下面操作:
1 | sudo gedit /etc/profile |
在/etc/profile
最后一行添加下面几句话设置全局开启 core文件调试,大小不限.
1 | # No core files by default 0, unlimited is oo |
最后立即生效.
1 | source /etc/profile |
再跟着我做, 因为生成的core
文件同名会覆盖. 这里为其加上一个core
命名规则, 让其变成[core.pid]
格式.
1 | sudo gedit /etc/sysctl.conf |
在该文件的最后的加上如下几句话,并保存
1 | # open, add core.pid |
立即启用
1 | sudo sysctl -p /etc/sysctl.conf |
最后是ulimit -c
与cat /proc/sys/kernel/core_uses_pid
查看,下面状态表示core启用都搞好了.
如果显示没有开启成功,可以试试注销系统或者重启
另外,如果遇到在gdb调试的时候遇到了Program received signal SIGSEGV, Segmentation fault.
,则可以在终端中运行man 7 signal | grep SEGV
查看错误信息。如下面这个例子,在函数返回的时候没有正确返回,直接报错了。原因是函数传参方式不对,导致一个参数的值没有传进去。
1 | $ man 7 signal | grep SEGV |
简单接触 GDB , 开始调试 r n p
第一个演示代码heoo.c
1 |
|
我们从下图说起,
使用命令
1 | gcc -g -Wall -o heoo.out heoo.c |
gdb heoo.out
表示gdb
加载heoo.out
开始调试. 如果需要使用gdb
调试的话编译的时候gcc
需要加上-g
命令.
其中l
命令表示 查看加载源码内容. .
下面将演示如何加断点,使用命令b 函数名
或者b 行数
,r
表示调试的程序开始运行.
p
命令表示 打印值. n
表示过程调试, 到下一步. 不管子过程如何都不进入. 直接一次跳过.
下面的s 表示单步调试, 遇到子函数,会进入函数内部调试.
总结一下 . l
查看源码 ,b
加断点, r
开始运行调试, n
下一步, s
下一步但是会进入子函数. p
输出数据. c
跳过直到下一个断点处,watch 变量名
给变量添加监视点,whatis 变量名
打印变量名的类型, finish
跳出当前代码(之前跳入调试),q
表示程序退出.
终端打印的一行代码是下一行要执行的代码,而不是已经执行过的代码
到这里gdb 基本会用了. 是不是也很容易. 直白. 小代码可以随便调试了.
看到这里基础知识普及完毕了. 后面可以不看了. 有机会再看. 好那我们接着扯.
gdb其它开发中用的命令
开始扯一点, linux总是敲命令操作, 也很不安全. 有时候晕了. 写这样编译命令.
1 | gcc -g -Wall -o heoo.c heoo.out |
非常恐怖, heoo.c
代码删除了. heoo.out => heoo.c
先创建后生成失败退出. 原先的内容被抹掉了. 哈哈. 服务器开发, 经验不足, 熟练度不够.自己都怕自己.
gdb 其它常用命令用法 c q b info
首先看 用到的调试文件houge.c
1 |
|
同样需要仔细看下面图中使用的命令. 首先对前言部分加深一些. 看下面
这个图是前言的补充, c
跳过直到下一个断点处, q
表示程序退出.
在houge.c
中我们开始调试. 输入下面指令进行运行:
1 | gcc -g -Wall -o houge.out houge.c |
一运行段错误, 出现了我们的 core.pid
文件
通过gdb houge.out core.27047
开始调试. 马上定位出来了错误原因.
循环的调试
例如
1 | while(i<10) |
若想每次while循环均停止,需要在i++
行打上断点,而不是在while(i<10)
。
调试内存堆栈信息
刚开始print a
, 在main
中当做数组处理.打印的信息多. 后面在_add
函数中, a
就是个形参数组地址.
主要看info args
查看当前函数参数值
info locals
看当前函数栈上值信息,info registers
表示查看寄存器值.
后面查看内存信息 需要记得东西多一些. 先看图,x /23dw a
意思是 查看 从a
地址开始 23个 4字节 有符号十进制数 输出.
关于x
更加详细见下面,这个命令常用于监测内存变化.调试中特别常用.
1 | 用gdb查看内存格式: |
gdb设置条件断点
如下如所示,很简单b 17 if i == 8
. 在17行设置一个断点,并且只有i==8
的时候才会触发.
gdb删除断点
d
后面跟断点索引1,2,3..clear
行数或名称. 删除哪一行断点. 看下面演示
到这里 介绍的gdb调试技巧基本都够用了. 感觉用图形ide,例如vs调试也就用到这些了.
估计gdb调试突破20min过去了.够用了. 后面可以不用看了.
gdb调试回退
加入你正在使用GDB7.0以上版本的调试器并且运行在支持反向调试的平台,你就可以用以下几条命令来调试程序:
首先,需要输入
r
开始执行程序,然后输入record
指令记录,否则的话,不能回退。
反向运行程序知道遇到一个能使程序中断的事件(比如断点,观察点,异常)。
1 | reverse-continue |
反向运行程序到上一次被执行的源代码行。
1 | reverse-step |
反向运行程序到上一条机器指令
1 | reverse-stepi |
反向运行到上一次被执行的源代码行,但是不进入函数。
1 | reverse-next |
反向运行到上一条机器指令,除非这条指令用来返回一个函数调用、整个函数将会被反向执行。
1 | reverse-nexti |
反向运行程序回到调用当前函数的地方。
1 | reverse-finish |
设置程序运行方向,可以用平常的命令step
和continue
等来执行反向的调试命令。
1 | set exec-direction [forward | reverse] |
上面的反向运行也可以理解为撤销后面运行的语句所产生的效果,回到以前的状态。
好的,接下来我们来试试看如何反向调试。
首先确认自己的平台支持进程记录回放(Process Record and Replay),当在调试器启用进程记录回放功能时,调试器会记录下子进程,也就是被调试进程的每一步的运行状态与上一步运行状态的差异,需要撤销的时候就可以很方便回到上一步。
假设我们有以下C程序:
1 | int main(int argc, const char *argv[]) |
将它编译并加上调试符号:
1 | gcc -Wall -g a.c |
开始调试
1 | gdb a.out |
接下来设置一个断点在第三行:
1 | (gdb) b 3 |
运行,程序会在第三行的地方停下来:
1 | (gdb) r |
给变量a设置监视点方便我们观察:
1 | (gdb) watch a |
启动进程记录回放:
1 | (gdb) record |
现在每运行一步调试器都会记录下变化,以便回溯。我们连续执行3条语句。
1 | (gdb) n |
可以看到,a的值先是从0变为了1,然后变为2,如果想让程序倒退回到以前的状态怎么办?可以用reverse-next命令:
1 | (gdb) reverse-next |
这样程序就倒退到了我们启动进程记录回放的地方,a的值经过两步回到了最初的状态。
若需要关闭进程记录回放,可以使用record stop:
1 | (gdb) record stop |
带参数的调参
如正常运行为./main file1.pgm file2.pgm
,则使用gdb调试的时候为下面语句,加上了--args
参数
1 | gdb --args ./main file1.pgm file2.pgm |
设置命中断点次数
使用ignore命令,ignore bnum count
。例如ignore 2 10
含义为2号断点命中10次时停止。
gdb 格式化结构体输出
set print address on
打开地址输出,当程序显示函数信息时,GDB会显出函数的参数地址。系统默认为打开的,
show print address
查看当前地址显示选项是否打开。
set print array on
打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。这个选项默认是关闭的。与之相关的两个命令如下,我就不再多说了。
set print array off
show print array
set print elements
这个选项主要是设置数组的,如果你的数组太大了,那么就可以指定一个来指定数据显示的最大长度,当到达这个长度时,GDB就不再往下显示了。如果设置为0,则表示不限制。
show print elements
查看print elements的选项信息。
set print null-stop
如果打开了这个选项,那么当显示字符串时,遇到结束符则停止显示。这个选项默认为off。
set print pretty on
如果打开printf pretty这个选项,那么当GDB显示结构体时会比较漂亮。
set print pretty off
show print pretty
set print union on
set print union off
show print union
打印 C 中的联合体。默认是 on 。
set pagination off
使用该命令可以禁止gdb打印的信息翻页
gdb 打印数组
可以用下面的方法来显示数组
1 | p *array@len |
其中p
相当于print
,array
就是数组首地址,也可以是数组名,len
是想要显示的数组的长度。
比如我有一个数组的定义
1 | int a[] = {1, 2, 3, 4, 5}; |
那么想要显示的时候就可以写:
1 | p *a@5 |
这样就会显示数组a中的所有元素。
也可以使用display
在每一步调试的时候都显示:
1 | display *a@5 |
取消显示就用undisplay
,不过这时候要写显示的号码。
gdb输出重定向
方法一:适合临时向文件输出些信息的情况。
比如要用info functions输出所有函数,结果往往有一大坨,所以可以将之输出到文件。
1 | (gdb) set logging file <file name> |
方法二:适合整个gdb会话期间都重定向输出的情况。
1 | gdb |tee newfile |
gdb 调试darknet实际工程
darknet
源代码是makefile管理的,之前不会在Linux调试大型项目,今天探索了一下,这里介绍一下。
准备工作
从这里下载源代码
修改makefile文件中DEBUG=0
改为DEBUG=1
进行调试。其中编译选项-O0
,意思是不进行编译优化,gdb在默认情况下会使用-O2
,会出现print变量中出现<optimized out>
。
接着编译源代码:
1 | make clean |
根目录会出现darknet
可执行文件。
在工程根目录运行如下命令下载权重:
1 | wget https://pjreddie.com/media/files/yolov3-tiny.weights |
开始调试
终端输入如下语句,开始调试
1 | gdb ./darknet |
在gdb
命令中输入运行程序需要的参数类型
1 | set args detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg |
为了对整个工程进行调试,这里需要将src
目录添加进来,在gdb
命令中输入如下指令:
1 | DIR ./src |
在gdb
命令中为main
函数设置断点
1 | b main |
开始调试,在gdb
命令中输入r
,回车,发现程序停留在第一行。
接着可以在第435行,即char *outfile = find_char_arg(argc, argv, "-out", 0);
,打上断点b 435
;
在gdb
命令中输入b parser.c:761
在子函数parser.c
的761行打上断点;
输入c
,回车,程序跳到下一个断点,即停留下一个断点所在行;
输入n
单步执行,不跳入子函数。
输入s
命令单步执行并跳入此处调用的子函数;
输入print 变量名
或者p 变量名
即可查看该变量值;输入finish
跳出子函数;
输入q
结束调试。
gdb 多线程多进程调试
到这里实战中用的机会少了, 也就老鸟会用上些. 这部分可以调试,不好调试. 一般一调估计小半天就走了. 好,那我们处理最后10min.
gdb调试宏
首先看上面命令
- macro expand 宏(参数) => 得到宏导出内容.
- info macro 宏名 => 宏定义内容
如果你需要用到上面gdb功能, 查看和导出宏的话.还需要gcc 支持,生成的时候加上 -ggdb3如下
1 | gcc -Wall -ggdb3 -o houge.out houge.c |
就可以使用了. 扩展一下 对于 gcc 编译的有个过程叫做 预编译gcc -E -o *.i *.c.
这时候处理多数宏,直接展开, 也可以查看最后结果. 也算也是一个黑科技.
开始多线程调试
首先看测试用例dasheng.c
1 |
|
编译命令
1 | gcc -Wall -g -o dasheng.out dasheng.c -lpthread |
那先看下面测试图
上面info threads
查看所有运行的线程信息. *
表示当前调试的线程.
后面l _run
表示查看 _run
附近代码. 当然还有l 16
查看16行附近文件内容.
gdb多线程切换 测试如下
thread 3表示切换到第三个线程, info threads 第一列id 就是 thread 切换的id.
上面测试线程 就算你切换到 thread 3. 其它线程还是在跑的. 我们用下面命令 只让待调试的线程跑. 其它线程阻塞.
set scheduler-locking on
开始多线程单独调试. 不用了 设置set scheduler-locking off
关闭. 又会回到你调试这个, 其它线程不阻塞.
总结 多线程调试常用就这三个实用命令
- info threads
- thread id
- set scheduler-locking on/off
分别是查看,切换,设置同步调试.到这里多线程调试基本完毕了.
开始gdb多进行调试
首先看liaobude.c
测试代码
1 |
|
编译命令
1 | gcc -Wall -g -o liaobude.out liaobude.c |
其实对多进程调试, 先介绍一个 常用的, 调试正在运行的程序. 首先让./liaobude.out
跑起来.
再通过ps -ef
找到需要调试的进程. 复制进程文件描述符pid.
这时候启动gdb.
attach pid
gdb就把pid那个进程加载进来了. 加载的进程会阻塞到当前正在运行的地方. 直到使用命令控制. 这个功能还是非常猛的.
最后介绍 进程调试的有关命令(需要最新的gdb才会支持). 多进程的调试思路和多线程调试流程很相似.
1 | GDB可以同时调试多个程序。 |
具体的意思有
1 | set follow-fork-mode [parent|child] set detach-on-fork [on|off] |
更加详细的 gdb 多进程调试demo 可以参照 http://blog.csdn.net/pbymw8iwm/article/details/7876797
使用方式和线程调试思路是一样的. 就是gdb 的命令换了字符. 工作中多进程调试遇到少.
遇到了很少用gdb调试. 会用下面2种调试好办法
2) 写单元测试
3) 打日志检测日志,分析
到这里 gdb30分钟内容讲解完毕. 多试试写写练一练, gdb基本突破没有问题.
经验
如果遇到子函数在函数返回的时候,也就是执行到最后一个)
的时候,忽然报了Program received signal SIGSEGV, Segmentation fault.
错误,那么可以使用s
先进入该子函数,然后运行finish
跳出子函数,若此时正常跳出了,那么就不是该子函数的问题,此时再使用s
进入下一个调用的子函数,依次排查。此外,还可以先输入set pagination off
禁止gdb打印消息分页,然后使用bt
查看调用的堆栈。
参考链接
Linux基础 30分钟GDB调试快速突破
gdb调试4—回退
gdb debug with more than one argument
C/C++中的段错误(Segmentation fault)[转]
关于type return to continue,or q
各种奇特的事情:内存错误,无常的段错误,堆栈消失
将GDB中的输出定向到文件
gdb断点(六)condition 与ignore