在Android的JNI中,如何捕获SIGSEGV(段错误)并获取堆栈跟踪?

97
我正在将一个项目迁移到新的Android Native开发工具包(JNI),如果发生 SIGSEGV (可能还有 SIGILL、SIGABRT、SIGFPE) 将显示一个漂亮的崩溃报告对话框,而不是当前发生的情况:进程立即死亡,而操作系统可能会试图重新启动它。(编辑:JVM / Dalvik VM 捕捉到信号并记录堆栈跟踪和其他有用信息; 我只想向用户提供将该信息发送给我的选项。)
情况是:大量C代码(我没有编写)在应用程序中执行了大部分工作(所有游戏逻辑),虽然它已经在许多其他平台上进行了充分测试,但很可能我在Android端口中输入垃圾数据,导致本地代码崩溃,因此我需要当前在Android日志中显示的崩溃转储(包括本机和Java的)。(我猜在非Android环境中它将是stderr)。我可以随意修改C和Java代码,尽管回调(无论是进入还是离开JNI)数量约为40,但显然,差异小的奖励点更高。
我听说过J2SE中的信号链接库libjsig.so,如果我可以在Android上安全地安装这样的信号处理程序,那么我的问题就解决了,但我没有找到适用于Android / Dalvik的此类库。

如果您可以通过包装脚本启动Java VM,则可以检查应用程序是否异常退出,并进行错误报告。这将允许您干净地捕获所有类型的异常退出,无论是SIGSEGV、SIGKILL还是其他什么。但是,我认为这在原始的Android应用程序中不可能实现,因此将其发布为评论(从答案转换而来)。 - sleske
另请参见:无法使用Valgrind运行Java Android程序,了解如何使用包装脚本(在adb shell中)启动Android应用程序。 - sleske
1
答案需要更新。已接受答案中提供的源代码由于调用了非异步信号安全函数而导致未定义行为。请参见此处:https://dev59.com/3JLea4cB1Zd3GeqP35F9#34553070 - user1506104
4个回答

