当前位置:Gxlcms > 数据库问题 > 使用gdb调试c++程序

使用gdb调试c++程序

时间:2021-07-01 10:21:17 帮助过:10人阅读

上篇(使用c++开发跨平台程序)说到,我不怕造东西,我怕的是造出来的东西,如果出了问题,我却不知道原因.所以调试分析是一个重要的手段.

C++调试是一个复杂的活.虽然大部分调试可以通过IDE在开发期间就解决了.但是必然的,还有很多东西需要在生产环境中还原它.分析它,然后解决它.gdb是一个成熟的工具.围绕着它有很多的工具可以选择.不过这么多工具的根本还是命令行模式下的gdb.

废话不多说,现在我就用gdb来分析调试一下吧.

 

生成dump文件:

shell中输入命令:

ulimit -c unlimited;

然后运行自己的程序,如果程序此时崩溃,就会在目录生成一个名为core的文件.(这个也看系统配置.)

使用命令 gdb Test1 core加载文件.或者它的详细命令 gdb -c core -e Test1 --symbols Test1 --readnow

 

下面是一个命令行输出的截图:

技术图片

上图中可以解释的不多.因为我们现在刚要入门.所以只能注意上图中的三个红框.

红框1:命令行其中app7是可执行文件,coredump文件.

红框2:标明gdbapp7中找到了它对应的symbol.

红框3:标明core文件是经由app7产生的.这里是为了防止载入了错误的可执行文件.

 

注意一下几点:

如果使用sanitize,请取消.不然不会在崩溃时产生dump文件.反而是一个错误报告.

在生成可执行文件的时候,应该用debug模式,也可以用RelWithDebInfo模式.主要目的是能够获得程序的调试符号.

如果没有symbol信息,也可以调试,但是过程将会难上很多倍,毕竟我们是调试,不是破解.不过,还别说,gdb调试跟破解其实还是有点相通的.

由于gdb调试有非常多指令.从时效性上来说,不需要记住全部指令.只需要知道常用的指令就好.就算有人费事费力记住了所有指令,时间一长,如果不用的话也是会忘记的.所以能看到英文文档,我觉得比记住指令更有用.

大部分错误在IDE开发期间就已经被解决了.需要调试core dump文件的情况一般都是运行的时候出现的错误,我这里简单介绍以下几类

指针为NULL.栈溢出,除数为0,死锁.

调试指针为NULL

下面给定一个程序,程序的内容如下:

#include <stdlib.h>
void bar(int* p)
{
    int aa=*p;
}
void foo()
{
    int* p=NULL;
    bar(p);
}
int main(int argc, const char * argv[])
{
    foo();
    return 0;
}

  

编译后假设输出是app0,运行app0后会有core文件.现在我来加载这个core文件.截图如下:

技术图片

技术图片

加载完毕以后,可以看到gdb已经指出来了app0.cpp15行有问题.

然后我们回到源码,查看第15,的确是有问题.所有null问题已经解决.是不是简单无比?呵呵.但是我们要更进一.看看到底为什么.

1.       我使用p p,(第一个pprint,gdb指令,第二个p是参数p);

技术图片

这说明p是一个0.所以这里会出错.

2.       按理说,以上的分析可以得出结论了.不过这里我想再进一步.

首先我列出 所有线程

info thread

 技术图片

 

 

就只有一个线程,很好.

其次,我看看堆栈

bt

技术图片

 

 

可以看到调用堆栈,是从foo函数调用的bar函数.所以参数p是从foo里产生的.

   可以看出,空引用虽然解决了,回头考虑一下的话,这里有点事后诸葛的意思.有人会问你是已经事先知道空引用了.然后去分析的,这谁不会…”,真正的现实当中的空引用的确分析起来比这个困难一点.不过这个系列是让人们基本会用gdb.知道每种类型大体长什么样子.在现实问题中,分析的时候好有个方向.具体工作当中的问题.只能到时再分析.

 

调试栈溢出

栈溢出一般递归函数退出条件没有达成,导致的循环调用.栈溢出调试比较简单,特征也很明显.

下面我借用一个例子来说明一下.这个例子的作者是一个外国人,具体是谁.我忘记了.

 

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void procF(int i)
{
    int buffer[128] = {-1, 0, i+1, 0, -1};
    procF(buffer[2]);
}
void procE()
{
    procF(1);
}
#define THREAD_DECLARE(num,func) void bar_##num(){sleep(3);func;}void foo_##num(){bar_##num();}void * thread_##num (void *arg){foo_##num();return 0;}
THREAD_DECLARE(one,procE())
THREAD_DECLARE(two,sleep(-1))
THREAD_DECLARE(three,sleep(-1))
THREAD_DECLARE(four,sleep(-1))
THREAD_DECLARE(five,sleep(-1))
#define THREAD_CREATE(num) {pthread_t threadID_##num; pthread_create (&threadID_##num, NULL,thread_##num, NULL);}
int main(int argc, const char * argv[])
{
    THREAD_CREATE(one)
    THREAD_CREATE(two)
    THREAD_CREATE(three)
    THREAD_CREATE(four)
    THREAD_CREATE(five)
    sleep(-1);
    return 0;
}

 

  

 

以上文件很简单,定义了一个宏,然后使用这个宏,复制生成了5个线程.其中thread_one这个线程,会陷入死循环.它会在procF中循环调用,导致一个堆栈溢出.

我们来看看它长什么样子.具体怎么加载core我这里就略过了.直接看gdb内容吧.

技术图片

上面说cannot access memory at address xxx,然后列出最近执行具体位置是一个大括号,没有什么参考意义

1.       我先看看所有线程

技术图片

6个线程,除去第一个是不能能读取内存的错误以为,其余的都在sleep.这里按照gdb的提示(它说procF有问题),我先看看thread 1,因为只有它停留在了procF;

2.       指令thread 1 表示切换到线程1.然后查看它的堆栈,看看是如何到达这个procF的.

技术图片

到这里发现procF自己调用自己,按照经验,这里应该是栈溢出了.但是为了确认一下,我决定看看它调用了多少层.

3.       指令 bt是打印调用堆栈了.bt -20是打印最底层的20个调用

技术图片

发现它调用了15000..这里还有一个好处就是,

人气教程排行