解决随机崩溃问题

38

我在我的C++应用程序中遇到了随机崩溃的问题,它可能一个月不会崩溃,但有时会在启动时立即崩溃,有时则在数小时后崩溃(或根本不崩溃),甚至有时在一小时内会崩溃10次。

我在GNU/Linux上使用GCC,在Windows上使用MingW,因此无法使用Visual Studio JIT Debug...

我不知道该如何继续下去,随意查找代码是行不通的,因为代码很庞大(而且很大一部分不是我写的),还包含一些旧的东西,而且我也不知道如何重现这个错误。

编辑:很多人提到了...我如何生成核心转储、小转储或其他转储?这是我第一次需要进行事后调试。

编辑2:实际上,DrMingw捕获了一个调用堆栈,没有内存信息...不幸的是,调用堆栈并没有对我有太大帮助,因为最后突然进入了一些没有调试信息的库(或其他内容),导致只显示了一些十六进制数字...所以我仍然需要某种有效的转储来提供更多信息(特别是关于造成“访问冲突”错误的位置中存在的内容...具体来说就是,在内存中的什么位置出现了问题)。

此外,我的应用程序使用Lua和Luabind,也许错误是由.lua脚本引起的,但我不知道如何对其进行调试。


11
这是一个多线程应用程序吗? - Naveen
2
我怀疑这些崩溃不是随机的。 - Tom Gullen
3
原文:The cause is not likely to be random (hanging pointers, double delete, memory corruption) but the symptoms will be random (or more specifically non-deterministic)翻译:这个问题的原因可能不是随机的(例如悬挂指针、双重删除、内存损坏),但症状会是随机的(更准确地说是非确定性的)。 - the_mandrill
你好,我还没有解决这个问题...但我仍然计划着去解决它。 - speeder
@speeder:如果有答案对您有帮助,我可以建议您接受它。 - Mitch Wheat
显示剩余6条评论
17个回答

29

试用Valgrind(它是免费的、开源的):

Valgrind包含六个生产质量的工具:内存错误检测器、两个线程错误检测器、缓存和分支预测分析器、生成调用图的缓存分析器以及堆分析器。还包括两个实验性的工具:堆/栈/全局数组溢出检测器和SimPoint基本块向量生成器。它能运行在以下平台上:X86/Linux、AMD64/Linux、PPC32/Linux、PPC64/Linux和X86/Darwin(Mac OS X)。

Valgrind常见问题解答

该软件包中的Memcheck部分可能是开始使用的地方:

Memcheck是一种内存错误检测器。它可以检测以下C和C++程序中常见的问题。

  • 访问不应访问的内存,例如超越和低于堆块界限,超越栈的顶部以及访问已被释放的内存。

  • 使用未定义的值,即未初始化或来源于其他未定义值的值。

  • 错误释放堆内存,如重复释放堆块或使用malloc/new/new[]与free/delete/delete[]不匹配。

  • 在memcpy和相关函数中重叠src和dst指针。

  • 内存泄漏。


2
+1. Valgrind经常可以毫不费力地为您提供bug所在的行号,就像魔法一样。 - Jason Orendorff
我也会点赞。最近才开始使用它,我发现它几乎是必不可少的。 - paxdiablo
1
Valgrind很好用,但不幸的是,在Windows/MINGW上无法捕获错误,因为它在那里不存在。 可能的替代品:
  • https://dev59.com/JHRC5IYBdhLWcg3wFdFx
- Nordic Mainframe
@Luther Blissett:海报也在Linux上运行。 - Mitch Wheat
2
@Mitch:我的评论并没有否认这一点。 - Nordic Mainframe
4
我的问题正是因为我不能一直使用例如Valgrind之类的工具...Valgrind会让程序变得非常慢,非常非常慢。而且可能需要几个小时或几个月才能崩溃...我无法在整个月中都使用Valgrind运行程序。 - speeder

15

首先,你很幸运,在短时间内多次出现进程崩溃。这应该使得处理过程更加容易。

以下是处理方法:

  • 获取崩溃转储
  • 确定一组可能存在问题的函数
  • 加强状态检查
  • 重复上述步骤

获取崩溃转储

首先,你真的需要获取崩溃转储。

如果程序崩溃时没有生成转储文件,那么请编写一个可靠地生成崩溃转储的测试用例。

重新编译带有调试符号的二进制文件或确保可以使用调试符号分析崩溃转储。

确定可能存在问题的函数

假设你已经获得了崩溃转储,请在gdb或你最喜欢的调试器中查看它,并记得显示所有线程!可能导致错误的线程不一定是你在gdb中看到的线程。

