猜你喜欢
高效C/C++调试

高效C/C++调试

书籍作者:严琦 ISBN:9787302649717
书籍语言:简体中文 连载状态:全集
电子书格式:pdf,txt,epub,mobi,azw3 下载次数:8840
创建日期:2024-05-05 发布日期:2024-05-05
运行环境:PC/Windows/Linux/Mac/IOS/iPhone/iPad/Kindle/Android/安卓/平板
内容简介

《高效C/C++调试》是关于软件调试技术的深度探索,融合了作者的实践智慧。书中不仅指导读者如何使用专业的调试工具,还介绍了如何宏观和微观地分析问题,并最大限度地发挥调试器功能。此外,书中还深入解读了调试背后的技术原理,如调试符号、内存管理及系统内核对内存的操作机制,以揭示内存管理的关键性和复杂性。

除了介绍基础概念外,本书还涵盖了许多增强调试能力的工具与插件。尽管焦点集中在C/C++,但其中的策略与技巧具有普适性,适用于多种编程语境。内容包括从内存泄漏预防调试、Linux下的eBPF和strace调试方法,到Kubernetes容器调试,再到C++20的协程与崩溃信息收集策略。

《高效C/C++调试》主要面向具有C/C++编程基础的读者,同时也非常适合对软件技术原理有深入探索兴趣的读者以及追求高效调试技巧的开发人员。


作者简介

严琦 毕业于中国科技大学和美国伦斯勒理工学院。 毕业后近三十年里曾先后任职于五家软件技术公司从事技算机编程工作,从初级程序员成长为一个中等规模的软件公司的首席架构师,涉及包括嵌入式系统,有限元分析,计算机辅助设计,商业智能系统等领域,致力于微处理器,编译器,服务器,系统内核等技术。作者有多项开源项目在实际应用中受到公司内外的肯定和感谢,在工作中申请并获批一项软件专利,并有另一项专利在审批中。 卢宪廷 本科毕业于天津大学,研究生东南大学。目前在微策略软件担任高级软件工程师,负责设计和开发优化企业全域搜索引擎;专注于高稳定/事件驱动异步架构/C++/Rust编程。

编辑推荐

《高效C/C++调试》是一本精心编写的实用指南,为软件开发工程师提供了宝贵的调试技巧和知识。作者通过多年的一线经验,深入讲解了如何高效地调试软件内存故障、理解C++对象模型、阅读汇编代码等重要内容。书中还介绍了调试器插件和工具的开发,拓展了开发者的视野。无论是初学者还是有经验的开发人员,都能从本书中获得实际的指导和启发。丰富的实战例子和代码片段让读者更好地理解和应用所学知识。如果你想提升调试能力、掌握C/C++高级内容,并成为实战资质的中高级开发人员,那么这本书绝对是你的不二选择。

前言

序一

这是一本关于调试的书。作为一名程序员,在多年的写代码和调试代码的过程中,我一次又一次地经历了过山车般的情绪变化:困惑,沮丧,兴奋,周而复始,特别是在处理看上去永无止境的程序错误(bug)时尤其如此。随着时间的推移,我掌握了更多的调试技能,对要支持的产品和架构有了更多的了解,大部分问题变得容易解决。然而,偶尔也会出现一些棘手的问题,试图缩小范围并解决一个真正困难的问题可能需要数小时甚至数天的时间。

记得有一次,我花了几个月的时间尝试修复一个问题,这个问题的奇怪之处在于它只在每个星期二在客户的服务器上发生(我将在稍后的内存损坏一章中讲述这个实战故事)。我相信这不仅仅是我的故事,很多软件工程师都曾有过同样的经历。因为计算机已经深入我们的生活几十年,软件行业积累了大量的遗留代码。因此,我们中的许多人不得不花费大量时间来维护和完善现有程序。即使你为全新的项目编写代码,迟早也要对它进行调试。不管喜欢与否,调试bug是不可避免的,它已经成为软件开发工程师日常工作的一部分。

