gdb调试

准备工作

开启core, 采集程序崩溃的状态

首先你跟着我做开启core崩溃状态采集. 可以通过ulimit -c查看,如果是0表示没有开启. 开启按照下面操作:

1
sudo gedit /etc/profile

/etc/profile最后一行添加下面几句话设置全局开启 core文件调试,大小不限.

1
2
# No core files by default 0, unlimited is oo
ulimit -S -c unlimited > /dev/null 2>&1

最后立即生效.

1
source /etc/profile

再跟着我做, 因为生成的core文件同名会覆盖. 这里为其加上一个core命名规则, 让其变成[core.pid]格式.

1
sudo gedit /etc/sysctl.conf

在该文件的最后的加上如下几句话,并保存

1
2
3
# open, add core.pid
kernel.core_pattern = ./core_%t_%p_%e
kernel.core_uses_pid = 1

立即启用

1
sudo sysctl -p /etc/sysctl.conf

最后是ulimit -ccat /proc/sys/kernel/core_uses_pid查看,下面状态表示core启用都搞好了.

如果显示没有开启成功,可以试试注销系统或者重启

另外,如果遇到在gdb调试的时候遇到了Program received signal SIGSEGV, Segmentation fault.,则可以在终端中运行man 7 signal | grep SEGV查看错误信息。如下面这个例子,在函数返回的时候没有正确返回,直接报错了。原因是函数传参方式不对,导致一个参数的值没有传进去。

1
2
3
$ man 7 signal | grep SEGV
cific thread (e.g., certain signals, such as SIGSEGV and SIGFPE, generated as a consequence of executing a specific
SIGSEGV 11 Core Invalid memory reference

简单接触 GDB , 开始调试 r n p

第一个演示代码heoo.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int g_var = 0;

static int _add(int a, int b) {
printf("_add callad, a:%d, b:%d\n", a, b);
return a+b;
}

int main(void) {
int n = 1;

printf("one n=%d, g_var=%d\n", n, g_var);
++n;
--n;

g_var += 20;
g_var -= 10;
n = _add(1, g_var);
printf("two n=%d, g_var=%d\n", n, g_var);

return 0;
}

我们从下图说起,
使用命令

1
2
gcc -g -Wall -o heoo.out heoo.c
gdb heoo.out

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/*
* arr 只能是数组
* 返回当前数组长度
*/
#define LEN(arr) (sizeof(arr)/sizeof(*arr))

// 简单数组打印函数
static void _parrs(int a[], int len) {
int i = -1;
puts("当前数组内容值如下:");

while(++i < len)
printf("%d ", a[i]);
putchar('\n');
}

// 简单包装宏, arr必须是数组
#define PARRS(arr) \
_parrs(arr, LEN(arr))

#define _INT_OLD (23)

/*
* 主函数,简单测试
* 测试 core文件,
* 测试 宏调试
* 测试 堆栈内存信息
*/
int main(void) {
int i;
int a[_INT_OLD];
int* ptr = NULL;

// 来个随机数填充值吧
srand((unsigned)time(NULL));
for(i=0; i<LEN(a); ++i)
a[i] = rand()%222;

PARRS(a);

//全员加double, 包含一个错误方便测试
for(i=1; i<=LEN(a); ++i)
a[i] <<= 1;
PARRS(a);

// 为了错,强制错
*ptr = 0;

return 0;
}

同样需要仔细看下面图中使用的命令. 首先对前言部分加深一些. 看下面

这个图是前言的补充, c跳过直到下一个断点处, q表示程序退出.

houge.c中我们开始调试. 输入下面指令进行运行:

1
2
gcc -g -Wall -o houge.out houge.c
./houge.out

一运行段错误, 出现了我们的 core.pid 文件

通过gdb houge.out core.27047开始调试. 马上定位出来了错误原因.

循环的调试

例如

1
2
while(i<10)
i++;

若想每次while循环均停止,需要在i++行打上断点,而不是在while(i<10)

调试内存堆栈信息

刚开始print a, 在main中当做数组处理.打印的信息多. 后面在_add函数中, a就是个形参数组地址.

主要看info args查看当前函数参数值

info locals看当前函数栈上值信息,info registers表示查看寄存器值.

后面查看内存信息 需要记得东西多一些. 先看图,x /23dw a 意思是 查看 从a地址开始 23个 4字节 有符号十进制数 输出.

关于x更加详细见下面,这个命令常用于监测内存变化.调试中特别常用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
用gdb查看内存格式:
x /nfu ptr

说明
x 是 examine 的缩写
n表示要显示的内存单元的个数

f表示显示方式, 可取如下值
x 按十六进制格式显示变量。
d 按十进制格式显示变量。
u 按十进制格式显示无符号整型。
o 按八进制格式显示变量。
t 按二进制格式显示变量。
a 按十六进制格式显示变量。
i 指令地址格式
c 按字符格式显示变量。
f 按浮点数格式显示变量。

u表示一个地址单元的长度
b表示单字节,
h表示双字节,
w表示四字节,
g表示八字节

Format letters are o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction), c(char) and s(string).
Size letters are b(byte), h(halfword), w(word), g(giant, 8 bytes)

