概述
app 奔溃主要有两种原因:
1,程序某处抛了一个异常,却未被捕获,会导致std::terminate
函数被调用,std::terminate
调用std::terminate_handler
类型的终止处理器,默认的终止处理器调用abort
函数终止程序
2,进程收到一个默认终止进程的信号,大多数信号的默认行为都是终止进程
针对第一种情况,可以安装一个终止处理器来来执行异常信息收集
针对第二种情况,可以安装信号处理器来执行信号信息收集
异常信息收集
void terminate_handler()
{
std::string name, reason;
std::vector<std::string> symbols;
auto except = std::current_exception();
if (except) {
try {
std::rethrow_exception(except);
}
catch(const NSException *e) {
name = e.name.UTF8String;
reason = e.reason.UTF8String;
for (NSString *sym in e.callStackSymbols)
symbols.push_back(sym.UTF8String);
}
catch(const std::exception &e) {
name = "unknown";
reason = e.what() ?: "unknown";
}
catch(...) {
}
}
// send exception info
// ...
raise(SIGKILL);
}
std::set_terminate(terminate_handler);
在终止处理器里把异常重新抛出来,分别捕获两个异常基类,从基类里获取异常信息,然后发送给server。
信号信息收集
首先设置信号处理器的执行栈,如果不设置信号处理器的执行栈的话,当进程栈溢出时(比如无穷递归),信号处理器就没法执行了。
void setup_stack()
{
stack_t sigstack;
sigstack.ss_sp = ::operator new(SIGSTKSZ, std::nothrow);
sigstack.ss_size = SIGSTKSZ;
sigstack.ss_flags = 0;
sigaltstack(&sigstack, nullptr);
}
安装信号处理器
void install_sig_handler(int signo, void (*handler)(int, siginfo_t *, void *))
{
struct sigaction action, old_action;
sigset_t sigset; sigemptyset(&sigset);
action.sa_flags = SA_SIGINFO | SA_RESTART | SA_ONSTACK;
action.sa_sigaction = handler;
action.sa_mask = sigset;
sigaction(signo, &action, &old_action);
}
void sig_handler(int signo, siginfo_t *info, void *context)
{
}
install_sig_handler(SIGSYS, sig_handler);
install_sig_handler(SIGPIPE, sig_handler);
install_sig_handler(SIGTERM, sig_handler);
install_sig_handler(SIGBUS, sig_handler);
install_sig_handler(SIGFPE, sig_handler);
install_sig_handler(SIGTRAP, sig_handler);
install_sig_handler(SIGILL, sig_handler);
install_sig_handler(SIGSEGV, sig_handler);
install_sig_handler(SIGABRT, sig_handler);
把常见导致奔溃的信号都捕获了
信号处理器
看一下信号处理器的函数原型:
void sig_handler(int signo, siginfo_t *info, void *context);
参数说明:
signo: 捕获的 signal number
info
typedef struct __siginfo {
int si_signo; /* signal number */
int si_errno; /* errno association */
int si_code; /* signal code */
pid_t si_pid; /* sending process */
uid_t si_uid; /* sender's ruid */
int si_status; /* exit value */
void *si_addr; /* faulting instruction */
union sigval si_value; /* signal value */
long si_band; /* band event for SIGPOLL */
unsigned long __pad[7]; /* Reserved for Future Use */
} siginfo_t;
其中 si_addr 是奔溃的地址
context 这个参数的类型是:ucontext_t,这个类型是硬件相关的,不可移植
在iOS上这个类型里包含了一些寄存器信息:
// arm32
struct __darwin_arm_thread_state {
__uint32_t __r[13]; /* General purpose register r0-r12 */
__uint32_t __sp; /* Stack pointer r13 */
__uint32_t __lr; /* Link register r14 */
__uint32_t __pc; /* Program counter r15 */
__uint32_t __cpsr; /* Current program status register */
};
// arm64
struct __darwin_arm_thread_state64 {
__uint64_t __x[29]; /* General purpose registers x0-x28 */
__uint64_t __fp; /* Frame pointer x29 */
__uint64_t __lr; /* Link register x30 */
__uint64_t __sp; /* Stack pointer x31 */
__uint64_t __pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */
};
这些寄存器保存了程序奔溃时的值,其中比较重要的两个寄存器是lr
和fp
lr
是 link register
,即函数的返回地址
fp
是 frame pointer
,即当前栈帧的指针
arm32的fp
是r7
iOS 上函数调用栈的栈帧结构如下:
/**
* stack frame
*
* ----------
* | |
* ----------
* | lr | <- fp[1]
* ----------
* | old fp | <- fp
* ----------
* | |
* ----------
* | |
* ----------
*/
每一个栈帧中都保存了old fp
和lr
,可以通过栈帧指针回溯整个backtrace
然后通过lr
和dladdr
函数查询函数符号名等信息
Dl_info dlinfo;
dladdr(lr, &dlinfo);
Dl_info dlinfo;
void **fp = reinterpret_cast<void **>(__fp);
while (fp) {
void *lr = fp[1];
dladdr(lr, &dlinfo);
// dlinfo 里包含函数符号名,函数地址,动态库(image)地址,动态库路径等信息
// ...
fp = reinterpret_cast<void **>(fp[0]); // fp = old_fp
}
这样就可以拿到崩溃时的函数backtrace
注意,通过C库函数backtrace
, backtrace_symbols
或(Objc的[NSThread callStackSymbols]
)拿到的backtrace是不对的,库函数是拿不到奔溃时的栈回溯信息的。在异常信息收集时也是一样,捕获异常时通过库函数backtrace
, backtrace_symbols
也是拿不到抛异常时的栈回溯信息的,除非在抛异常前调用backtrace
, backtrace_symbols
拿到栈回溯信息,然后保存到即将要抛出的异常对象中,就像NSException那样。
总结
奔溃信息收集,主要是收集奔溃时的奔溃地址,以及栈回溯信息等。
像原始C数组越界会产生信号
而Objc的数组越界会抛出异常
注意:
在开发信号相关的程序时,不能连接调试器,否则,信号处理器不会被调用,因为调试器会捕获信号,并且覆盖了我们的信号处理器
参考资料:
《The Linux Programming Interface》
man page