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 

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/

你可能感兴趣的:(Crash与信号)