另一方面,调试也可以有很多乐趣。在经历了许多挫折和无聊的时刻后,我学到了许多探索和寻找bug的技巧,并开始感到兴奋和满足。每当我解决了案子中具有挑战性的问题时,我都会获得同事们的感谢与赞许。这让我觉得自己像一个能解决问题的真正的侦探。在现实世界的程序中有很多看似很困难的bug,我常常听到类似的抱怨和借口—“这是我见过最奇怪的事情”,“这段代码存在了这么多年,如果它有bug,早该失败了”,或者“我已经审阅我的代码好多遍了,这是不可能发生的”。随着在实战中积累的经验的增加,我更加相信通过正确的解决方案和基本技能,都可以有效地揭示并解决bug。无论表面上看起来多么神秘或不可能的问题,当我们最终找到根本原因时,一切都说得通了,毕竟计算机程序是那么虔诚地完全地照着我们编写的方式运行,即使那是错误的。

本书讨论调试方法论。尽管关于这一主题已经有很多优秀的书籍,但我相信通过总结我个人的实战经验,可以为读者提供更多实用的观察方法和技巧。从学校毕业以后,我阅读了各种关于编程和调试的书籍,曾以为已经完全理解并对解决任何问题都充满信心。然而,实际问题往往比书中的例子更为复杂。我经常在工作中找不到任何线索,无法将书本知识应用于实际问题。

回想起那些初出茅庐的岁月,一方面是我没有完全理解书中的内容,另一方面是大部分书籍都是从设计和编程的角度出发的。它们可能充满了使用调试器命令的技巧,但当问题类型和维度迷雾重重时,它们缺乏如何起步、如何从最基础去分解问题,以及如何选择不同的调试策略和有效利用调试器的各种功能的介绍。我看到许多年轻的工程师在没有明确计划的情况下就急切地启动调试器。对于一些人来说,调试程序就是使用调试器而已。在本书中,我将通过深入挖掘一些内部数据结构,展示许多调试过程的实战例子,并提出可操作的实用建议,以缩小理论知识和可用技术的沟壑。

本书的示例包含了大量的代码片段和实际案例。在编写过程中,我尽可能地运用真实发生的例子,除非在某些情况下,理论性例子的简明性和清晰度优于实战例子。此外,本书还专门介绍了调试器插件和实用工具的开发。这些工具能够增强现有的调试器,拓宽我们的视野,要么提供新的角度审视问题,要么帮助我们更深入地研究问题。尽管本书主要探讨的是C/C++,但书中所介绍的方法和策略是通用的,独立于特定语言。

通常,教材并不覆盖特定调试器、内存管理库或者编译器的内部实现,许多软件开发人员也不熟悉这些知识,因为在设计和编程阶段通常并不需要关注这些内容,而且常规的调试工作也不需要。有些人可能认为,除了软件的开发者之外,其他人没有必要去学习这些知识。然而,这些知识对于我们对可能会观察到的情况以及在错误发生时可能会错过的细节具有深远的影响。

如果你在软件行业待了足够长的时间,就会遇到需要深入理解程序行为的情况。例如,由于代码优化或者缺少足够调试符号,调试器可能无法正确地显示局部变量;如果栈损坏极为严重,调试器无法正确打印调用栈,因为它依赖保存在栈上的特定数据结构;程序也可能在看起来不可能崩溃(crash)的地方崩溃了。在这些情况下,我们必须比普通程序员挖掘得更深:可能需要梳理编译器布局的栈空间,或者内存管理库的堆数据结构,甚至需要手动重新生成调用栈和数据对象。

在本书中,我尝试铺就调试符号、调试器内部实现、内存管理器的内部结构、分析优化后的程序和C++对象模型等基础知识。这些知识肯定可以帮助你突破学习瓶颈,进一步提高调试技能,从而更上一层楼。

