Android 两种崩溃
- Java崩溃
- 在Java代码中出现了未捕获的异常,导致程序异常退出
- Native崩溃
- 在Native代码中访问非法地址或地址对齐出现问题或者程序主动 abort 异常退出 (默认的程序结束函数,这种方式可能会或可能不会以刷新与关闭打开的文件或删除临时文件)
Native崩溃捕获机制以及实现
- 首先native crash因为具有上下文不全,出错信息模糊,难以捕捉的特点,所以捕获难比Java crash更难修复
- 所以捕获组件需要达到以下特点
- 打印logcat和应用日志
- 上报crash次数
- 对不同的crash做不同的恢复措施
- 可以针对业务不断改进和适应
- 所以捕获组件需要达到以下特点
信号机制
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CkfcxK6b-1570331077566)(en-resource://database/8399:1)]
- 在Unix-like系统中,所有的崩溃都是编程错误或者硬件错误相关的,系统遇到不可恢复的错误时会触发崩溃时机让程序退出
- 异常发生时,CPU通过异常中断的方式触发异常处理流程,不同的处理器,有不同的异常中断处理方式
- 在Linux系统中,这些中断处理统称为信号量,可以注册信号量向量进行处理
- 信号(软中断信号)机制是进程之间信息传递的一种方法
- 当函数运行在用户态时,当遇到系统调用、中断或是异常的情况时,程序会进入内核态,信号的处理就在这两种状态之间相互转换
信号类型
Signal | Description |
---|---|
SIGABRT | 由调用abort函数产生,进程非正常退出 |
SIGALRM | 用alarm函数设置的timer超时或setitimer函数设置的interval timer超时 |
SIGBUS | 某种特定的硬件异常,通常由内存访问引起 |
SIGCANCEL | 由Solaris Thread Library内部使用,通常不会使用 |
SIGCHLD | 进程Terminate或Stop的时候,SIGCHLD会发送给它的父进程。缺省情况下该Signal会被忽略 |
SIGCONT | 当被stop的进程恢复运行的时候,自动发送 |
SIGEMT | 和实现相关的硬件异常 |
SIGFPE | 数学相关的异常,如被0除,浮点溢出,等等 |
SIGFREEZE | Solaris专用,Hiberate或者Suspended时候发送 |
SIGHUP | 发送给具有Terminal的Controlling Process,当terminal被disconnect时候发送 |
SIGILL | 非法指令异常 |
SIGINFO | BSD signal。由Status Key产生,通常是CTRL+T。发送给所有Foreground Group的进程 |
SIGINT | 由Interrupt Key产生,通常是CTRL+C或者DELETE。发送给所有ForeGround Group的进程 |
SIGIO | 异步IO事件 |
SIGIOT | 实现相关的硬件异常,一般对应SIGABRT |
SIGKILL | 无法处理和忽略。中止某个进程 |
SIGLWP | 由Solaris Thread Libray内部使用 |
SIGPIPE | 在reader中止之后写Pipe的时候发送 |
SIGPOLL | 当某个事件发送给Pollable Device的时候发送 |
SIGPROF | Setitimer指定的Profiling Interval Timer所产生 |
SIGPWR | 和系统相关。和UPS相关。 |
SIGQUIT | 输入Quit Key的时候(CTRL+\)发送给所有Foreground Group的进程 |
SIGSEGV | 非法内存访问 |
SIGSTKFLT | Linux专用,数学协处理器的栈异常 |
SIGSTOP | 中止进程。无法处理和忽略。 |
SIGSYS | 非法系统调用 |
SIGTERM | 请求中止进程,kill命令缺省发送 |
SIGTHAW | Solaris专用,从Suspend恢复时候发送 |
SIGTRAP | 实现相关的硬件异常。一般是调试异常 |
SIGTSTP | Suspend Key,一般是Ctrl+Z。发送给所有Foreground Group的进程 |
SIGTTIN | 当Background Group的进程尝试读取Terminal的时候发送 |
SIGTTOU | 当Background Group的进程尝试写Terminal的时候发送 |
SIGURG | 当out-of-band data接收的时候可能发送 |
SIGUSR1 | 用户自定义signal 1 |
SIGUSR2 | 用户自定义signal 2 |
SIGVTALRM | setitimer函数设置的Virtual Interval Timer超时的时候 |
SIGWAITING | Solaris Thread Library内部实现专用 |
SIGWINCH | 当Terminal的窗口大小改变的时候,发送给Foreground Group的所有进程 |
SIGXCPU | 当CPU时间限制超时的时候 |
SIGXFSZ | 进程超过文件大小限制 |
SIGXRES | Solaris专用,进程超过资源限制的时候发送 |
信号处理流程
-
信号的接收
- 接收信号是由内核代理的,当内核接收到信号后,会将其放到对应的进程信号队列中,同时向进程中发送一个中断,使其陷入内核态,注意,此时信号还只是在队列中,对进程来说暂时是不知道有信号的到来
-
信号的检测
- 进程陷入内核态后,有两种场景会对信号进行检测:
- 进程从内核态返回到用户态前进行信号检测
- 进程在内核态中,从睡眠状态到被唤醒时进行信号检测
- 进程陷入内核态后,有两种场景会对信号进行检测:
-
信号的处理
- 运行在用户态,调用函数处理前,内核会将当前内核栈的内容备份拷贝到用户栈上,并且修改指令寄存器将其指向信号处理函数
- 接着进程返回到用户态中,执行相应的信号处理函数
- 信号处理函数执行完成后,还需要返回内核态,检查是否还有信号未作处理,如果所欲信号都处理完成,就会将内核栈恢复,同时恢复指令寄存器将其指向终端前的运行位置,最后回到用户态继续执行进程
捕捉native crash
- 注册信号处理函数
//signum:信号编码
//act:指向结构体sigaction的一个实例的指针,该实例指定了对特定信号的处理,如果设置为空,进程会执行默认处理
//oldact:和参数act类似,只不过保存的是原来对相应信号的处理,也可设置为NULL
#include <signal.h>
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact));
struct sigaction sa_old;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_sigaction = my_handler;
sa.sa_flags = SA_SIGINFO;
if (sigaction(sig, &sa, &sa_old) == 0) {
...
}
- 设置额外栈空间
#include <signal.h>
int sigaltstack(const stack_t *ss, stack_t *oss);
stack_t stack;
memset(&stack, 0, sizeof(stack));
/* Reserver the system default stack size. We don't need that much by the way. */
stack.ss_size = SIGSTKSZ;
stack.ss_sp = malloc(stack.ss_size);
stack.ss_flags = 0;
/* Install alternate stack size. Be sure the memory region is valid until you revert it. */
if (stack.ss_sp != NULL && sigaltstack(&stack, NULL) == 0) { ... }
兼容其他signal处理
static void my_handler(const int code, siginfo_t *const si, void *const sc) { ... /* Call previous handler. */ old_handler.sa_sigaction(code, si, sc); }
Breakpad
- Breakpad是目前Native崩溃中最成熟的方案,
- google-breakpad有两种工作模式。进程内和进程外抓取
-
进程内模式通过使用exception_handle模块完成,使用也非常简单——实例化ExceptionHandler,在构造函数中注册回调FilterCallback和MinidumpCallback以及dump文件路径即可,前者Filter回调发生在崩溃之后,dump回调写入dump文件之间,在exception_handler.h中有详尽注释,毋庸赘言
-
进程外模式采用一个守护进程与崩溃进程进程间通讯的模式实现,xl当年的后台网页查毒也是使用这种结构,只是实现更笨重。这两种模式是排他的,进程外模式需要崩溃进程和监听者进程分别引入crash_generation_client和crash_generation_server
-
安装和编译
- 编译环境
- python 2.7
- windows sdk 7
- VS2005的补丁
- 资源下载
svn checkout http://google-breakpad.googlecode.com/svn/trunk/ google-breakpad-read-only
- 设置windows sdk 7
运行开始菜单->程序->Microsoft Windows SDK v7.0->Visual Studio Registration->Windows SDK Configuration Tool,选择v7.0,点击Make Current
- 生成windows工程文件
cd "源码目录/src/tools/gyp"
//注意,此处不能使用全路径,不然会出错
gyp.bat "../../client/windows/breakpad_client.gyp"
使用Breakpad
- 进程内抓取Dump文件
const std::wstring s_strCrashDir = L"c:\dumps";
bool
InitBreakpad()
{
google_breakpad::ExceptionHandler *pCrashHandler =
new google_breakpad::ExceptionHandler(s_strCrashDir,
onExceptionFilter,
onMinidumpDumped,
NULL,
google_breakpad::ExceptionHandler::HANDLER_ALL,
MiniDumpNormal,
NULL,
NULL);
if(pCrashHandler == NULL) {
return false;
}
return true;
}
- 进程外抓取Dump文件
- 使用进程外抓取Dump时,需要指定服务端和客户端,在服务端中需要创建CrashGenerationServer的实例,而在客户端中则只需要创建ExceptionHandler即可。此外,如果服务端自己需要抓进程内的Dump,请将pipe的参数置为NULL
const wchar_t s_pPipeName[] = L"\\.\pipe\breakpad\crash_handler_server";
const std::wstring s_strCrashDir = L"c:\dumps";
bool
InitBreakpad()
{
google_breakpad::CrashGenerationServer *pCrashServer =
new google_breakpad::CrashGenerationServer(s_pPipeName,
NULL,
onClientConnected,
NULL,
onClientDumpRequest,
NULL,
onClientExited,
NULL,
true,
&s_strCrashDir);
if(pCrashServer == NULL) {
return false;
}
// 如果已经服务端已经启动了,此处启动会失败
if(!pCrashServer->Start()) {
delete pCrashServer;
pCrashServer = NULL;
}
google_breakpad::ExceptionHandler *pCrashHandler =
new google_breakpad::ExceptionHandler(s_strCrashDir,
onExceptionFilter,
onMinidumpDumped,
NULL,
google_breakpad::ExceptionHandler::HANDLER_ALL,
MiniDumpNormal,
(pCrashServer == NULL) ? s_pPipeName : NULL, // 如果是服务端,则直接使用进程内dump
NULL);
if(pCrashHandler == NULL) {
return false;
}
return true;
}