STL是标准模板库(Standard Template Library)的简称,是C++的三大件之一。
Alex是STL的核心设计者。他于1950年出生在莫斯科,后来到美国发展,曾经在Adobe、A9.com等公司工作。在Adobe工作时,他和保罗•麦克琼斯是同事和好朋友,他们曾合作出版了《Elements of Programming》一书。
我在写作《软件简史》时,结识了保罗,多次和他邮件来往和视频交流。在保罗为《软件简史》的序言里,特别提到,他曾和Alex一起邀请Fortran之父约翰•巴克斯到Adobe做主旨演讲。
STL的第一大特色当然就是模板。模板增加了STL库的通用性,也让STL具有了如下两个特色:
- 源代码难读,难理解
- 不好调试
对于不好调试,又可以分解为如下两点:
- 模板展开后的类型名和函数名往往很长,冗长晦涩,令人生畏
- 观察STL对象时,经常看到一些难以理解的设计细节,看不到用户关心的属性。
针对这样的问题,我特别在GDB系列讲座的序言一讲,用一个案例分享了用GDB调试STL有关问题时的挑战和化解方法。
我先尝试在x86虚拟机里打开core文件,
GDB给出了四个警告,两对问号,连寄存器这样的基本信息都没有。
使用readelf观察,原因是core文件是在arm64上系统产生的。
于是换gdb-multiarch来打开,情况大为好转,不仅可以看到崩溃点,还可以看到比较完美的调用栈。
崩溃发生在dog_t类的构造函数中:
构造函数异常简单,看起来不应该有错误:
dog_t(int age, const char* name)
{
age_ = age;
name_ = name;
}
从反汇编来看,是访问对象指针时出错了。
对象指针哪里来的呢?来自父函数。
顺着调用栈一级级看父函数,这时就感觉到前面说的问题了,很长的类名,很长的参数名。
只有栈帧5简短,ge_work,看起来是个纯粹的c函数。
使用frame命令切换ge_work,再l,可以看到它的源代码:
void ge_work(void* ptr_vdogs, int no)
{
int i = 0;
vector<dog_t>* vdogs = (vector<dog_t>*)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命令观察,看到的都是“噪声”:
接下来,我换用幽兰代码本来分析同样的core文件。
打开core文件后,gdb便自动下载调试符号,因为幽兰上已经预先配置好了基于debuginfod的符号服务器。
接下来使用同样步骤切到栈帧5,用p命令观察容器,这次就看到了我们想看到的信息:
与上次看到的信息对比,这次看到的太美好了,不仅包含清楚的元素列表,而且还包含向量的两个关键属性:length和capacity。
让我们惊讶的是,length值为2056,capacity的值为2048。
这是不对的啊,length代表的是实际元素个数,capacity代表的是容器的容纳能力,前者应该小于后者才对。
但是现在前者大于后者了,显然是有问题的。
怎么会出现这样的情况呢?
使用info threads列出进程里的所有线程:
然后使用thread apply all命令观察每个线程的调用栈。
浏览gdb显示的结果,可以看到4号线程也在操作stl的vector对象。
切换到这个线程,观察它操作的vector对象,将其与1号线程对比,竟然是同一个vector.
这显然是代码错误了。标准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 7上工作的并不好。所以还是使用高一些的版本更稳妥。
另一个原因是幽兰上自动启用了符号服务器,可以通过互联网从服务器上下载libc等库模块调试符号。有了这些符号后,才可以顺利观察多个线程的调用栈。
比如,在没有使用符号服务器时,线程列表里的信息只有1号线程比较完整,其它线程都缺少当前函数信息。
有了符号后,信息便完整了。
GDB是软件工程师需要修炼的一门硬功夫。在接下来的大约2个月时间里,我会每周六直播一讲,以实战方式讲解gdb的用法。
想和我们一起学习的同行可以在微信中搜索盛格塾小程序,然后搜GDB(注意大写),便可以找到这门课程。
(写文章很辛苦,恳请各位读者点击“在看”,也欢迎转发)
*************************************************
正心诚意,格物致知,以人文情怀审视软件,以软件技术改变人生
扫描下方二维码或者在微信中搜索“盛格塾”小程序,可以文章和有声读物
也欢迎关注格友公众号