如何追踪SIGFPE/算术异常

10

我有一个C++应用程序,经过交叉编译适用于在ARM CortexA9处理器上运行的Linux系统。该应用程序出现了SIGFPE/算术异常导致崩溃的问题。一开始我认为这可能是由于gcc的-O3优化标志引起的,但后来我在调试模式下构建它,还是会崩溃。

我使用gdb调试应用程序,捕获了异常,但不幸的是触发异常的操作似乎也破坏了堆栈,所以我无法获取有关导致异常的代码位置的详细信息。最后我唯一能够得到的细节是触发异常的操作(从以下堆栈跟踪片段中获得):

    3 raise()  0x402720ac   
    2 __aeabi_uldivmod()  0x400bb0b8    
    1 __divsi3()  0x400b9880
__aeabi_uldivmod()是执行无符号长长除法和取模运算的函数,我尝试了粗暴的方法,搜索可能使用该操作的代码,但没有取得什么成果,因为这证明是一项艰巨的任务。此外,我尝试检查潜在的零除法,但是由于代码库太大,检查每个除法操作是一种繁琐而愚蠢的方法。因此,必须有更聪明的方法来找出问题所在。
在调试器无法提供帮助的情况下,是否有任何技术可以追踪这些异常的原因? 更新: 在处理十六进制数、转储内存并进行堆栈取证(感谢Crashworks)后,我在ARM编译器文档中发现了这个宝石(尽管我没有使用ARM Ltd.编译器):
整数除以零错误可以通过重新实现相应的C库帮助程序来捕获和标识。 当使用信号函数或重新实现 __rt_raise() 或 __aeabi_idiv0() 时, 除以零时的默认行为是调用 __aeabi_idiv0()。 否则,除法函数返回零。 __aeabi_idiv0()会引发SIGFPE,并附带一个参数DIVBYZERO。
因此,我在__aeabi_idiv0(_aeabi_ldiv0)处设置了断点,然后,我在被完全崩溃之前就获得了完整的堆栈跟踪。感谢大家给出的非常有启发性的答案! 免责声明:本“获胜”的答案是根据其对我的调试工作的建议的重要性而单独和主观地选择的,因为不止一个回答是具有信息量并且真正有帮助的。

2
我最近做了一个关于追踪各种疯狂的发布模式崩溃,包括堆栈损坏的演讲。我的幻灯片主要涵盖x86和PowerPC,但PPC与ARM相似,因此其中一种技术可能会有所帮助(除了堆栈帧布局的具体细节)。 幻灯片在这里:http://crashworks.org/gdc11/elan_ruskin_programming_forensic_debugging.pdf - Crashworks
5个回答

10
我的第一个建议是打开一个内存窗口,查看堆栈指针附近的区域,并深入挖掘,看看是否可以找到附近未被破坏的堆栈帧,这可能会给你一个线索,告诉你崩溃发生的地方。通常情况下,堆栈崩溃只会烧掉几个堆栈帧,所以如果你向上查看几百字节,就可以越过受损区域,对代码的位置有一个大致的感觉。你甚至可以朝着堆栈的下面看,因为死亡函数在死亡之前可能调用了其他函数,因此在内存中可能还有一个旧的堆栈帧指向当前的IP。
在评论中,我链接了一些演示文稿幻灯片,展示了如何在PowerPC上使用这种技术——大约在#73-86左右进行类似的堆栈崩溃案例研究。显然,ARM的堆栈帧布局会有所不同,但是一般原则仍然适用。

我正在尝试这个建议。也许正是我所需要的。漂亮而且信息量大的幻灯片,谢谢。 - celavek
我希望我能更具体一些,但我已经好几年没有使用ARM了。祝你好运! - Crashworks

3

使用Fedor Skrynnikov的基本思路,但使用编译器来辅助实现。

使用-pg编译您的代码。这将在每个函数中插入对mcountmcountleave()的调用。不要链接GCC分析库,而是提供自己的库。在mcountmcountleave()中唯一想做的就是保留当前堆栈的副本,因此只需将堆栈顶部的约128字节复制到固定缓冲区即可。由于堆栈和缓冲区始终在缓存中,因此成本相对较低。


1
你能详细说一下这个技术吗?谢谢 ;-) - Michal Gonda

2

您可以在可能导致异常的函数中实现特殊的守卫。Guard 是一个简单的类,在这个类的构造函数中,您将文件和行号 (_FILE_, _LINE_) 放入文件/数组/其他存储器中。主要条件是所有此类实例的存储器都应该相同(就像堆栈一样)。在析构函数中,您将删除这行。为了使其起作用,您需要将此守卫的创建放在每个函数的第一行,并且只在堆栈上创建它。当您离开当前块时,析构函数将被调用。因此,在出现异常时,您将从这个临时的调用堆栈中知道哪个函数引起了问题。 当然,您可以将这个类的创建放在调试条件下。


这可能是一个可行的解决方案,但在我的情况下并不完全适用,因为我有一个庞大的代码库,而且我不知道我的哪一部分代码会导致异常。修改每个类和函数以包含您建议的内容不是一个选项。作为替代,我可以直接在我的代码中使用回溯http://www.gnu.org/software/libc/manual/html_node/Backtraces.html,但它与您建议的方法具有相同的缺点。 - celavek

2

启用核心文件的生成,并使用调试器打开核心文件


已经尝试过了,结果相同。它无法确定导致崩溃的确切位置(或者至少给我一些查找方向)。 - celavek
在这种情况下,为您的代码编写单元测试。在执行除法之前使用assert检查值。我没有看到你还能做什么。 - BЈовић
一个明智的建议 :) 但是至少需要花费数天/数周的时间来覆盖潜在的单元测试位置。 - celavek

2

由于它使用raise()来引发异常,我认为signal()应该能够捕获它。这不是这种情况吗?

或者,您可以在__aeabi_uldivmod处设置条件断点,以在除数(r1)为0时中断。


信号确实被调试器捕获了。但是堆栈跟踪是我在问题中发布的那个,它并不完整,因为__divsi3()是来自libgcc的实际除法“/”运算符实现,所以它必须从代码中的某个地方“调用”,因此我的结论是异常实际上破坏了堆栈。我在__aeabi_uldivmod()处设置了一个有条件的断点,$r1==0,但它从未被触发,我仍然遇到崩溃。另一件事是,我没有该库的调试符号,因为我使用的工具链版本(CodeSourcery-Lite)没有提供它们。 - celavek

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