Android 平台 Native Crash (一)捕获原理详解

2,818 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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描述例子
SIGILL4非法指令损坏的可执行文件或者代码区损坏
SIGABRT6进程发现错误或者调用了 abort()很多 C 的库函数,如果发现异常会调用 abort(),如 Strlen
SIGBUS7不存在的物理地址,硬件有误更多的是因为硬件或者系统引起的
SIGFPE8浮点数运算错误如除 0 操作,余 0,整型溢出
SIGSEGV11段地址错误空指针,访问不存在的地址空间,访问内核区,写只读空间。栈溢出,数组越界,野指针
SIGPIPE13管道错误,往没有 reader 得管道中写Linux 中的 socket,如果断掉了继续写。signal(SIGPIPE, SIG_IGN)

三 Native Crash 处理流程

Native 程序通过 link 连接后,当发生 Native Crash 时,则 kernel 会发送相应的 signal,当进程捕获致命的 signal,通知 debuggerd 调用 ptrace 来获取有价值的信息(这是发生在 crash 前)。

  1. kernel 发送 signal 给 target 进程(包含 native 代码);
  2. target 进程通过 debuggerd_signal_handler,捕获 signal;
    • 建立于 debuggerd 进程的 socket 通道;
    • 将 action = DEBUGGER_ACTION_CRASH 的消息发送给 debuggerd 服务端;
    • 阻塞等待 debuggerd 服务端的回应数据。
  3. debuggerd 作为守护进程,一直在等待 socket client 的连接,此时收到 action = DEBUGGER_ACTION_CRASH 的消息;
  4. 执行到 handle_request 时,通过 fork 创建子进程来执行各种 dump 相关操作;
  5. 新创建的进程,通过 socket 与 system_server 进程中的 NativeCrashListener 线程建立 socket 通道,并向其发送 native crash 信息;
  6. NativeCrashListener 线程通过创建新的名为“NativeCrashReport”的子线程来执行 AMS 的 handleApplicationCrashInner 方法。

四 Native Crash 问题定位

可参考文章: Android 平台 Native Crash 问题分析与定位

参考文献

Android 平台 Native 代码的崩溃捕获机制及实现

Linux 信号列表

理解Native Crash处理流程