查看gdb报告的二进制文件崩溃位置,确定一些可能导致问题的函数。

查看多个崩溃并隔离出所有崩溃中常见的活动代码段,这样可节省时间。

加强状态检查

通常情况下,崩溃发生是因为某些不一致的状态。要继续进行,最好的方法通常是加强状态要求。具体步骤如下:

对于你认为可能导致问题的每个函数,请记录输入或对象在进入该函数时必须具有的合法状态。(同样适用于从函数退出时必须具有的合法状态,但这并不太重要)。

如果函数包含循环,请记录其每个循环迭代开始时需要具有的合法状态。

为所有此类合法状态表达式添加asserts断言。

重复

然后重复这个过程。如果它仍然在你的断言之外崩溃,请进一步加强断言。在某个时刻,处理将在断言上崩溃而不是因为某些随机崩溃。此时,您可以集中精力尝试弄清楚程序是如何从函数入口处的合法状态到达断言发生的非法状态的。

如果将断言与详细日志记录配对,则应更容易跟踪程序的操作。


14
如果其他方法都失败了(特别是如果在调试器下性能不可接受),那就进行广泛的日志记录。从入口点开始,应用程序是否是事务性的?每次事务进来时都要记录日志。记录关键对象的所有构造函数调用。由于崩溃非常间歇性,所以记录可能不会每天被调用的所有函数调用。
这样至少可以缩小崩溃出现的可能范围。

1
我曾经也这样做,但我注意到通常情况下,记录日志会导致程序进行I/O操作,这有时会防止一些错误/竞争条件的发生。我认为当您遇到以确定的方式出现的错误时,记录日志是一种更有效的技术。 - ereOn
3
没错,你得爱上那些海森堡BUG。 - paxdiablo
讨厌海森堡/薛定谔虫。摆脱它们以使行为可预测(可能导致崩溃,但是有已知的原因)非常重要,因为这几乎总是很快导致完全正常的代码... - Donal Fellows

8

在调试器下启动程序(我确定GCC和MingW会有一个调试器),等待它在调试器下崩溃。在崩溃点,您将能够看到哪个特定操作失败了,查看汇编代码、寄存器、内存状态 - 这通常有助于找到问题的原因。


1
我做不到,调试器下的性能太慢了,根本无法使程序有用,并且它可能需要很长时间才会崩溃。因此,这将要求我一直使用GDB,在这个项目中这是完全不合理的。 - speeder
@speeder:就我个人而言,在调试器下运行时,我从未看到过任何速度差异。我的意思不是逐步执行,而是只需运行并让它一直运行,直到崩溃。 - sharptooth
我通常也不这样做,但我的程序调试版本二进制文件有140Mb,它还加载了超过100Mb的数据(其中一部分是动态生成的),当我的程序与GDB一起加载时,GDB本身需要200Mb以上的内存...这导致操作系统疯狂地使用页面文件,而且我的内存并不是最好的(实际上它相当老旧,总共只有2GB...) - speeder
@speeder:你可以这样做:使用.pdb和优化编译程序。这样它不会像完整的调试版本那样臃肿,当它在调试器下崩溃时,你仍然能够看到调用堆栈。 - sharptooth
没有什么能阻止你调试真正的发布模式构建。你只需要一个从Debug构建中生成的PDB文件,你可以从那里窃取...但最好是设置你的Release构建以生成Debug信息。请注意,优化将破坏你的调试体验,变量将会丢失等等,但是你仍然可以通过这种方式获得很多帮助。 - Петър Петров

8

在我工作的地方,程序崩溃通常会生成一个 core dump 文件,可以在 windbg 中进行加载。

然后我们会有程序崩溃时内存的图像。虽然你并不能做太多事情,但至少它能给你最后一个调用栈。一旦你知道了导致崩溃的函数,你就可以追踪问题,或者至少可以将问题减少到更可重现的测试用例。


你可以给些细节吗?我最新了解到的是,mingw-gcc二进制文件无法生成核心转储文件,而且windbg对于mingw二进制文件的支持很少,因为它们使用stabs调试格式,而windbg不理解。 - Nordic Mainframe
@Luther Blissett,不幸的是,核心转储文件似乎是由系统生成的(我在一家非常大的公司工作,我不是实际设置这个的团队的一部分)。然而,我确信我的测试二进制文件(使用mingw创建)在崩溃时会产生“核心转储”,我非常怀疑负责的团队添加了这种特殊情况。 - ereOn
2
我相信这些被称为(微软术语)“Minidumps”。Windbg有一个设置可以“事后”读取它们,并且可以揭示“东西”。 - JustBoo