ptr 表示从那个地址开始

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

设置程序运行方向,可以用平常的命令stepcontinue等来执行反向的调试命令。

1
set exec-direction [forward | reverse]

上面的反向运行也可以理解为撤销后面运行的语句所产生的效果,回到以前的状态。

好的,接下来我们来试试看如何反向调试。

首先确认自己的平台支持进程记录回放(Process Record and Replay),当在调试器启用进程记录回放功能时,调试器会记录下子进程,也就是被调试进程的每一步的运行状态与上一步运行状态的差异,需要撤销的时候就可以很方便回到上一步。

假设我们有以下C程序:

1
2
3
4
5
6
7
int main(int argc, const char *argv[])  
{
int a = 0;
a = 1;
a = 2;
return 0;
}

将它编译并加上调试符号:

1
gcc -Wall -g a.c

开始调试

1
gdb a.out

接下来设置一个断点在第三行:

1
2
(gdb) b 3  
Breakpoint 1 at 0x804839a: file a.c, line 3.

运行,程序会在第三行的地方停下来:

1
2
3
4
(gdb) r  
Starting program: /home/cheryl/a.out
Breakpoint 1, main (argc=1, argv=0xbffff3e4) at a.c:3
3 int a = 0;

给变量a设置监视点方便我们观察:

1
2
(gdb) watch a  
Hardware watchpoint 2: a

启动进程记录回放:

1
(gdb) record

现在每运行一步调试器都会记录下变化,以便回溯。我们连续执行3条语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(gdb) n  
4 a = 1;
(gdb)
Hardware watchpoint 2: a
Old value = 0
New value = 1
main (argc=1, argv=0xbffff3e4) at a.c:5
5 a = 2;
(gdb)
Hardware watchpoint 2: a
Old value = 1
New value = 2
main (argc=1, argv=0xbffff3e4) at a.c:6
6 return 0;

可以看到,a的值先是从0变为了1,然后变为2,如果想让程序倒退回到以前的状态怎么办?可以用reverse-next命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
(gdb) reverse-next  
Hardware watchpoint 2: a
Old value = 2
New value = 1
main (argc=1, argv=0xbffff3e4) at a.c:5
5 a = 2;
(gdb)
Hardware watchpoint 2: a
Old value = 1
New value = 0
main (argc=1, argv=0xbffff3e4) at a.c:4
4 a = 1;
(gdb)
No more reverse-execution history.
main (argc=1, argv=0xbffff3e4) at a.c:3
3 int a = 0;
(gdb)

这样程序就倒退到了我们启动进程记录回放的地方,a的值经过两步回到了最初的状态。
若需要关闭进程记录回放,可以使用record stop:

1
2
(gdb) record stop  
Process record is stoped and all execution log is deleted.

带参数的调参

如正常运行为./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相当于printarray就是数组首地址,也可以是数组名,len是想要显示的数组的长度。
比如我有一个数组的定义

1
int a[] = {1, 2, 3, 4, 5};

那么想要显示的时候就可以写:

1
p *a@5

这样就会显示数组a中的所有元素。
也可以使用display在每一步调试的时候都显示:

1
display *a@5

取消显示就用undisplay,不过这时候要写显示的号码。

gdb输出重定向

方法一:适合临时向文件输出些信息的情况。

比如要用info functions输出所有函数,结果往往有一大坨,所以可以将之输出到文件。

