当GDB遇到STL

STL是标准模板库(Standard Template Library)的简称,是C++的三大件之一。

当GDB遇到STL_第1张图片

Alex是STL的核心设计者。他于1950年出生在莫斯科,后来到美国发展,曾经在Adobe、A9.com等公司工作。在Adobe工作时,他和保罗•麦克琼斯是同事和好朋友,他们曾合作出版了《Elements of Programming》一书。

我在写作《软件简史》时,结识了保罗,多次和他邮件来往和视频交流。在保罗为《软件简史》的序言里,特别提到,他曾和Alex一起邀请Fortran之父约翰•巴克斯到Adobe做主旨演讲。

STL的第一大特色当然就是模板。模板增加了STL库的通用性,也让STL具有了如下两个特色:

- 源代码难读,难理解

- 不好调试

对于不好调试,又可以分解为如下两点:

- 模板展开后的类型名和函数名往往很长,冗长晦涩,令人生畏

- 观察STL对象时,经常看到一些难以理解的设计细节,看不到用户关心的属性。

针对这样的问题,我特别在GDB系列讲座的序言一讲,用一个案例分享了用GDB调试STL有关问题时的挑战和化解方法。

当GDB遇到STL_第2张图片

我先尝试在x86虚拟机里打开core文件,

当GDB遇到STL_第3张图片

GDB给出了四个警告,两对问号,连寄存器这样的基本信息都没有。

使用readelf观察,原因是core文件是在arm64上系统产生的。

当GDB遇到STL_第4张图片

于是换gdb-multiarch来打开,情况大为好转,不仅可以看到崩溃点,还可以看到比较完美的调用栈。

当GDB遇到STL_第5张图片

崩溃发生在dog_t类的构造函数中:

85def6801293a9d25c157ccc9731698f.png

构造函数异常简单,看起来不应该有错误:

dog_t(int age, const char* name)
        {
                age_ = age;
                name_ = name;
        }

从反汇编来看,是访问对象指针时出错了。

当GDB遇到STL_第6张图片

对象指针哪里来的呢?来自父函数。

顺着调用栈一级级看父函数,这时就感觉到前面说的问题了,很长的类名,很长的参数名。

只有栈帧5简短,ge_work,看起来是个纯粹的c函数。

使用frame命令切换ge_work,再l,可以看到它的源代码:

void ge_work(void* ptr_vdogs, int no)
{
        int i = 0;
        vector* vdogs = (vector*)ptr_vdogs;


        cout << "thread "<< no << " starts working" << endl;
        do {
                vdogs->push_back(dog_t(no*(i++), "little dog"));
        } while(1);
}

看起来这个函数是在使用stl的vector容器,在向容器里面存入dog_t对象。

于是便想知道,崩溃时容器里已经有了多少对象,使用p命令观察,看到的都是“噪声”:

a582a501ac4215f80c8df84ca705ac50.png

接下来,我换用幽兰代码本来分析同样的core文件。

当GDB遇到STL_第7张图片

打开core文件后,gdb便自动下载调试符号,因为幽兰上已经预先配置好了基于debuginfod的符号服务器。

接下来使用同样步骤切到栈帧5,用p命令观察容器,这次就看到了我们想看到的信息:

当GDB遇到STL_第8张图片

与上次看到的信息对比,这次看到的太美好了,不仅包含清楚的元素列表,而且还包含向量的两个关键属性:length和capacity。

让我们惊讶的是,length值为2056,capacity的值为2048。

这是不对的啊,length代表的是实际元素个数,capacity代表的是容器的容纳能力,前者应该小于后者才对。

但是现在前者大于后者了,显然是有问题的。

怎么会出现这样的情况呢?

使用info threads列出进程里的所有线程:

当GDB遇到STL_第9张图片

然后使用thread apply all命令观察每个线程的调用栈。

浏览gdb显示的结果,可以看到4号线程也在操作stl的vector对象。

当GDB遇到STL_第10张图片

切换到这个线程,观察它操作的vector对象,将其与1号线程对比,竟然是同一个vector.

当GDB遇到STL_第11张图片

这显然是代码错误了。标准stl的容器是不支持并发的,也就是多线程使用时要程序员自己加锁进行保护。这就是这个案例的bug。

那么为什么在幽兰上很顺利观察到stl对象属性,快速找到bug了呢?主要原因有如下两个。

首先,幽兰上的gdb版本很高,是非常新的13.1。而不顺利的虚拟机里面,gdb的版本是7。

geduer@ulan:~/gelabs/gestl$ gdb --version

GNU gdb (Ubuntu 13.1-2ubuntu2) 13.1

其实,正是从gdb 7.0开始,gdb引入了pretty-printers功能,使用python脚本来解析stl对象,以优雅的格式显示用户希望看到的属性。

当GDB遇到STL_第12张图片

但是根据我的实际测试,这个功能在一些版本的gdb 7上工作的并不好。所以还是使用高一些的版本更稳妥。

另一个原因是幽兰上自动启用了符号服务器,可以通过互联网从服务器上下载libc等库模块调试符号。有了这些符号后,才可以顺利观察多个线程的调用栈。

比如,在没有使用符号服务器时,线程列表里的信息只有1号线程比较完整,其它线程都缺少当前函数信息。

当GDB遇到STL_第13张图片

有了符号后,信息便完整了。

当GDB遇到STL_第14张图片

GDB是软件工程师需要修炼的一门硬功夫。在接下来的大约2个月时间里,我会每周六直播一讲,以实战方式讲解gdb的用法。

想和我们一起学习的同行可以在微信中搜索盛格塾小程序,然后搜GDB(注意大写),便可以找到这门课程。

当GDB遇到STL_第15张图片

(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)

*************************************************

正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生

扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以阅读更多文章和有声读物

当GDB遇到STL_第16张图片

也欢迎关注格友公众号

当GDB遇到STL_第17张图片

你可能感兴趣的:(c++,开发语言)