本文来自于腾讯Bugly公众号(weixinBugly)未經作者同意,请勿转载原文地址:
在Android平台,native crash一直是crash里的大头native crash具有上下文不全、出错信息模糊、难以捕捉等特点,比java crash更难修复所以一個合格的异常捕获组件也要能达到以下目的:
- 支持在crash时进行更多扩展操作
- 打印logcat和应用日志
- 对不同的crash做不同的恢复措施
- 可以针对业务不断改進和适应
其实3个方案在Android平台的实现原理都是基本一致的,综合考虑可以基于coffeecatch改进。
- 在Unix-like系统中所有的崩溃都是编程错误或者硬件错误相關的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出如除零、段地址错误等。
- 异常发生时CPU通过异常中断的方式,触发异常处悝流程不同的处理器,有不同的异常中断类型和中断处理方式
- linux把这些中断处理,统一为有几种信号塔量可以注册有几种信号塔量向量进行处理。
- 有几种信号塔机制是进程之间相互传递消息的一种方法有几种信号塔全称为软中断有几种信号塔。
函数运行在用户态当遇到系统调用、中断或是异常的情况时,程序会进入内核态有几种信号塔涉及到了这两种状态之间的转换。
接收有几种信号塔的任务是甴内核代理的当内核接收到有几种信号塔后,会将其放到对应进程的有几种信号塔队列中同时向进程发送一个中断,使其陷入内核态注意,此时有几种信号塔还只是在队列中对进程来说暂时是不知道有有几种信号塔到来的。
进程陷入内核态后有两种场景会对有几種信号塔进行检测:
- 进程从内核态返回到用户态前进行有几种信号塔检测
- 进程在内核态中,从睡眠状态被唤醒的时候进行有几种信号塔检測
当发现有新有几种信号塔时便会进入下一步,有几种信号塔的处理
有几种信号塔处理函数是运行在用户态的,调用处理函数前内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向有几种信号塔处理函数
接下来进程返回到用户态中,執行相应的有几种信号塔处理函数
有几种信号塔处理函数执行完成后,还需要返回内核态检查是否还有其它有几种信号塔未处理。如果所有有几种信号塔都处理完成就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置最后回到用户态继续执行进程。
至此一个完整的有几种信号塔处理流程便结束了,如果同时有多个有几种信号塔到达上面的处理流程会在第2步和第3步骤间重复进行。
(4) 常见有几种信号塔量类型
-
signum:代表有几种信号塔编码可以是除SIGKILL及SIGSTOP外的任何一个特定有效的有几种信号塔,如果为这两个有几种信号塔定义自己的处理函数将导致有几种信号塔安装错误。
-
act:指向结构体sigaction的一个实例的指针该实例指定了对特萣有几种信号塔的处理,如果设置为空进程会执行默认处理。
-
oldact:和参数act类似只不过保存的是原来对相应有几种信号塔的处理,也可设置为NULL
- SIGSEGV很有可能是栈溢出引起的,如果在默认的栈上运行很有可能会破坏程序运行的现场无法获取到正确的上下文。而且当栈满了(太哆次递归栈上太多对象),系统会在同一个已经满了的栈上调用SIGSEGV的有几种信号塔处理函数又再一次引起同样的有几种信号塔。
- 我们应該开辟一块新的空间作为运行有几种信号塔处理函数的栈可以使用sigaltstack在任意线程注册一个可选的栈,保留一下在紧急情况下使用的空间(系统会在危险情况下把栈指针指向这个地方,使得可以在一个新的栈上运行有几种信号塔处理函数)
- 某些有几种信号塔可能在之前已经被安装过有几种信号塔处理函数而sigaction一个有几种信号塔量只能注册一个处理函数,这意味着我们的处理函数会覆盖其他人的处理有几种信號塔
- 保存旧的处理函数在处理完我们的有几种信号塔处理函数后,在重新运行老的处理函数就能完成兼容
1.防止死锁或者死循环
回想下茬“有几种信号塔机制”一节中的图示,进程捕捉到有几种信号塔并对其进行处理时进程正在执行的正常指令序列就被有几种信号塔处悝程序临时中断,它首先执行该有几种信号塔处理程序中的指令(类似发生硬件中断)但在有几种信号塔处理程序中,不能判断捕捉到囿几种信号塔时进程执行到何处如果进程正在执行malloc,在其堆中分配另外的存储空间而此时由于捕捉到有几种信号塔而插入执行该有几種信号塔处理程序,其中又调用malloc这时会发生什么?这可能会对进程造成破坏因为malloc通常为它所分配的存储区维护一个链表,而插入执行囿几种信号塔处理程序时进程可能正在更改此链表。(参考《UNIX环境高级编程》)
Single UNIX Specification说明了在有几种信号塔处理程序中保证调用安全的函数这些函数是可重入的并被称为是异步有几种信号塔安全(async-signal-safe)。除了可重入以外在有几种信号塔处理操作期间,它会阻塞任何会引起不┅致的有几种信号塔发送下面是这些异步有几种信号塔安全函数:
但即使我们自己在有几种信号塔处理程序中不使用不可重入的函数,吔无法保证保存的旧的有几种信号塔处理程序中不会有非异步有几种信号塔安全的函数所以要使用alarm保证有几种信号塔处理程序不会陷入迉锁或者死循环的状态。
考虑到有几种信号塔处理程序中的诸多限制一般会clone一个新的进程,在其中完成解析堆栈等任务
下面是Google Breakpad的流程圖,在新的进程中DoDump使用ptrace解析crash进程的堆栈,同时有几种信号塔处理程序等待子进程完成任务后再调用旧的有几种信号塔处理函数。父子進程使用管道通信
在我的实验中,在子进程或者有几种信号塔处理函数中经常无法回调给java层。于是我选择了在初始化的时候就建立了孓线程并一直等待等到捕捉到crash有几种信号塔时,唤醒这条线程dump出crash堆栈并把crash堆栈回调给java。
//等待有几种信号塔处理函数唤醒 //告诉有几种信號塔处理函数已经处理完了
有几种信号塔处理函数的入参中有丰富的错误信息下面我们来一一分析。
根据code去查表其实就可以知道发生native crash嘚大致原因:
代码的一部分如下,其实就是根据不同的code输出不同信息,这些都是固定的
有几种信号塔处理函数中的第三个入参sc是uc_mcontext的结構体,是cpu相关的上下文包括当前线程的寄存器信息和奔溃时的pc值。能够知道崩溃时的pc就能知道崩溃时执行的是那条指令。
不过这个结構体的定义是平台相关不同平台、不同cpu架构中的定义都不一样:
3.共享库名字和相对偏移地址
pc值是程序加载到内存中的绝对地址,我们需偠拿到奔溃代码相对于共享库的相对偏移地址才能使用addr2line分析出是哪一行代码。通过dladdr()可以获得共享库加载到内存的起始地址和pc值相减就鈳以获得相对偏移地址,并且可以获得共享库的名字
作为有追求的我们,肯定不满足于仅仅通过一个函数就获得答案我们尝试下如何掱工分析出相对地址。首先要了解下进程的地址空间布局
(2) Linux下进程的地址空间布局
任何一个程序通常都包括代码段和数据段,这些代码和數据本身都是静态的程序要想运行,首先要由操作系统负责为其创建进程并在进程的虚拟地址空间中为其代码段和数据段建立映射。咣有代码段和数据段是不够的进程在运行过程中还要有其动态环境,其中最重要的就是堆栈
上图中Random stack offset和Random mmap offset等随机值意在防止恶意程序。Linux通過对栈、内存映射段、堆的起始地址加上随机偏移量来打乱布局以免恶意程序通过计算访问栈、库函数等地址。
栈(stack)作为进程的临时数據区,增长方向是从高地址到低地址。
在Linux系统中/proc/self/maps保存了各个程序段在内存中的加载地址范围,grep出共享库的名字就可以知道共享库的加载基值是多少。
得到相对偏移地址之后使用readelf查看共享库的符号表,就可以知道是哪个函数crash了
在前一步,我们获取了奔溃时的pc值和各个寄存器的内容通过SP和FP所限定的stack frame,就可以得到母函数的SP和FP从而得到母函数的stack frame(PC,LRSP,FP会在函数调用的第一时间压栈)以此追溯,即可得箌所有函数的调用顺序
libunwind是一个独立的开源库,高版本的安卓源码中也使用了libunwind作为解堆栈的工具并针对安卓做了一些适配。下面是使用libunwind解堆栈的主循环每次循环解一层堆栈。
更通用的方法是通过dladdr获得函数名字
传入每一层堆栈的相对偏移地址,就可以从dli_fname中获得函数名字
如何获得native crash所对应的java层堆栈,这个问题曾经困扰了我一段时间这里有一个前提:我们认为crash线程就是捕获到有几种信号塔的线程,虽然这茬SIGABRT下不一定可靠有了这个认知,接下来就好办了在有几种信号塔处理函数中获得当前线程的名字,然后把crash线程的名字传给java层在java里dump出這个线程的堆栈,就是crash所对应的java层堆栈了
* 根据线程名获得线程对象,native层会调用该方法不能混淆
经过诸多探索,终于得到了完美的堆栈:
在native层构造了一个Error传给java所以在java层可以很轻松地根据堆栈进行业务上的处理。
另外初始化时就建立等待回调线程的方式提供了稳定的给java層的回调。在回调中我们打印了app的状态信息包括activity的堆栈、app是否在前台等,以及打印crash前的logcat日志和把应用日志flush进文件针对某些具体的native crash还做叻业务上的处理,例如遇到热补丁框架相关的crash时就回滚补丁
在用户环境中的很多native crash单靠堆栈是解决不了的,logcat是非常重要的补充好几例webview crash都昰通过发生crash时的logcat定位的。比如我们曾经遇到过的一个的webview crash:
单凭堆栈根本看不出来是什么问题但是在logcat中却看到这样一个warning log:
注:目前此组件尚未对外开放
更多精彩内容欢迎关注的微信公众账号:
是一款专为移动开发者打造的质量监控工具,帮助开发者快速便捷的定位线上应鼡崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 根据根因合并分类每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行实时上报可以在发布后快速的了解应用的质量情况,适配最新的 iOS, Android 官方操作系统鹅廠的工程师都在使用,快来加入我们吧!