写 C++ 的同学想必有太多和内存打交道的血泪经验了, 常常被 C++ 的内存问题搅的焦头烂额.(写 core 的经验了) 有很多同学一见到 core 就两眼一抹黑, 不知所措了. 笔者 入 "坑"C++ 之后, 在调试 C++ 代码的过程之中, 学习了不少调试代码内存的工具. 希望借这个机会来介绍一下笔者常用的工具, GDB,Valgrind 等等, 相信大家通过好好运用这些工具, 能更好的驯服内存这匹 "野马".
1. 利用 GDB 调试 CoreDump
CoreDump 时一个二进制的文件, 进程发生错误崩溃时, 内核会产生一个瞬时的快照, 记录该进程的内存, 运行堆栈状态等信息保存在 core 文件之中. 做个简单的类比, core 文件相当于飞机运行时的 "黑匣子", 能够帮助我们更好的调试 C++ 程序的问题. OK, 接下来笔者将介绍一下如果利用 GDB 来调试 CoreDump 的文件.
CoreDump 文件的大小
首先我们先确定一下操作系统是否会产生 CoreDump 文件. 通过 ulimit -c 获取 core 文件的限制大小:
上面显示笔者电脑的 core 文件的大小是 0, 我们需要调整一下. 通过 ulimit 调整为无限制. 当然这种调整是临时的, reboot 之后就恢复为 0 了.
ulimit -c ulimited
如果需要永久修改, 可以通过 / etc/security/limits.conf 来修改 core 文件的大小.
CoreDump 文件的生成路径
默认情况下, core dump 生成的文件名为 core, 而且就在程序当前目录下. 通过修改 / proc/sys/kernel/core_pattern 可以控制 core 文件保存位置和文件格式.(建议将后缀改为进程号) 笔者这里简单起见, 不进行修改了.
编写 core 代码, 这里笔者利用线程访问了空指针
- #include <unistd.h>
- #include <thread>
- void core() {
- char* ch = nullptr;
- *ch = 'a';
- }
- int main() {
- auto t1 = std::thread(core);
- sleep(5);
- return 0;
- }
编译运行该代码, 产生段错误, 生成了 core 文件
利用 GDB 调试 core 文件
调试 core 文件需要利用原生编译出的二进制文件调试. 这里有一点需要注意的, 如果编译 C++ 文件之时没有加 - g 的编译选项, core 文件的调试内容会不够完整. 笔者这里建议开启对应的编译选项, 这会导致对应的二进制文件变大, 编译时间变长.(生产环境可以考虑关闭) 使用
gdb 二进制文件 core 文件
打开 core 文件.
core 文件列出了两个线程的信息. 我们需要判断对应的问题代码的定位, 接下来我们一起来梳理一下:
用 info thread 查看线程的运行情况, 在这里我们就可以判断代码 core 在什么线程之中了, 如果还是无法确定, 可以通过 thread apply all bt 列出更加详尽的堆栈信息.
通过上述信息可以确认, thread 1 的代码存在问题. 我们通过 thread 1 切换到 thread 1, 用 bt 显示堆栈信息继续追查:
之后我们来看看令人生疑的栈内容, 这里显然栈 0 是我们怀疑的代码, 用 frame 1 查看.
好了, 这里我们找到了引起问题罪魁祸首的代码, 访问了空指针.
小结
程序运行的 core 文件是我们调试代码十分重要依据, 通过 GDB 可以很好的给出我们修改代码的线索和参考, 熟悉掌握 GDB 的调试技巧, 能够大大解放我们调试问题代码的生产力.
2. 利用 Valgrind 判断内存泄露
亡羊补牢不如未雨绸缪, 与其等到出现程序崩溃时使用 GDB 来调试解决, 不如事前确认代码之中可能引发的问题. 所以笔者接下来要介绍一款来自大不列颠的 C++ 代码分析神器: Valgrind.(Valgrind 的作者也通过开发 Valgrind 获得了第二届 Google-O'Reilly 开源代码大奖~~~)
Valgrind 十分强大, 适用于内存分析, 泄漏检测, 锁分析, 性能评估. 笔者也只掌握了一些基本的入门使用. 希望这里能够抛砖引玉, 更多复杂的用法烦请参考官方文档 http://valgrind.org/docs/manual/QuickStart.html .
Valgrind 的安装
Valgrind 的安装很简单, 笔者的发行版带了对应的 deb 包. 通过 apt-get 的包管理工具就可以直接安装了, 其他的发行版也可以作为参考.
sudo apt-get install valgrind
Valgrind 的使用
与 GDB 类似, Valgrind 同样推荐使用 - g 作为编译参数. 能够更好的对代码进行分析. 这里我们依旧使用之前的例子进行测试:
valgrind ./untitiled
下面是 Valgrind 的分析结果:
这里有显示 Invalid write of size 1, 说明这里有一个不合法的写入, 并且写入了 1 个字节的内容. 也就是指的是我们之前代码之中写入空指针的行为.
接下来我们要展示 Valgrind 更加强大的功能. 它展示了程序的内存使用情况, 并且给出总结:
这里列出了多种的内存泄露情况:
definitely lost: 肯定的内存泄漏, 这表示在程序退出时, 有内存没有回收, 但是也没有指针指向该内存. 这种情况最为严重.
indirectly lost: 间接的内存泄漏, 如类之中定义的指针指向的内存没有回收. 这种情况和上述相同.
possibly lost: 可能出现内存泄漏. 这种情况需要仔细排查, 可能代码没有问题, 也可能有异常的内存泄露.
still reachable: 程序没主动释放内存, 在退出时候该内存仍能访问到. 这种情况一般问题不大, 因为程序退出之后操作系统会回收程序的内存, 所以这种情况一般问题不大.
这里没有给出具体泄露的内容, 需要加入参数 --leak-check=full 将完整的结果打印出来, 会指出对应的引起内存泄露的具体代码, 可以继续深入分析.
代码调优
这里进行代码调优的时, 需要利用 qcachegrind 来进行分析. 首先笔者先进行安装:
sudo apt-get install qcachegrind
之后我们调用 Valgrind 来生成运行数据:
valgrind --tool=callgrind -v main(需要分析的程序)
运行之后在目录下生成对应的分析数据, 我们用 qcachegrind 打开, 这里用的代码是笔者之前实现的 SkipList.
qcachegrind callgrind.out.29235
接下来我们来分析对应的结果:
上图显示了各个函数的被调用的耗时百分比, 我们可以选取对性能感兴趣的函数来进行深入分析. 我们下面继续分析其中一个函数被调用和它使用函数的性能情况
所以通过上述数据, 我们可以给出性能分析的证据和线索, 依据这些信息来更好的优化我们代码的性能.
3. 小结
本文介绍了亡羊补牢的工具 GDB, 也简介了未雨绸缪的 Valgrind . 通过上述工具对 C++ 程序更加深入分析. 工欲善其事, 必先利其器, 希望大家也能好好掌握这些提供生产力的工具, 让 C++ 不再恼人.
来源: https://yq.aliyun.com/articles/666102