84
编辑:从Jelly Bean版本开始,您无法获取堆栈跟踪信息,因为READ_LOGS已被删除。:(
实际上,我成功地使用信号处理程序而不进行任何奇特的操作,并发布了使用它的代码,您可以在Github上查看(编辑:链接到历史版本;此后我已删除崩溃处理程序)。这是如何实现的:
  1. 使用 sigaction() 捕捉信号并存储旧处理程序。(android.c:570)
  2. 一段时间后,发生segfault。
  3. 在信号处理程序中,最后一次调用JNI,然后调用旧的处理程序。(android.c:528
  4. 在该JNI调用中,记录任何有用的调试信息,并调用被标记为需要在自己的进程中运行的活动的startActivity()。(SGTPuzzles.java:962AndroidManifest.xml:28
  5. 当你从Java回来并调用旧的处理程序时,Android框架将连接到debuggerd为您记录一个好的本机跟踪,然后进程将死亡。(debugger.cdebuggerd.c
  6. 同时,您的崩溃处理活动正在启动。实际上,您应该将PID传递给它,以便它可以等待第五步完成;我没有这样做。在这里,您向用户道歉并询问是否可以发送日志。如果可以,收集logcat -d -v threadtime的输出,并使用填充了收件人、主题和正文的ACTION_SEND启动。用户将不得不按Send键。(CrashHandler.javaSGTPuzzles.java:462strings.xml:41
  7. 注意logcat失败或花费超过几秒钟的情况。我遇到过一种设备,T-Mobile Pulse / Huawei U8220,在该设备上,logcat立即进入了T(跟踪)状态并挂起了。(CrashHandler.java:70strings.xml:51
在非Android情况下,一些内容会有所不同。您需要收集自己的本地跟踪信息,请参见这个问题,具体取决于您使用的libc类型。您需要以适当的方式处理转储该跟踪信息,启动单独的崩溃处理器进程,并使用某些适合您平台的方法发送电子邮件,但我想一般方法仍然有效。

2
理想情况下,您应该检查崩溃是否发生在您的库中。如果它发生在其他地方(比如在VM内部),您从信号处理程序中进行的JNI调用可能会使事情变得非常混乱。虽然您已经处于崩溃状态,但这可能会使VM崩溃的诊断更加困难(或者导致一个奇怪的VM崩溃,最终在Android错误报告中找到并让所有人感到困惑)。 - fadden
3
在服务中启动一个新进程的活动还需要添加以下代码:newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - Graeme
如果你不使用“yeah activity new task”,新的活动就不会开始,因此它非常重要。 - Gelldur
1
这个解决方案在Jelly Bean下仍然有效吗?第6步不会无法记录任何debuggerd输出吗? - Josh
我创建了一个小库,使用这些步骤来启用ACRA崩溃报告JNI代码。你的帖子非常有帮助,非常感谢! - Salomon BRYS
显示剩余4条评论

16

我有点晚了,但是我有同样的需求,因此我开发了一个小库来解决这个问题,通过捕获JNI代码中常见的崩溃(如SEGVSIBGUS等),并将它们替换为常规的java.lang.Error异常。如果客户端运行在Android >= 4.1.1上,堆栈跟踪会嵌入崩溃的已解析回溯(一个伪跟踪,包含完整的本地堆栈跟踪)。你无法从恶性崩溃中恢复(例如,如果你破坏了分配器),但至少它应该能让你从大多数崩溃中恢复过来。(请报告成功和失败,代码是全新的)

更多信息请查看:https://github.com/xroche/coffeecatch(代码使用BSD 2-Clause license


6

就此而言,Google Breakpad 在 Android 上运行良好。我进行了移植工作,并将其作为 Firefox 移动版的一部分进行了发布。它需要一些设置,因为它不能在客户端提供堆栈跟踪,但会将原始堆栈内存发送到服务器端进行堆栈遍历(这样您就不必将调试符号与应用程序一起发布)。


1
考虑到完全缺失的文档,配置 Breakpad 几乎是不可能的。 - shader
这真的不难,而且项目维基上有大量文档。事实上,对于Android来说,现在有一个NDK构建Makefile,使用起来应该非常容易: http://code.google.com/p/google-breakpad/source/browse/trunk/README.ANDROID - Ted Mielczarek
你还需要编译一个预处理 Android 调试符号文件的模块,而这只能在 Linux 上编译。如果你在 Mac 上编译,那么只会构建 Mac/iOS 的 dSym 预处理器。 - shader

5

根据我的有限经验(非Android),JNI代码中的SIGSEGV通常会在控制权返回到Java代码之前使JVM崩溃。我模糊地记得听说过一些非Sun JVM可以让你捕获SIGSEGV,但是据我所知,你不能指望能够这样做。

您可以尝试在C中捕获它们(请参见sigaction(2)),尽管在SIGSEGV(或SIGFPE或SIGILL)处理程序之后,进程的持续行为被正式定义为未定义,因此您几乎无法采取任何措施。


行为在“忽略由kill(2)或raise(3)未生成的SIGFPE,SIGILL或SIGSEGV信号”之后是未定义的,但在捕获此类信号期间不一定如此。当前计划是尝试一个C信号处理程序,该处理程序回调到Java,并以某种方式终止线程而不终止进程。这可能是可能的,也可能不可能。 :-) - Chris Boyle
1
C回溯指令:http://stackoverflow.com/questions/76822/how-to-generate-a-stacktrace-when-my-c-app-crashes-using-gcc-compiler/77281#77281 - Chris Boyle
1
除了我不能使用backtrace()之外,因为Android不使用glibc,而是使用Bionic。 :-( 可能需要涉及“unwind.h”中的_Unwind_Backtrace。 - Chris Boyle

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