gdb调试详解与darknet框架gdb调试过程

准备工作

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

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

sudo gedit /etc/profile

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

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

最后立即生效.

source /etc/profile

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

sudo gedit /etc/sysctl.conf

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

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

立即启用

sudo sysctl -p /etc/sysctl.conf

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

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

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

第一个演示代码heoo.c

#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;
}

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

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总是敲命令操作, 也很不安全. 有时候晕了. 写这样编译命令.

gcc -g -Wall -o heoo.c heoo.out

非常恐怖, heoo.c代码删除了. heoo.out => heoo.c 先创建后生成失败退出. 原先的内容被抹掉了. 哈哈. 服务器开发, 经验不足, 熟练度不够.自己都怕自己.

gdb 其它常用命令用法 c q b info

首先看 用到的调试文件houge.c

#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中我们开始调试. 输入下面指令进行运行:

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

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

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

调试内存堆栈信息

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

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

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

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

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

用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以上版本的调试器并且运行在支持反向调试的平台,你就可以用以下几条命令来调试程序:

reverse-continue

反向运行程序知道遇到一个能使程序中断的事件(比如断点,观察点,异常)。

reverse-step

反向运行程序到上一次被执行的源代码行。

reverse-stepi

反向运行程序到上一条机器指令

reverse-next

反向运行到上一次被执行的源代码行,但是不进入函数。

reverse-nexti

反向运行到上一条机器指令,除非这条指令用来返回一个函数调用、整个函数将会被反向执行。

reverse-finish

反向运行程序回到调用当前函数的地方。

set exec-direction [forward | reverse]

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

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

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

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

假设我们有以下C程序:

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

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

gcc -Wall -g a.c 

开始调试

gdb a.out 

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

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

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

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

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

(gdb) watch a  
Hardware watchpoint 2: a  

启动进程记录回放:

(gdb) record  

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

(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命令:

(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:

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

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 。

gdb 打印数组

可以用下面的方法来显示数组

p *array@len

其中p相当于printarray就是数组首地址,也可以是数组名,len是想要显示的数组的长度。
比如我有一个数组的定义

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

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

p *a@5

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

display *a@5

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

gdb 调试darknet实际工程

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

准备工作

这里下载源代码

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

接着编译源代码:

make clean
make

根目录会出现darknet可执行文件。

在工程根目录运行如下命令下载权重:

wget https://pjreddie.com/media/files/yolov3-tiny.weights

开始调试

终端输入如下语句,开始调试

gdb ./darknet

gdb命令中输入运行程序需要的参数类型

set args detect cfg/yolov3-tiny.cfg yolov3-tiny.weights data/dog.jpg

为了对整个工程进行调试,这里需要将src目录添加进来,在gdb命令中输入如下指令:

DIR ./src

gdb命令中为main函数设置断点

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如下

gcc -Wall -ggdb3 -o houge.out houge.c

就可以使用了. 扩展一下 对于 gcc 编译的有个过程叫做 预编译gcc -E -o *.i *.c.

这时候处理多数宏,直接展开, 也可以查看最后结果. 也算也是一个黑科技.

开始多线程调试

首先看测试用例dasheng.c

#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;
}

编译命令

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测试代码

#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;
}

编译命令

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

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

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

这时候启动gdb.

attach pid

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

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

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>

具体的意思有

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种调试好办法

  1. 写单元测试

  2. 打日志检测日志,分析

到这里 gdb30分钟内容讲解完毕. 多试试写写练一练, gdb基本突破没有问题.

参考链接

Linux基础 30分钟GDB调试快速突破
gdb调试4–回退


One more thing

更多关于人工智能、Python、C++、计算机等知识,欢迎访问我的个人博客进行交流, 点这里~~

猜你喜欢

转载自blog.csdn.net/zdaiot/article/details/83066133