CPP Debug Analyze 01
Last updated on a minute ago
C++ Debug/Analyze 专题指南-01
GDB篇
GDB是Linux下非常好用且强大的调试工具,虽然它是命令行模式的调试工具,能够让用户在程序运行时观察程序的内部结构和内存的使用情况。
一般来说,GDB主要帮助你完成下面四个方面的功能:
- 按照自定义的方式启动运行需要调试的程序。
- 可以使用指定位置和条件表达式的方式来设置断点。
- 程序暂停时的值的监视。
- 动态改变程序的执行环境。
GDB的用法
常用调试命令
GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,并且该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),GDB才会派上用场。
所以在编译时需要使用 gcc/g++ -g 选项编译源文件,才可生成满足 GDB 要求的可执行文件。
此处可以参考之前的内存泄漏专题指南,描述了如何加入”-g”以及验证是否存在调试信息
| 调试命令 (缩写) | 作用 |
|---|---|
| (gdb) break (b) | 在源代码指定的某一行设置断点,其中xxx用于指定具体打断点位置 |
| (gdb) run (r) | 执行被调试的程序,其会自动在第一个断点处暂停执行。 |
| (gdb) continue (c) | 当程序在某一断点处停止后,用该指令可以继续执行,直至遇到断点或者程序结束。 |
| (gdb) next (n) | 令程序一行代码一行代码的执行。 |
| (gdb) step(s) | 如果有调用函数,进入调用的函数内部;否则,和 next 命令的功能一样。 |
| (gdb) until (u) (gdb) until (u) location |
当你厌倦了在一个循环体内单步跟踪时,单纯使用 until 命令,可以运行程序直到退出循环体。 until n 命令中,n 为某一行代码的行号,该命令会使程序运行至第 n 行代码处停止。 |
| (gdb) print (p) | 打印指定变量的值,其中 xxx 指的就是某一变量名。 |
| (gdb) list (l) | 显示源程序代码的内容,包括各行代码所在的行号。 |
| (gdb) finish(fi) | 结束当前正在执行的函数,并在跳出函数后暂停程序的执行。 |
| (gdb) return(return) | 结束当前调用函数并返回指定值,到上一层函数调用处停止程序执行。 |
| (gdb) jump(j) | 使程序从当前要执行的代码处,直接跳转到指定位置处继续执行后续的代码。 |
| (gdb) quit (q) | 终止调试。 |
GDB调试执行异常崩溃的程序
在大多数情况下,我们遇到异常崩溃的程序才会使用GDB来定位问题,而GDB又有可能影响效率,所以若不确定能否必现问题,则coredump文件是较好的选择
在Linux操作系统中,当程序执行发生异常崩溃时,系统可以将发生崩溃时的内存数据、调用堆栈情况等信息自动记录下载,并存储到一个文件中,该文件通常称为core 文件,Linux 系统所具备的这种功能又称为核心转储(core dump)
生成Core方法
需要确认当前会话的ulimit –c,若为0,则不会产生对应的coredump,需要进行修改和设置
1 | |
当为0时,即便程序core dump了也不会有core文件留下。我们需要让core文件能够产生,设置core大小为无限:
1 | |
*更改core dump生成路径
因为core dump默认会生成在程序的工作目录,但是有些程序存在切换目录的情况,导致core dump生成的路径没有规律,
所以最好是自己建立一个文件夹,存放生成的core文件。
1 | |
调试程序和Core文件
1 | |
VSCODE GDB调试c++程序
鉴于GDB需要手动交互略显麻烦,查看代码也不够清晰,基于Vscode GUI的DEBUG工具链就显得尤为简单
Apollo docker环境的GDB运行
对于简单的c++程序,参考👆上面的链接即可,对于apollo这种以so被调用的形式,则需要定制相关的文件
在
/apollo/.vscode目录下添加launch.json和tasks.json文件修改tasks.json文件
此处可以添加多个task,并且在launch.json中
preLaunchTask处被预先执行,配置环境1
2
3
4
5
6
7
8
9
10
11
12
13
14
15{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "env",
"type": "shell",
"command": "source",
"args": [
"/apollo/cyber/setup.bash"
]
}
]
}修改launch.json文件
此处定义了debug的程序和配置文件,同时指定了源码路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch",
"preLaunchTask": "env",
"program": "/apollo/bazel-bin/cyber/mainboard/mainboard",
"args": [
"-d",
"modules/omnisense/drivers/lidar/innovusion/dag/01_single.dag"
],
"stopAtEntry": false,
"cwd": "/apollo",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
}
]
}接着就可以按下
F5,即可正常运行了,常用的断点等功能也能正常使用
UnitTest单元测试篇
unittest的重要性不用多说,可以让你的程序代码质量大幅提升、协助你进行良好的程序设计。
而单元测试的覆盖率越高,则也可以认为代码可能存在的bug越少。
并且良好的单元测试也有利于code review。
在工具上,我们会使用下面这些:
gcov是由GCC工具链提供的代码覆盖率生成工具。
对于代码覆盖率工具所做的工作,可以简单的理解为:标记一次运行过程中,哪些代码被执行过,哪些没有执行。
因此,即便没有测试代码,直接运行编译产物也可以得到代码的覆盖率。只不过,通常情况下这样得到的覆盖率较低罢了
lcov是gcov工具的图形前端。它收集多个源文件的gcov数据,并生成描述覆盖率的HTML页面。生成的结果中会包含概述页面,以方便浏览。
UnitTest Bazel实战
google test官网详细描述了bazel或cmake的使用流程,而Apollo中的bazel也集成了gtest
当我们写完了unittest后,只需使用如下命令:
1 | |
待编译完成后,则会自动执行单元测试,若没有问题,则会输出PASSED,显示如下:
1 | |
Coverage代码覆盖率
在进行单元测试之后,我们当然希望能够直观的看到我们的测试都覆盖了哪些代码。
理论上,如果我们能做到100%的覆盖我们的所有代码,则可以说我们的代码是没有Bug的。
但实际上,100%的覆盖率要比想象得困难。对于大型项目来说,能够达到80% ~ 90%的语句覆盖率就已经很不错了。
覆盖率的类型
先来看一下,当我们在说“覆盖率”的时候我们到底是指的什么。
实际上,代码覆盖率有下面几种类型:
- 函数覆盖率:描述有多少比例的函数经过了测试。
- 语句覆盖率:描述有多少比例的语句经过了测试。
- 分支覆盖率:描述有多少比例的分支(例如:
if-else,case语句)经过了测试。 - 条件覆盖率:描述有多少比例的可能性经过了测试。
这其中,函数覆盖率最为简单,就不做说明了。
语句覆盖率是我们最常用的。因为它很直观的对应到我们写的每一行代码。
而分支覆盖率和条件覆盖率可能不太好理解
以下面这个C语言函数为例:
1 | |
这个函数中包含了一个if语句,因此if语句成立或者不成立构成了两个分支。所以如果只测试了if成立或者不成立的其中之一,其分支覆盖率只有 1/2 = 50%。
而条件覆盖率需要考虑每种可能性的情况。
对于if (a && b)这样的语句,其一共有四种可能的条件:
- a = true, b = true
- a = true, b = false
- a = false, b = true
- a = false, b = false
综上,在编写代码的时候,尽可能的减少代码嵌套,并且简化逻辑运算是一项很好的习惯。便于测试的代码也是便于理解和维护的,反之则反。
有了这些概念之后,我们就可以看懂测试报告中的覆盖率了。
Coverage Bazel实战
由于Apollo Bazel中集成了LCOV,其他编译环境的配置则不在此展开,可以自行搜索。
当完成上面的UintTest后,我们就可以检查覆盖率了:
1 | |
一切正常的话,会输出以下信息:
1 | |
能看到其中50.4%和59.9%则是输出的覆盖率了,相关的html格式报告则存放于/apollo/.cache/coverage路径下。
此时我们可以进入到路径/apollo/.cache/coverage,然后运行一个http server,则就可以在浏览器中查看细节了:
1 | |
此时打开浏览器,输入127.0.0.1:8000,则会显示类似如下的界面。