7

听起来你的程序可能受到了内存损坏的影响。如前所述,Linux上最好的选择可能是使用valgrind。但以下还有两个选项:

  • 首先使用调试malloc。几乎所有的C库都提供了一个调试malloc实现,它可以初始化内存(普通的malloc会在内存中保留“旧”的内容),检查分配块的边界是否损坏等等。如果这还不够,也有很多第三方实现可供选择。

  • 你可能想看一下VMWare Workstation。我没有这样设置过,但从他们的营销材料中得知,他们支持一种非常有趣的调试方式:在“记录”虚拟机中运行调试程序。当内存损坏发生时,在受损地址处设置一个内存断点,然后在VM中回溯到恰好覆盖那块内存的那一刻。请参阅此PDF文档,了解如何在Linux/gdb中设置重放调试。我相信Workstation 7有15或30天的试用期,这可能足以从你的代码中找出这些错误。


6
这类错误总是很棘手 - 除非您能重现错误,否则您唯一的选择就是对应用程序进行更改以记录额外信息,然后等待错误再次在实际中发生。
有一个名为 Process Dumper 的优秀工具,可用于获取经历异常或意外退出的进程的崩溃转储 - 您可以要求用户安装并为您的应用程序配置规则。
或者,如果您不想要求用户安装其他应用程序,则可以让您的应用程序监视异常并通过调用 MiniDumpWriteDump 自己创建转储。
另一个选择是改进日志记录,但找出要记录的信息(而不仅仅是记录所有内容)可能很棘手,因此可能需要多次迭代 崩溃 - 更改日志记录 才能找到问题。
正如我所说,这类错误总是很棘手 - 在我的经验中,通常需要花费数小时查看日志和崩溃转储,直到突然出现那个顿悟时刻,一切都变得清晰明了 - 关键在于收集正确的信息。

5
您已经听说过如何在Linux下处理这个问题:检查核心转储并在valgrind下运行代码。因此,您的第一步可能是在Linux下找到错误,然后检查它们是否在mingw下消失。由于没有人在这里提到mudflap,我会这样做:如果您的Linux发行版提供它,请使用mudflap。mudflap通过跟踪指针实际允许指向的信息来帮助您捕获指针误用和缓冲区溢出:

对于Windows:有一个适用于mingw的JIT调试器,称为DrMingw:


哦哦...那东西居然有效!有一个特定的崩溃我知道如何引发(但不知道如何修复),我用它来测试DrMingw。可惜它没有提供关于内存的任何信息,只有关于调用栈的信息... :( - speeder

4
在Linux下使用valgrind运行应用程序,以查找内存错误。随机崩溃通常是由于破坏内存引起的。
使用valgrind的memcheck工具修复每个发现的错误,然后希望崩溃会消失。
如果整个程序在valgrind下运行时间太长,则将功能拆分为单元测试,并在valgrind下运行这些测试,希望您能找到导致问题的内存错误。
如果还没有解决,那么请确保启用了coredumps(ulimit -a),然后当它崩溃时,您将能够通过gdb找到崩溃位置。

Valgrind 最终可以在 Windows 上运行了吗?我已经寻找多年了。 - ereOn
@ereOn:不幸的是,它不支持,但OP也在使用Linux,所以这对他来说应该是一个选项。目前只有Linux和OS X得到了真正的支持,尽管FreeBSD和NetBSD有非官方端口。请参见http://www.valgrind.org/info/platforms.html。 - Nicholas Knight
Valgrind对于这个项目来说毫无用处,因为它运行得太慢了,直到Valgrind能够捕获到某些错误之前,我可能已经死于老年... - speeder
不幸的是,valgrind 或其他一些内存检查器是我能建议的最好的东西了。否则,你几乎必须重写整个应用程序。 - Douglas Leeder

3

听起来像是类似于竞态条件的棘手问题。

我建议您创建一个调试版本并使用它。您还应确保程序崩溃时创建了核心转储。

下次程序崩溃时,您可以在核心转储上启动gdb,并查看问题所在。这可能是一个连续的故障,但这应该能帮助您入门。


这可能是竞态条件或其他导致未定义行为的情况。我们没有足够的信息进行有根据的猜测。 - ereOn
是的,这就是为什么核心转储应该有用的原因。他说他不想随机查看源代码,我也同意。核心转储应该让他入门。 - fhd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接