1
2
3
4
(gdb) set logging file <file name>
(gdb) set logging on
(gdb) info functions
(gdb) set logging off

方法二:适合整个gdb会话期间都重定向输出的情况。

1
gdb |tee newfile

gdb 调试darknet实际工程

darknet源代码是makefile管理的,之前不会在Linux调试大型项目,今天探索了一下,这里介绍一下。

准备工作

这里下载源代码

修改makefile文件中DEBUG=0改为DEBUG=1进行调试。其中编译选项-O0,意思是不进行编译优化,gdb在默认情况下会使用-O2,会出现print变量中出现<optimized out>

接着编译源代码:

1
2
make clean
make

根目录会出现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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 声明一个都用的量
static int _old;

// 线程跑的函数
static void* _run(void* arg) {
int piyo = 10;
int n = *(int*)arg;
int i;

//设置线程分离
pthread_detach(pthread_self());

for(i=0; i<n; ++i) {
printf("n=%d, i=%d\n", n, i);
++_old;
printf("n=%d, piyo = %d, _old=%d\n", n, piyo, _old);
}

return NULL;
}

#define _INT_PTX (3)

int main(void) {
int i, rt, j;
pthread_t tx[_INT_PTX];

puts("main beign");

for(i=0; i<_INT_PTX; ++i) {
// &i 是有问题的, 但是这里为了测试, 可以乱搞
rt = pthread_create(tx+i, NULL, _run, &i);
if(rt < 0) {
printf("pthread_create create error! rt = %d, i=%d\n", rt, i);
break;
}
}

//CPU忙等待
for(j=0; j<1000000000; ++j)
;
puts("end");

return 0;
}

编译命令

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

// 声明一个都用的量
static int _old;

// 线程跑的函数
static void _run(int n) {
int piyo = 10;
int i;

++n;
for(i=0; i<n; ++i) {
printf("n=%d, i=%d\n", n, i);
++_old;
printf("n=%d, piyo = %d, _old=%d\n", n, piyo, _old);
}
}

#define _INT_PTX (3)

int main(void) {
int i;
pid_t rt;

puts("main beign");

for(i=0; i<_INT_PTX; ++i) {
// &i 是有问题的, 但是这里为了测试, 可以乱搞
rt = fork();
if(rt < 0) {
printf("fork clone error! rt = %d, i=%d\n", rt, i);
break;
}
if(rt == 0) {
_run(i);
exit(EXIT_FAILURE);
}
}

//等待子进程结束
  for(;;) {
    rt = waitpid(-1, NULL, WNOHANG);
    if(rt>=0 || errno==EINTR)
      continue;
    break;
  }

puts("end");

// 这里继续等待
for(i=0; i<190; ++i){
printf("等待 有缘人[%d]!\n", i);
sleep(1);
}

return 0;
}

编译命令

1
gcc -Wall -g -o liaobude.out liaobude.c

其实对多进程调试, 先介绍一个 常用的, 调试正在运行的程序. 首先让./liaobude.out跑起来.

再通过ps -ef找到需要调试的进程. 复制进程文件描述符pid.

这时候启动gdb.

attach pid

gdb就把pid那个进程加载进来了. 加载的进程会阻塞到当前正在运行的地方. 直到使用命令控制. 这个功能还是非常猛的.

最后介绍 进程调试的有关命令(需要最新的gdb才会支持). 多进程的调试思路和多线程调试流程很相似.

1
2
3
4
5
6
7
GDB可以同时调试多个程序。
只需要设置follow-fork-mode(默认值:parent)和detach-on-fork(默认值:on)即可。

设置方法:set follow-fork-mode [parent|child] set detach-on-fork [on|off]

查询正在调试的进程:info inferiors
切换调试的进程: inferior <infer number>

具体的意思有

1
2
3
4
5
6
set follow-fork-mode [parent|child]   set detach-on-fork [on|off]

parent on 只调试主进程(gdb默认)
child on 只调试子进程
parent off 同时调试两个进程,gdb跟主进程,子进程block在fork位置
child off 同时调试两个进程,gdb跟子进程,主进程block在fork位置

更加详细的 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 to quit
各种奇特的事情:内存错误,无常的段错误,堆栈消失
将GDB中的输出定向到文件
gdb断点(六)condition 与ignore

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道