iOS奔溃信息收集

来源:互联网 发布:淘宝美姿堂meiji真假 编辑:程序博客网 时间:2024/06/05 07:59

概述

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上这个类型里包含了一些寄存器信息:

// arm32struct __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 */};
// arm64struct __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 */};

这些寄存器保存了程序奔溃时的值,其中比较重要的两个寄存器是lrfp
lrlink register,即函数的返回地址
fpframe pointer,即当前栈帧的指针

arm32的fpr7

iOS 上函数调用栈的栈帧结构如下:

/** * stack frame * * ---------- * |        | * ---------- * | lr     | <- fp[1] * ---------- * | old fp | <- fp * ---------- * |        | * ---------- * |        | * ---------- */

每一个栈帧中都保存了old fplr,可以通过栈帧指针回溯整个backtrace
然后通过lrdladdr函数查询函数符号名等信息

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