本文已参与「新人创作礼」活动,一起开启掘金创作之路。
Android 平台 Native Crash 系列文章:
Android 平台 Native Crash (一)捕获原理详解
Android 平台 Native Crash (二)问题分析与定位
一 Native Crash 简介
从 Android 系统全局来说,Crash 通常分为 App/Framework Crash,Native Crash,以及 Kernel Crash。
-
对于 App 层或者 Framework 层的 Crash(即 Java 层面 Crash),那么往往是通过抛出未捕获异常而导致的 Crash。
-
至于 Kernel Crash,很多情况是发生 Kernel panic,对于内核崩溃往往是驱动或者硬件出现故障。
-
Native Crash,即 C/C++层面的 Crash,是介于系统 framework 层与 Linux 层之间的一层,这是本文接下来要讲解的内容。 Native Crash 发生后,系统会在路径
/data/tombstones/
下产生导致程序 Crash 的文件 tombstone_xx,并且 Google 还在 NDK 包中为我们提供了一系列的调试工具,例如 addr2line、objdump、ndk-stack。
二 信号机制
在介绍 Tombstone 之前,先简单补充下 Linux 信号机制的知识。
2.1 程序奔溃
- 在 Unix-like 系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃机制让程序退出,如除零、段地址错误等。
- 异常发生时,CPU 通过异常中断的方式,触发异常处理流程。不同的处理器,有不同的异常中断类型和中断处理方式。
- linux 把这些中断处理,统一为信号量,可以注册信号量向量进行处理。
- 信号机制是进程之间相互传递消息的一种方法,信号全称为软中断信号。
2.2 信号机制
函数运行在用户态,当遇到系统调用、中断或是异常的情况时,程序会进入内核态。信号涉及到了这两种状态之间的转换。
(1) 信号的接收
接收信号的任务是由内核代理的,当内核接收到信号后,会将其放到对应进程的信号队列中,同时向进程发送一个中断,使其陷入内核态。注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号到来的。
(2) 信号的检测
进程陷入内核态后,有两种场景会对信号进行检测:
- 进程从内核态返回到用户态前进行信号检测
- 进程在内核态中,从睡眠状态被唤醒的时候进行信号检测 当发现有新信号时,便会进入下一步,信号的处理。
(3) 信号的处理
信号处理函数是运行在用户态的,调用处理函数前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器(eip)将其指向信号处理函数。
接下来进程返回到用户态中,执行相应的信号处理函数。
信号处理函数执行完成后,还需要返回内核态,检查是否还有其它信号未处理。如果所有信号都处理完成,就会将内核栈恢复(从用户栈的备份拷贝回来),同时恢复指令寄存器(eip)将其指向中断前的运行位置,最后回到用户态继续执行进程。
至此,一个完整的信号处理流程便结束了,如果同时有多个信号到达,上面的处理流程会在第 2 步和第 3 步骤间重复进行。
(4) 常见信号量类型
在 Linux 下,每个信号的名字都以字符 SIG 开头,每个信号和一个数字编码相对应,在头文件 signum.h 中,这些信号都被定义为正整数。信号名定义路径:/usr/include/i386-linux-gnu/bits/signum.h
信号量 | Value | 描述 | 例子 |
---|---|---|---|
SIGILL | 4 | 非法指令 | 损坏的可执行文件或者代码区损坏 |
SIGABRT | 6 | 进程发现错误或者调用了 abort() | 很多 C 的库函数,如果发现异常会调用 abort(),如 Strlen |
SIGBUS | 7 | 不存在的物理地址,硬件有误 | 更多的是因为硬件或者系统引起的 |
SIGFPE | 8 | 浮点数运算错误 | 如除 0 操作,余 0,整型溢出 |
SIGSEGV | 11 | 段地址错误 | 空指针,访问不存在的地址空间,访问内核区,写只读空间。栈溢出,数组越界,野指针 |
SIGPIPE | 13 | 管道错误,往没有 reader 得管道中写 | Linux 中的 socket,如果断掉了继续写。signal(SIGPIPE, SIG_IGN) |
三 Native Crash 处理流程
Native 程序通过 link 连接后,当发生 Native Crash 时,则 kernel 会发送相应的 signal,当进程捕获致命的 signal,通知 debuggerd 调用 ptrace 来获取有价值的信息(这是发生在 crash 前)。
- kernel 发送 signal 给 target 进程(包含 native 代码);
- target 进程通过 debuggerd_signal_handler,捕获 signal;
- 建立于 debuggerd 进程的 socket 通道;
- 将 action = DEBUGGER_ACTION_CRASH 的消息发送给 debuggerd 服务端;
- 阻塞等待 debuggerd 服务端的回应数据。
- debuggerd 作为守护进程,一直在等待 socket client 的连接,此时收到 action = DEBUGGER_ACTION_CRASH 的消息;
- 执行到 handle_request 时,通过 fork 创建子进程来执行各种 dump 相关操作;
- 新创建的进程,通过 socket 与 system_server 进程中的 NativeCrashListener 线程建立 socket 通道,并向其发送 native crash 信息;
- NativeCrashListener 线程通过创建新的名为“NativeCrashReport”的子线程来执行 AMS 的 handleApplicationCrashInner 方法。
四 Native Crash 问题定位
可参考文章: Android 平台 Native Crash 问题分析与定位