可以看到,除了第三方库httplib.h头文件,其他的代码覆盖率都能达到90%。
而如果先前也已经完成了内存泄漏的检查,到这里基本上可以认为自己的代码已经相当稳定了,是可以交付生产或者进行Code Review的了。
Perf/Profiling性能/分析篇
Bottlenecks occur in surprising places, so don’t try to second guess and put in a speed hack until you’ve proven that’s where the bottleneck is. - Rob Pike
性能分析,Linux下有一个非常好用的工具,叫做perf。几乎每个发行版都有它的安装包。perf诞生于2009年,是一个内核级的工具;另外,还有一个工具叫做gperftools(pprof),是google的产品,是一个应用级别的产品。
不得不提的一点是,perf和gperftools对性能的影响,虽然不是特别大,但也尽量不要在线上环境使用它们,这个性能损耗率大概在30%左右。
由于perf仅适用于linux,且对内核版本有要求,而gperftools则没有这个限制,使用也会更宽泛,我们便以此来示范
gperftool介绍
gperftools包含了一套分析工具,在此我们只使用它的性能分析模块(cpu profiler)
安装
1 | |
也可以使用源码安装,
1 | |
使用
它的使用较为简单,一般情况下仅需链接-lprofiler即可,动态库和静态库都支持
Cmake
只需要**TARGET_LINK_LIBRARIES(${TARGET} tcmalloc profiler)**即可
用例
一般情况下,我们使用性能分析有以下几种需求或场景
程序运行即开始性能分析(即从启动程序到结束)
1
CPUPROFILE=./path2save.prof ./execute此处的CPUPROFILE即是指定路径
监控部分函数
1
2
3
4
5
6
7
8#include <gperftools/profiler.h>
int main(){
ProfilerStart("test.prof"); // 指定所生成的profile文件名
func_prof();
ProfilerStop(); // 结束profiling
return 0;
}分析一直运行的程序
一直运行的程序由于不能正常退出,所以不能采用上面的方法。我们可以用信号量来开启/关闭性能分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30#include <gperftools/profiler.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void gprofStartAndStop(int signum) {
static int isStarted = 0;
if (signum != SIGUSR1) return;
//通过isStarted标记未来控制第一次收到信号量开启性能分析,第二次收到关闭性能分析。
if (!isStarted){
isStarted = 1;
ProfilerStart("test.prof");
printf("ProfilerStart success\n");
}else{
ProfilerStop();
printf("ProfilerStop success\n");
}
}
int main(){
signal(SIGUSR1, gprofStartAndStop);
while(1){
printf("call f\n");
func_prof();
sleep(1);//为了防止死循环,导致信号处理函数得不到调度
}
return 0;
}通过kill命令发送信号给进程来开启/关闭性能分析:
1
2
3用top命令查看进程的PID
kill -s SIGUSR1 PID //第一次运行命令启动性能分析
kill -s SIGUSR1 PID //再次运行命令关闭性能分析,产生test.prof这种方式适合灵活关闭profile,不用重启启动服务,适合在线上查看。
分析
当生成了profile后,就可以使用google-pprof工具来转换为我们能分析的文件。
以下为常用的几种目标输出:
1 | |
以输出的svg文件为例,我们可以使用浏览器打开,其中占用面积越大则意味着消耗了更多的cpu

