读书笔记:高效C/C++调试
高效C/C++调试 Effective Debugging (美)严琦、卢宪廷
目 录
第1章 调试符号和调试器 1
1.1 调试符号 1
1.1.1 调试符号概览 2
- 全局变量
- 文件行号
- 数据类型
1.1.2 DWARF格式 3
1.2 实战故事1:数据类型的不一致 14
1.3 调试器的内部结构 16
1.3.1 用户界面 16
1.3.2 符号管理模块 16
1.3.3 目标管理模块 17
1.4 技巧和注意事项 21
1.4.1 特殊的调试符号 21
库的符号可能被部分剥离,重新编译进符号表
1.4.2 改变执行及其副作用 24
set var gFlags = 5;
1.4.3 符号匹配的自动化 25
1.4.4 后期分析 26
1.4.5 内存保护 27
1.4.6 断点不工作 27
断点不工作场景:
(1)调试符号不匹配
(2)共享库未加载
(3)启用了优化
1.5 本章小结 28
第2章 堆数据结构 29
2.1 理解内存管理器 30
2.1.1 ptmalloc 31
边界标签和盒子
2.1.2 TCMalloc 34
google开发
2.1.3 多个堆 38
2.2 利用堆元数据 39
2.3 本章小结 42
第3章 内存损坏 43
3.1 内存是怎么损坏的 44
3.1.1 内存溢出与下溢 44
用户代码写入超过分盘内存块,会覆写下一个内存块的标签
3.1.2 访问释放的内存 45
用户代码持有指向已释放指针
3.1.3 使用未初始化的值 46
info symbol 0x002ab
3.2 调试内存损坏 47
3.2.1 初始调查 49
3.2.2 内存调试工具 53
3.2.3 堆与栈内存损坏对比 53
3.2.4 工具箱 54
3.3 实战故事2:神秘的字节序转换 55
3.3.1 症状 55
3.3.2 分析和调试 56
3.3.3 错误和有价值的点 64
3.4 实战故事3:覆写栈变量 65
3.4.1 症状 65
3.4.2 分析和调试 65
3.5 本章小结 68
第4章 C++对象布局 69
4.1 对齐和大小端 69
4.1.1 对齐 69
字节:Byte,字:word
4.1.2 大小端 70
4.2 C++对象布局 71
4.3 实战故事4:访问已经释放的数据 94
4.3.1 症状 94
4.3.2 分析和调试 94
4.4 搜索引用树 95
4.5 本章小结 101
第5章 优化后的二进制 102
5.1 调试版和发行版的区别 102
5.2 调试优化代码的挑战 106
5.3 汇编代码介绍 108
5.3.1 寄存器 109
5.3.2 指令集 111
5.3.3 程序汇编的结构 113
5.3.4 函数调用习惯 116
5.4 分析优化后的代码 127
5.5 调试优化后的代码示例 130
5.6 本章小结 141
第6章 进程镜像 142
进程映射:这个视图提供了从内核角度观察进程地址空间的视角。
# cat /proc/<pid>/maps
Address perm offset device inode pathname
55bc80a11000-55bc80a32000 r--p 00000000 fd:00 1054763 /usr/sbin/nginx
说明:
- Address: 显示内存区域的地址范围。在linux内核中,这也被称为虚拟内存区域(VMA)
- perm: 展示权限位。其中,rwx分别代表读、写和执行权限。“-”表示该权限被禁止。这一列的最后一个字符要么是s要么是p,分别代表该内存区域是共享的还是私有的。
- offset: 表示该内存区域关联的磁盘文件的偏移量。
- device: 展示以marjor:minor格式表示的设备号。
- inode: 给出设备的inode号。
- pathname: 显示相关文件的路径。
6.1 二进制文件格式 144
6.2 运行期加载和链接 148
6.3 进程映射表 153
6.3.1 可执行文件 154
6.3.2 共享库 156
6.3.3 线程栈 157
6.3.4 无名区域 157
6.3.5 拦截 158
6.3.6 链接时替换 158
6.3.7 预先加载代理函数 159
6.3.8 修改导入和导出表 159
6.3.9 对目标函数进行手术改变 164
6.3.10 核心转储文件格式 166
6.3.11 核心转储文件分析工具 169
6.4 本章小结 170
第7章 调试多线程程序 171
7.1 竞争条件 171
7.2 它是竞争条件吗 172
7.3 调试竞争条件 174
7.4 实战故事5:记录重要区域 175
7.4.1 症状 175
7.4.2 分析调试 175
7.5 死锁 177
7.6 本章小结 179
第8章 更多调试方法 180
8.1 重现错误 180
8.1.1 归因 181
8.1.2 收集环境信息 182
8.1.3 重建环境 184
8.2 防止未来的bug 184
8.2.1 知识保留和传递 185
8.2.2 增强提前检查 185
8.2.3 编写更好调试的代码 185
8.3 不要忘记这些调试规则 189
8.3.1 分治法 189
8.3.2 退一步,获取新的观点 189
8.3.3 保留调试历史 190
8.4 逆向调试 190
reverse-step: 反向逐步执行
8.4.1 rr:Record and Replay 191
8.4.2 rr注意事项 191
8.5 本章小结 192
第9章 拓展调试器能力 193
9.1 使用Python拓展GDB 193
9.1.1 美化输出 194
9.1.2 编写自己的美观打印器 195
9.1.3 将重复的工作变成一个命令 197
9.1.4 更快地调试bug 198
9.1.5 使用Python设置断点 200
9.1.6 通过命令行来启动程序和设置断点 203
9.2 GDB自定义命令 203
9.3 本章小结 206
第10章 内存调试工具 207
内存调试工具底层算法分为3种类型:
(1)填充字节方法:最常用的是在每个内存块的开头和末尾添加额外的填充字节。有缺陷的代码可能会越过分配的内存块的界限,修改这些填充字节。调试工具在内存API的入口malloc, free检查这些 填充字节。如果发现填充字节被修改,就表示内存损坏。工具报告错误的上下文。
(2)系统保护页方法:工具在可能越界的内存块前后设置一个不可访问的系统保护页。程序非法访问时,系统通过硬件检测到。这种方法可以立即捕获无效的内存访问。但是频繁的设置系统保护页会造成内存和CPU开销大。
(3)动态二进制分析:valgrind可以运行任何现有程序而无须重新编译。在内部使用影子内存跟踪程序内存使用情况,每次内存访问都会更新影子内存。缺点:细粒度和软件模式检查性能下降。google address sanitizer通过编译器在生成的二进制文件中插入诊断代码,不需要二进制检测框架。
对于复杂问题,常见的方式是根据收集到的信息尝试在受控环境中重现问题。然而,如果问题的重现具有很强的时序相关性。或者每次运行时内存块的地址可能会改变。这时我们可以通过各种工具尽早地检测到内存。
有时错误可能因为分配算法改变而掩盖。但并不意味着修复。
不同内存调试工具的对比
调试特性 | ptmalloc MALLOC_CHECK _ | Asan | AccuTrak | Valgrind/Memcheck |
---|---|---|---|---|
实现原理 | 软件填充 | 软件影子内存 | 硬件和软件填充 | 软件影子内存 |
检测上溢出 | Yes | Yes | Yes | Yes |
检测下溢出 | No | Yes | Yes | Yes |
检测重复释放 | Yes | Yes | Yes | Yes |
检测释放后使用 | No | Yes | Yes | Yes |
检测使用未初始化内存 | No | No | No | Yes |
粒度 | 字节 | 字节 | 字节 | 位 |
变慢程度 | 小 | 中 | 小 | 大 |
空间开销(每个用户块) | 1-16字节 | 1字节 | 8字节 or 1系统页 | 与块大小相同 |
配置 | No | No | Yes | No |
代码开源 | Yes | Yes | Yes | Yes |
重新编译 | No | Yes | No | No |
10.1 ptmalloc’s MALLOC_CHECK_ 208
10.2 Google Address Sanitizer 212
10.3 AccuTrak 213
10.4 有效地调试内存损坏 225
10.5 实战故事6:内存管理器的崩溃问题 228
10.5.1 症状 229
10.5.2 分析和调试 229
10.6 本章小结 235
第11章 Core Analyzer 236
解析进程的核心转储文件或内存映像
11.1 使用示例 237
11.2 主要功能 239
11.2.1 搜索引用的对象(水平搜索) 239
11.2.2 查询地址及其底层对象(垂直搜索) 240
11.2.3 内存模式分析 241
11.2.4 查询堆内存块 242
11.2.5 堆遍历(检查整个堆以发现损坏并获取内存使用统计) 242
11.3 本章小结 246
第12章 更多调试工具 247
12.1 strace 247
作用:程序和操作系统如何交流
12.1.1 常用功能 247
12.1.2 常用附加选项 248
12.2 实战故事7:僵尸进程 248
strace -o debug.txt -f -e trace=signal <program>
12.2.1 遇到难题 248
12.2.2 揭示bug的真相 249
12.3 Perf 249
作用:分析系统性能瓶颈
收集性能数据
(1)CPU周期
(2)指令计数
(3)缓存未命中
(4)分支预测错误
(5)内存访问
12.4 eBPF 250
作用:高度定制和细致分析
12.4.1 准备环境 251
12.4.2 编写代码 251
12.4.3 编译程序 252
12.4.4 加载和运行程序 254
12.5 实战故事8:链接问题 255
12.5.1 切入 255
12.5.2 更奇怪的事情 258
12.5.3 柳暗花明 259
12.5.4 补充 260
12.5.5 结论 261
12.6 实战故事9:临时变量的生命周期 261
12.7 本章小结 264
第13章 崩溃发送机制 265
崩溃报告
13.1 客户端 266
13.2 远程报告收集服务器 267
13.3 终端集成器 268
13.4 本章小结 268
第14章 内存泄漏 269
g++ -fsanitize=address
14.1 为什么RAII是基石 269
14.2 分析 270
14.3 调试内存泄漏 273
14.4 本章小结 275
第15章 协程 276
15.1 C++协程 277
15.2 协程的切分点 279
15.3 协程之诺 281
15.4 本章小结 283
第16章 远程调试 284
16.1 GDB远程调试 285
16.2 Visual Studio远程调试 286
16.3 本章小结 287
第17章 容器世界 288
17.1 容器示例 288
17.2 容器应用 289
17.3 C/C++容器调试 291
17.4 实战故事10:CrashLoopBackOff 292
17.5 实战故事11:liveness failure 292
17.6 本章小结 294
第18章 尽量不要调试程序 295
18.1 借助编译器来提前发现错误 295
18.2 编写简短的实验代码 295
18.3 日志和监控 296
18.3.1 日志 296
要素:
- 日志级别
- 时间戳
- Json格式化,方便日志解析
18.3.2 监控 297
- metrics
- alerts
- dashboards
18.4 遵循最佳编码实践 297
18.5 本章小结 298
附录A 调试混合语言 299
附录B 在Windows/x86环境下进行程序调试 301
B.1 PE文件格式 301
B.2 Windows Minidump格式 306
附录C 一个简单的C++ coroutine程序 309
资料:
core analyzer https://cloud.tencent.com/developer/article/2408828
作者博客:高效C/C++调试 - CrackingOysters的文章 - 知乎
https://zhuanlan.zhihu.com/p/675726977