大数跨境
0
0

谈一谈 C/C++ 程序崩溃与异常该如何快速定位

谈一谈 C/C++ 程序崩溃与异常该如何快速定位 CppGuide
2024-02-01
1

作为开发人员,我们都知道C/C++程序中不可避免的挑战之一就是程序崩溃。当面对这样的问题时,核心转储(core dump)成为了我们不可或缺的调试工具。但是,如何高效地使用这些工具进行故障排除呢?

在我们的实际开发经验中,就遇到过一个棘手的问题,我们的服务器程序在测试阶段出现了随机崩溃。经过一段时间的调试,我们怀疑这可能是由于内存越界错误引起的,问题似乎出在一个特定的数据对象上。当程序尝试更新该对象的一个数据成员时,紧随其后的数据对象就会被损坏。

然而,这段代码看似是无辜的,因为它只是在访问自己的数据成员。令人困惑的是,这个简单操作如何会损坏另一个数据对象。通过深入调查,我们发现这个问题对象是在一个模块中创建的,然后传递到另一个模块,在那里进行数据成员的更新。

在一番探索后,我们发现两个模块对同一个数据对象的大小存在不一致的看法。调试器在第一个模块环境中显示一个尺寸,而在第二个模块中打印出另一个更大的尺寸。这令人大为惊讶,因为这个对象是在一个头文件中声明的,且这个头文件被两个项目共享。通过更深入地打印并比较每个模块内的对象布局以及其数据成员的偏移量(对象的类型调试符号),明显看出编译器对对象的布局有所不同:一个模块中的所有数据成员都适当地对齐,而另一个模块并没有,而是将所有的数据成员打包在一起。数据对象以较小的尺寸被打包创建。

这种情况也得到了底层内存管理器分配的内存块大小的证实。当对象从打包对齐的模块被传入未打包布局的模块时,更新数据成员的操作就会覆盖内存并损坏附近的对象。图1-3以更简洁的方式描述了这个bug。一个T类型的结构体对象在模块A中被创建为打包格式,然后传到模块B,模块B却认为它是未打包的格式。模块B中灰色的数据成员data3覆盖了已分配的内存块。

你可能很想知道这种情况是如何发生的。结果表明,尽管对象在头文件中被正确地声明,但bug却来自另一个头文件,其中使用了以下编译指令:

#pragma pack(4)
...
#pragma pack()

开发工程师打算将编译指令中间的结构体打包为4字节边界。这个指令被微软的Visual Studio编译器正确解析。然而,当同一份代码被AIX的Visual Age C++编译器编译时,问题就出现了。该编译器有一个类似但略有不同的编译指令语法来结束打包作用域:

#pragma pack(4)
...
#pragma pack(nopack)

由于这个语法差异,Visual Age C++编译器只识别了打包的开始编译指令(第一行),却忽略了结束打包的编译指令(最后一行)。在程序员试图结束数据打包的地方,编译器仍然在继续打包数据结构。在模块A中,我们的受害对象在引入包含上述编译指令的头文件之后声明。在模块B中,问题头文件没有被引入,所以对象没有被打包。这就是不一致性的产生原因。

数据类型的调试符号准确地反映了编译器如何解析数据类型,生成的机器指令也根据这个解析结果来操作数据对象。具体来说,当创建新对象时,编译器会申请与结构体大小相等的内存块;数据成员的访问地址是通过从内存块开头的偏移量来计算的。

Core Dump:程序的“犯罪现场”

在Linux、Windows或Mac系统中,当程序异常终止时,操作系统会生成一份内存快照,即core dump。它记录了程序崩溃时的详细状态,就像留下了犯罪现场的线索。

高效调试的首步:日志与GDB

一般情况下,我们首先检查程序日志。如果日志无法提供有效信息,那么就需要借助GNU调试器(GDB)来加载core dump,查找程序崩溃的具体位置。这对于新手程序员而言,通常是寻找bug的“终点”。

迈向深度调试:Google Address Sanitizer (GAS)

然而,C/C++程序中常见的内存破坏类错误并非总是能通过GDB轻易定位。这时,Google Address Sanitizer(GAS)成为了一个强大的工具。它像在程序中安装了监控摄像头,帮助我们捕捉到内存破坏的瞬间,极大地简化了调试过程。

全面解析:Core Analyzer

当GDB和GAS无法完全解决问题时,Core Analyzer这款开源工具展现了其独特优势。它能遍历整个内存,识别出内存破坏的具体位置,甚至能够追踪多线程共享变量的问题,为深入调试提供了强大的支持。

在面对复杂和顽固的bug时,掌握高效的调试技巧至关重要。但关键在于,当常规方法无法解决问题时,我们应该如何行动呢?

这里推荐一本清华大学出版社的新书《高效C/C++调试》,书里深入探讨了C/C++软件调试的各种方法和技巧。作者Michael和卢宪廷凭借他们丰富的实战经验,不仅提供了对调试工具(GDB、Core Analyzer等等)的深入指南,还教导读者如何从宏观和微观视角审视问题,策略性地选择解决问题的最佳路径。这本书的核心在于,它不仅聚焦于C/C++,提出的多数策略具有广泛的通用性,适用于多种编程语言和平台。

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

本书的示例包含了大量的代码片段和实际案例。此外,本书还专门介绍了调试器插件和实用工具的开发。这些工具能够增强现有的调试器,拓宽我们的视野,要么提供新的角度审视问题,要么帮助我们更深入地研究问题。尽管本书主要探讨的是C/C++,但书中所介绍的方法和策略是通用的,独立于特定语言。

这是一套经过实战检验的、系统化的调试方法,旨在帮助具有编程背景的读者更高效地解决软件问题。无论您是编程新手还是经验丰富的开发者,《高效C/C++调试》都十分值得参考。

读者福利

在文末留言,小方将从留言的读者中选出 6 位读者各自赠送此书一本,留言内容必须和本文内容有关,截止时间 2023 年 2 月 6 日晚点 8 点整


【声明】内容源于网络
0
0
CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
内容 1260
粉丝 0
CppGuide 专注于高质量高性能C++开发,站点:cppguide.cn
总阅读582
粉丝0
内容1.3k