Crash与信号

什么是信号

信号(signal)是一种XPC通信方式。
signal是一个4字节的无符号整形数字,在iOS/OSX中定义了31个已知的信号;
在Unix系统中,crash仅仅是singal触发的一个行为。signal的用途/产生包括但不限于:

  • 显示调用kill,killpg触发signal
  • 改变子进程的状态
  • 致命性中断
  • job控制
  • timer过期
  • 各种通知,如cpu resource limit或file size limit等

signal会导致以下几种行为(action):

  • Terminate 杀死进程
  • Dump core 杀死进程并创建一个core file
  • Stop 暂停进程suspend
  • Continue 恢复进程resume
  • Ignore 忽略/丢弃该信号

每个signal号都有默认的action,可以通过sigaction() 系统调用来修改signal actions,为SIG_DFL(use the default action),或者修改为SIG_IGN(ignore the signal),或者指定signal handler function(捕获signal)

一般的异常捕获工具会基于上述方式来修改signal的actions或执行signal handler,从而达到异常捕获的目的。我们也可以通过修改signal actions来达到让应用直接忽略某个信号,而不发生异常退出。但是这也有个别例外情况,比如SIGKILL,SIGSTOP 无法被捕获或忽略。

信号如何产生

使用过kill函数来达到向某个进程pid发送signal:

int kill(pid_t pid, int sig);

pid>0,则sig会发送给对应process id的进程;

pid=0,则会发给所有和sender具有相同group id的进程;

pid=-1,如果具有超级权限,则sig会发给除了系统进程以及sender进程外的所有进程;否则sig会发给除了sender以外的所有当前user下的进程;

pid<-1,效果等同killpg

使用pthread_kill向某个线程发送signal:

int pthread_kill(pthread_t thread, int sig);
信号与多线程

signal的分发实现是基于per-thread signal masks的,它确保了多线程情况下信号依旧能被顺序处理,即线程a在处理信号时,其它线程的信号会被阻塞。系统提供了pthread_sigmask()调用来修改signal mask;默认情况下子线程的masks是和创建它的线程是一致的。

如果一个信号是由于trap,illegal instruction或arithmetic exception则该信号会被分发给对应触发的线程(主要是synchronous signal);否则这些信号会被发给第一个不阻塞该signal的线程;

注意:SIGKILL,SIGSTOP,SIGTERM会影响整个进程;

当发送信号时,接收者signal_handler会收到siginfo_t,这个结构存储了详细的信号信息;siginfo_t结构如下

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;

在iOS中,当进程/线程收到crash信号时,会统一先交给_sigtramp去处理

void
_sigtramp(
    union __sigaction_u __sigaction_u,
    int             sigstyle,
    int             sig,
    siginfo_t       *sinfo,
    ucontext_t      *uctx
)

而sigtramp会将对应的信号交给注册的signal_handler处理。

Mach异常如何转为signal

内核启动后,会通过ux_handler_init()来初始化Unix exception handler,并通过内置的kernel exception handler来将Mach异常转为Unix信号。
当Mach异常发生时,其会通过ux_exception调用将mach exception转为unix signal信号并发出去,大概伪代码流程如下:

kern_return_t
catch_exception_raise(...)
{
...
    if (th_act != THR_ACT_NULL) {
        ut = get_bsdthread_info(th_act);
        //convert {Mach exception,code,subcode} to {Unix signal,uu_code}
        ux_exception(exception, code[0], code[1], &ux_signal, &ucode);
        //send signal
        if (ux_signal != 0)
            threadsignal(th_act, signal, ucode);
        thread_deallocate(th_act);
    }
...
}

实际的转换大概如下表:

Mach Exception Mach Exception Code Unix signal
EXC_ARITHMETIC - SIGFPE
EXC_BAD_ACCESS KERN_INVALID_ADDRESS SIGSEGV
EXC_BAD_ACCESS - SIGBUS
EXC_BAD_INSTRUCTION - SIGILL
EXC_BREAKPOINT - SIGTRAP
EXC_EMULATION - SIGEMT
EXC_SOFTWARE EXC_UNIX_ABORT SIGABRT
EXC_SOFTWARE EXC_UNIX_BAD_PIPE SIGPIPE
EXC_SOFTWARE EXC_UNIX_BAD_SYSCALL SIGSYS
EXC_SOFTWARE EXC_SOFT_SIGNAL SIGKILL

在arm64后,基本不存在SIGFPE的异常了;主要是SDIV等指令主动规避了异常;

信号的转换是基于ux_exception函数的,源码如下:

/*
 *  ux_exception translates a mach exception, code and subcode to
 *  a signal and u.u_code.  Calls machine_exception (machine dependent)
 *  to attempt translation first.
 */