树上的每个节点代表一个函数,节点数据格式:
- 函数名 或者 类名+方法名
- 不包含内部函数调用的样本数 (百分比)
- 包含内部函数调用的样本数 (百分比) ,如果没有内部调用函数则这一项数据不显示
Apollo Bazel实战
Apollo Bazel中集成了pprof,其他的环境或者更详细的配置可以参考https://gperftools.github.io/gperftools/cpuprofile.html
当我们需要进行性能分析时,只需要使用如下命令编译:
1 | |
需要注意的事,一般类似的工具都需要运行流程可正常退出(此处若异常退出则无法统计结果)
当然,有时候我们只想要统计部分时间或者部分函数,那我们就需要参考👆上面的用例进行定制代码
运行的时候,只需要增加环境变量,填入我们希望存放的.prof路径即可
1 | |
当运行结束或者我们Ctrl+C退出时,将会在命令行输出以下信息,即为收集到了相关信息:
1 | |
火焰图(Flame Graphs)
火焰图(flame graph)是性能分析的利器,通过它可以快速定位性能瓶颈点。一般perf或gperftools产生的数据可以导出为火焰图,以方便将数据呈现得更为直观。
安装
火焰图时脚本工具,只需要将其仓库下载即可使用
1 | |
gperftools生成火焰图
在上节生成了profile文件后,我们只需要:
1 | |
将会在当前目录生成svg文件,这时候就可以用浏览器打开并交互了

火焰图有以下特征(这里以 on-cpu 火焰图为例):
- 每一列代表一个调用栈,每一个格子代表一个函数
- 纵轴展示了栈的深度,按照调用关系从下到上排列。最顶上格子代表采样时,正在占用 cpu 的函数。
- 横轴的意义是指:火焰图将采集的多个调用栈信息,通过按字母横向排序的方式将众多信息聚合在一起。需要注意的是它并不代表时间。
- 横轴格子的宽度代表其在采样中出现频率,所以一个格子的宽度越大,说明它是瓶颈原因的可能性就越大。
- 火焰图格子的颜色是随机的暖色调,方便区分各个调用信息。
- 其他的采样方式也可以使用火焰图, on-cpu 火焰图横轴是指 cpu 占用时间,off-cpu 火焰图横轴则代表阻塞时间。
- 采样可以是单线程、多线程、多进程甚至是多 host