许多非法操作的行为,如常见的内存溢出、重复释放内存块、访问释放后的对象、使用未初始化的变量等,根据编程语言的标准和文档都是未定义行为。这基本上意味着这些违规行为的实际结果完全是随机的或取决于具体实现;它们可能在一个环境无害,但是在另一个环境就是灾难性的。一个经典的例子是:同样有bug的代码在一个平台上没有发生任何问题,可以正常运行,但在另一个平台上,程序就会崩溃。最糟糕的情况是一个bug在初始阶段没有任何错误的迹象,在它完成了某些恶意操作很久以后,才出现奇怪和意料之外的行为。

从调试的角度看,理解特定实现中的“未定义”行为是必要的。这与我们不知道也不应该假设任何关于“未定义”行为的设计和编程实践相违背。一种实现的内部数据结构不同于另一种实现。因此,有些人可能选择忽略这些“未定义”行为。但是,当我们面临由未定义行为引起的未知问题时,对这些内部数据结构的理解可以带领我们走出迷雾,找到最终的解决方案。因此,在我看来,了解程序如何因这些“未定义”行为而失败对于调试许多棘手问题至关重要。我的工作经历也证明了这一点。本书中的许多示例将展示如何利用这些知识更有效地进行调试。

本书假设读者具有基本的计算机科学和软件开发学习经历。读者至少具有一年的实际编程经验,并且知道怎么使用调试器解决较为复杂的问题。在整本书中,我致力于关注书的主题—更高效的调试。为了避免偏离主题,一些相关的概念和术语被简要描述或者以跳跃性方式串联在一起。对于核心知识,我尽量以实际操作为主(可能不完全准确或者不具有学术性)来解释。我们的目的是帮助读者掌握基本的概念,并能够快速将这些知识应用到调试实践中。

通过互联网,可以方便地获取几乎所有事物的权威性定义。如果读者对书中提及的内容不太熟悉,或者需要更详细的解释,可以通过网上搜索来解决疑惑。本书末尾的引用也可以为读者提供线索。希望本书没有重复很多读者已经知晓的内容,或者一些可以轻松获取的信息,比如如何使用某个工具的命令,通常都可以在它的手册中找到清晰的解释。

本书的许多章节是独立的,读者可以跳到任何感兴趣或适合当前工作的章节;跳过熟悉或者不感兴趣的章节也没有问题。一些章节会介绍调试器、运行时或者语言的底层细节,也许这些知识并非必需,但它确实能够帮助你应对更复杂的问题。本书的许多例子都使用Linux/x86_64平台,但是底层方法通过微小的调整就可以应用到其他平台上。

附录提供了其他平台的丰富的示例,鼓励读者使用本书提供的源文件和链接生成对应的项目,并加以应用。这些实战的示例可以进一步帮助读者理解书中讨论的话题,也可以作为开发自己项目的起点。事实上,一些程序是我在工作中开发的,从那时起它们就成为不可或缺的工具。其中大部分源代码都是跨平台的,如果碰巧你使用其中某个平台,它可能会立即引起你的兴趣;如果不碰巧,那么当你理解这些设计背后的思路后,自己编写工具也并非难事。

根据我的个人经验,许多程序bug,特别是用C/C++编写的程序,都与内存相关。从各个角度理解内存怎么分配和使用非常必要。本书的大部分内容聚焦于应用程序、编译器、内存管理器、系统加载器/连接器和内核虚拟内存,以及如何从微观到宏观看待一块内存。

内存是动态资源,会在程序执行的各个阶段发生变化。在本书中,读者将了解内存管理器如何分配内存,编译器如何在分配的内存块中布局应用程序的数据结构,以及栈是如何被局部变量和函数参数使用的。此外,读者还将了解系统链接器和加载器如何跟系统虚拟内存管理器合作,创建进程的虚拟地址空间。应用程序以源文件声明的形式看待数据对象:它们要么是原始的数据类型,要么是其他类型的聚合。编译器会添加更多隐藏的数据成员,例如指向虚函数表的指针,并在必要时为了对齐而进行填充。为了满足对齐要求和其自身的隐藏标签,内存管理器会插入额外的字节。系统内核负责使用由页构成的段来记录进程的内存。