static
void ux_exception(
    int         exception,
    int         code,
    int         subcode,
    int         *ux_signal,
    int         *ux_code
)
{
    /*
     *  Try machine-dependent translation first.
     */
    if (machine_exception(exception, code, subcode, ux_signal, ux_code))
    return;
    
    switch(exception) {

    case EXC_BAD_ACCESS:
        if (code == KERN_INVALID_ADDRESS)
            *ux_signal = SIGSEGV;
        else
            *ux_signal = SIGBUS;
        break;

    case EXC_BAD_INSTRUCTION:
        *ux_signal = SIGILL;
        break;

    case EXC_ARITHMETIC:
        *ux_signal = SIGFPE;
        break;

    case EXC_EMULATION:
        *ux_signal = SIGEMT;
        break;

    case EXC_SOFTWARE:
        switch (code) {

        case EXC_UNIX_BAD_SYSCALL:
        *ux_signal = SIGSYS;
        break;
        case EXC_UNIX_BAD_PIPE:
        *ux_signal = SIGPIPE;
        break;
        case EXC_UNIX_ABORT:
        *ux_signal = SIGABRT;
        break;
        }
        break;

    case EXC_BREAKPOINT:
        *ux_signal = SIGTRAP;
        break;
    }
}

如何捕获信号

iOS异常的形式主要表现形式为signal,操作系统会自动把mach异常也转为signal分发给进程;所以crash捕获(信号捕获)主要是基于可捕获的部分signal来做的。当发生错误时,系统通过signal将错误传递给进程,进程可以通过注册signal_handler来对大部分signal进行crash跟踪上报;

一般方式是

//...sigaltstack
    struct sigaction sa = {0};
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO|SA_ONSTACK;
#ifdef __LP64__
    sa.sa_flags |= SA_64REGSET;
#endif
    sa.sa_sigaction = &my_signal_handler;

    sigaction(fatalsigs[i], &sa, &g_previousSignalHandlers[i]);
    
    //
    static void my_signal_handler(int signal, siginfo_t *info, void *uap)
    {
        //record crash
    }
关于SA_ONSTACK

一般而言当crash发生时如果signal_handler的回调是在当前crash堆栈里的,则可能会破坏线程栈信息,因此POSIX.1允许进程在指定signal_handler时让signal_handler回调执行在特定的signal的栈空间内,而不破坏原始的栈信息。

常见Crash信号

标准的crash信号,定义在signal.h头文件里,大家可以在如下头文件里去找

#include <sys/signal.h>

iOS开发中常见的Crash或调试相关的信号如下:

信号 官方注释 可能原因
SIGILL 4 illegal instruction (not reset when caught) ILL_ILLTRP at 0xxxx通常是二进制出错,典型比如app升级前后或者dyld缓存出错
SIGTRAP 5 trace trap (not reset when caught) __builtin_trap()系统调用brk触发软中断结束进程,一般是数据或参数校验异常
SIGABRT 6 abort() 调用abort(),比如典型的NS异常或C++异常
SIGKILL 9 kill (cannot be caught or ignored) 系统升级,app升级,XCode调试等触发杀死app,摄像头权限变更等
SIGBUS 10 bus error 总线错误,内存访问未对齐
SIGSEGV 11 segmentation violation 内存访问越界,内存crash或者地址错误,如栈溢出等
SIGPIPE 13 write on a pipe with no one to read it 管道异常,socket通信异常
SIGSTOP 17 sendable stop signal not from tty XCode调试时pause操作可触发

另外armv8开始已经没有除0异常了,如SDIV除法指令从内部做了校验,如果被除数为0则结果返回0;

SIGBUS VS SIGSEGV

SIGBUS和SIGSEGV问题都代表是内存访问错误;他们都可以是mach_exception转换而来,核心差异在是否是KERN_INVALID_ADDRESS;(参见ux_exception源码)

SIGSEGV意味着segmentation fault,可能原因如下:

  • 使用未初始化的指针
  • 解引用空指针
  • 访问无效内存(无权限或不存在)
  • 访问悬垂指针

SIGBUS意味着bus error,可能原因如下:

  • 内存访问未对齐
  • 访问的内存地址有效但是无权限

主要差异在于SIGSEGV访问的VA必定是无效的且未映射到内存,而SIGBUS访问的VA是有效的但没有权限访问;

如何触发SIGBUS ?

一般情况下即便你通过硬编码来尝试让代码走入非对齐内存访问,也基本不会有问题;因为常用的LDR,LDRB,STR等指令自动处理了内存对齐错误的问题;

In ARMv6 and later, except ARMv6-M, unaligned accesses are permitted for LDR, LDRH, STR, STRH, LDRSH, LDRT, STRT, LDRSHT, LDRHT, STRHT, and TBH instructions, where the architecture supports the instruction.

On some ARM processors, you can enable alignment checking. Non word-aligned 32-bit transfers cause an alignment exception if alignment checking is enabled.

只有在一些特殊的如LDRB等未兼容unaligned accessed的指令下,出现未对齐错误则会容易出现该问题。

ldrb w0,x20

参考

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/signal.3.html

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/kill.2.html

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/pthread_kill.2.html

https://developer.apple.com/library/archive/technotes/tn2151/_index.html

https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/sigaction.2.html#//apple_ref/doc/man/2/sigaction

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Exceptions/Articles/Exceptions64Bit.html#//apple_ref/doc/uid/TP40009044-SW1

https://www.geeksforgeeks.org/segmentation-fault-sigsegv-vs-bus-error-sigbus/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容