当研究一个有疑问的数据对象时,有经验的工程师可以理解以上组件的各个视角:从编译器的角度来看,该数据对象的大小和结构定义是怎样的;从内存管理器的角度看,该数据对象的内存块被释放了还是在使用中;从链接器和加载器的角度看,该数据对象是在代码段、全局数据段、堆数据还是栈段;从内核虚拟内存管理器的角度看,该数据对象是不是被某些权限保护着。所有这些信息可以作为创建一个理论的基石,验证或证伪程序错误原因的假设。毋庸置疑,当调试与内存相关的问题时,这些知识是无价的。

在许多情况下,调试是一个试错的过程。一个特定的问题有各种可能的原因,工程师通常通过分析问题的症状来开始调研,接着根据观察和推理提出一个可能的原因假设,然后证明这个假设,并给出一种修复方案,最后测试和验证修复方案。如果理论无法解释现象或者修复方案不行,该该参数需要重复上面的步骤。调试同一个问题有多种方法,每个人也有自己偏好的方法和风格。本书展示的例子和技巧是我在实践中积累的,旨在与读者分享其中的方法。当一种方法看上去没有出路时,另一种使用其他工具的方法可能就是你所需要的。同样地,非常欢迎读者跟我分享自己的经验和调试方法。


严琦





序二

在编程的道路上,每一个程序员都不可避免会遇到调试的挑战。我仍然记得那些难忘的调试经历:大学时期,我和朋友共同调试机器人的程序;进入职场后,我又开始钻研数百万行的C++代码。从初入编程世界时的探索与迷茫,到如今的稳健与沉稳,这背后蕴含着无数次的学习与实践。更为关键的是,我们站在诸多行业前辈的肩膀上。本书的第一作者严琦,正是其中一位令人尊敬的巨人。幸运的是,我在美国工作期间得到了他的直接指导和悉心帮助。

当清华大学出版社的编辑询问我是否有兴趣出版书籍时,我想到了从学生时代到职场的点滴经验。我常常与同学或者同事分享自己的体会,也在知乎账号(CrackingOysters)上发表相关文章,但要整理成一本完整的书籍,仍有不少工作要做。这时,我想到了严琦以及他那份关于高效调试的英文书稿。于是,我建议基于这份书稿共同打造一本新的书籍。因此,本书中绝大部分的内容都深受他的经验和智慧的启发。同时我在他的书稿的基础上增添了关于Google Address Sanitzer和逆向调试的内容、以及编写了第9章和第12~18章的内容。

希望这本书能为编程爱好者提供实用的知识和启示。如果读者在书中发现了错误,欢迎指正。我乐于分享我的学习体会,因为总有热心的朋友愿意纠正我的错误。另一方面,读者所认为的“错误”可能只是对知识理解的不同,在讨论中可以加深或者修正理解。


卢宪廷




配书资源

为方便读者使用本书,本书提供了源代码文件,需要使用微信扫描下面的二维码获取。如果阅读中发现问题或有疑问,请通过[email protected]与我们联系,邮件主题请写“高效C/C++调试”。




目录

第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

1.4.3 符号匹配的自动化 25

1.4.4 后期分析 26

1.4.5 内存保护 27

1.4.6 断点不工作 27

1.5 本章小结 28

第2章 堆数据结构 29

2.1 理解内存管理器 30

2.1.1 ptmalloc 31

2.1.2 TCMalloc 34

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

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

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

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

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

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

12.2.1 遇到难题 248

12.2.2 揭示bug的真相 249

12.3 Perf 249

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

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

18.3.2 监控 297

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


产品特色