iOS crash classification, Mach exception, Unix signal and NSException exception

preface

The main reason for Crash is that your application receives an unprocessed signal.

Unprocessed signals may come from three places: the kernel, other processes, and the App itself.

Therefore, crash exceptions can also be divided into three types:

  • Mach exception: refers to the lowest kernel level exception. Developers in user mode can directly set the exception ports of thread, task and host through the Mach API to catch Mach exceptions.
  • UNIX signal: also known as BSD signal. If the developer does not catch Mach exception, it will be rejected by the host layer method ux_exception() converts the exception into the corresponding UNIX signal and delivers the signal to the error thread through the method threadsignal(). single can be captured through the method signal(x, SignalHandler).
  • NSException: application level exception. It is an uncapped Objective-C exception that causes the program to crash after sending SIGABRT signal to itself. Uncapped Objective-C exceptions can be caught by try catch or by NSSetUncaughtExceptionHandler() mechanism.

Mach exception and Unix signal

What is Mach exception? How does it connect with Unix signals? Mach is an XNU microkernel core, and Mach exception refers to the lowest kernel level exception. Each thread, task and host has an exception port array. Some APIs of Mach are exposed to the user state. Developers in the user state can directly set the exception ports of thread, task and host through the Mach API to catch Mach exceptions and grab Crash events.

 

All Mach exceptions are not handled and will be UX executed at the host layer_ The exception is converted into the corresponding Unix signal, and the signal is delivered to the wrong thread through threadsignal. POSIX API in iOS is implemented through BSD layer above Mach.

Look at Matt answer:

EXC_BAD_ACCESS is a Mach exception sent by the kernel to your application when you try to access memory that is not mapped for your application. If not handled at the Mach level, it will be translated into a SIGBUS or SIGSEGV BSD signal.

Exc_ BAD_ The access exception mainly refers to accessing the memory address that does not belong to the process or has been released. If it is not captured in the mach layer, it will be converted into SIGSEGV signal at the host layer and delivered to the wrong thread.

The crash event can be caught by capturing Mach exceptions or Unix signals. Which of these two methods is better?

Mach exception is preferred, because Mach exception processing will occur before Unix signal processing. If the handler of Mach exception lets the program exit, the Unix signal will never reach this process.

If Mach is preferred to catch exceptions, why convert it to unix signals?

Unix signal conversion is to be compatible with the more popular POSIX standard (SUS specification), so that Mach kernel can be developed through Unix signal without understanding.

Why does the third-party library PLCrashReporter give up capturing Mach exception exc even when it is preferred to capture Mach exception_ Crash, and select to capture the corresponding SIGABRT signal?

We still need to use signal handlers to catch SIGABRT in-process. The kernel sends an EXC_CRASH mach exception to denote SIGABRT termination. In that case, catching the Mach exception in-process leads to process deadlock in an uninterruptable wait. Thus, we fall back on BSD signal handlers for SIGABRT, and do not register for EXC_CRASH.

be careful:
Because the signal generated by the hardware (through the CPU trap) is captured by the Mach layer and then converted into the corresponding Unix signal; In order to unify the mechanism, the signals generated by the operating system and users (by calling kill and pthread_kill) also sink first, are converted into Mach exceptions, and then into Unix signals.

In other words, the whole process is as follows:
The hardware generates a signal or kill or pthread_kill signal -- > Mach exception -- > UNIX signal (SIGABRT)

Therefore, the process of capturing crash is like this

Crash collection method

Collected through UncaughtExceptionHandler mechanism

This mobile phone method is only suitable for collecting application level exceptions. All we have to do is replace the ExceptionHandler with a custom function.

// Crash callback function before logging
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

@implementation NWUncaughtExceptionHandler

#pragma mark - Register

+ (void)registerHandler {
    //Take out and back up the handler previously registered by others
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}

#pragma mark - Private

// Callback function on crash
static void UncaughtExceptionHandler(NSException * exception) {
    // Abnormal stack information
    NSArray * stackArray = [exception callStackSymbols];
    // Cause of abnormality
    NSString * reason = [exception reason];
    // Exception name
    NSString * name = [exception name];
    
    NSString * exceptionInfo = [NSString stringWithFormat:@"========uncaughtException Exception error report========\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@", name, reason, [stackArray componentsJoinedByString:@"\n"]];
    
    // Save the crash log to the sandbox cache directory
    [NWCrashTool saveCrashLog:exceptionInfo fileName:@"Crash(Uncaught)"];
    
    //Register other people's handlers after their own handlers are processed and deliver them in a proper manner
    if (previousUncaughtExceptionHandler) {
        previousUncaughtExceptionHandler(exception);
    }
    
    // Kill the program to prevent SIGABRT thrown at the same time from being caught by SignalException
    kill(getpid(), SIGKILL);
}

@end

be careful:

Is it wise to integrate multiple Crash log collection services in your own program?

Generally, the third-party functional SDK will integrate a Crash collection service to find its own SDK problems in time. When each service aims to ensure that its Crash statistics are correct and complete, it is inevitable that there will be malicious competition such as timing manipulation and forced coverage, which will lead to the loss of Last Exception Backtrace and other information because the Crash log written by the previously registered log collection service cannot get NSException.

Therefore, if multiple parties register exception handlers through NSSetUncaughtExceptionHandler at the same time, the correct approach is: the later registrant takes out and backs up the previously registered handlers of others through NSGetUncaughtExceptionHandler, and consciously registers other people's handlers after handling their own handlers, so as to deliver them in a regular manner

Will the NSException without NSSetUncaughtExceptionHandler be converted into a Unix signal?

No matter whether the NSSetUncaughtExceptionHandler is set or not, as long as it is not tried to catch, it will eventually be converted into a Unix signal, but it is set that the Unix signal type that can not be finally sent can not be obtained in its ExceptionHandler

Mach exception mode

 

This is basically unused.

Unix signal

Unix signal: signal(SIGSEGV,signalHandler);

SIGABRT is a BSD signal sent by an application to itself when an NSException or obj_exception_throw is not caught.

The general meaning is:
When NSException or obj_ exception_ When throw is not captured, the application sends itself a SIGABRT signal.
However, this does not mean that SIGABRT is caused by NSException, because SIGABRT is a signal generated by calling abort().
If the program crashes due to NSException, the Last Exception Backtrace information in the system log is complete and accurate.

#import "NWCrashSignalExceptionHandler.h"
#import <execinfo.h>
#import "NWCrashTool.h"

typedef void(*SignalHandler)(int signal, siginfo_t *info, void *context);

static SignalHandler previousABRTSignalHandler = NULL;
static SignalHandler previousBUSSignalHandler = NULL;
static SignalHandler previousFPESignalHandler = NULL;
static SignalHandler previousILLSignalHandler = NULL;
static SignalHandler previousPIPESignalHandler = NULL;
static SignalHandler previousSEGVSignalHandler = NULL;
static SignalHandler previousSYSSignalHandler = NULL;
static SignalHandler previousTRAPSignalHandler = NULL;

@implementation NWCrashSignalExceptionHandler

+ (void)registerHandler {
    // Take out and back up the handler previously registered by others
    [self backupOriginalHandler];
    
    [self signalRegister];
}

+ (void)backupOriginalHandler {
    struct sigaction old_action_abrt;
    sigaction(SIGABRT, NULL, &old_action_abrt);
    if (old_action_abrt.sa_sigaction) {
        previousABRTSignalHandler = old_action_abrt.sa_sigaction;
    }
    
    struct sigaction old_action_bus;
    sigaction(SIGBUS, NULL, &old_action_bus);
    if (old_action_bus.sa_sigaction) {
        previousBUSSignalHandler = old_action_bus.sa_sigaction;
    }
    
    struct sigaction old_action_fpe;
    sigaction(SIGFPE, NULL, &old_action_fpe);
    if (old_action_fpe.sa_sigaction) {
        previousFPESignalHandler = old_action_fpe.sa_sigaction;
    }
    
    struct sigaction old_action_ill;
    sigaction(SIGILL, NULL, &old_action_ill);
    if (old_action_ill.sa_sigaction) {
        previousILLSignalHandler = old_action_ill.sa_sigaction;
    }
    
    struct sigaction old_action_pipe;
    sigaction(SIGPIPE, NULL, &old_action_pipe);
    if (old_action_pipe.sa_sigaction) {
        previousPIPESignalHandler = old_action_pipe.sa_sigaction;
    }
    
    struct sigaction old_action_segv;
    sigaction(SIGSEGV, NULL, &old_action_segv);
    if (old_action_segv.sa_sigaction) {
        previousSEGVSignalHandler = old_action_segv.sa_sigaction;
    }
    
    struct sigaction old_action_sys;
    sigaction(SIGSYS, NULL, &old_action_sys);
    if (old_action_sys.sa_sigaction) {
        previousSYSSignalHandler = old_action_sys.sa_sigaction;
    }
    
    struct sigaction old_action_trap;
    sigaction(SIGTRAP, NULL, &old_action_trap);
    if (old_action_trap.sa_sigaction) {
        previousTRAPSignalHandler = old_action_trap.sa_sigaction;
    }
}

+ (void)signalRegister {
    NWSignalRegister(SIGABRT);
    NWSignalRegister(SIGBUS);
    NWSignalRegister(SIGFPE);
    NWSignalRegister(SIGILL);
    NWSignalRegister(SIGPIPE);
    NWSignalRegister(SIGSEGV);
    NWSignalRegister(SIGSYS);
    NWSignalRegister(SIGTRAP);
}

#pragma mark - Private

#pragma mark Register Signal

static void NWSignalRegister(int signal) {
    struct sigaction action;
    action.sa_sigaction = NWSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}

#pragma mark SignalCrash Handler

static void NWSignalHandler(int signal, siginfo_t* info, void* context) {
    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Signal Exception:\n"];
    [mstr appendString:[NSString stringWithFormat:@"Signal %@ was raised.\n", signalName(signal)]];
    [mstr appendString:@"Call Stack:\n"];
    
    // The first line of log is filtered out here
    // Because the signal crash callback method is registered, the system will call it and record it on the call stack. Therefore, this line log needs to be filtered out
    for (NSUInteger index = 1; index < NSThread.callStackSymbols.count; index++) {
        NSString *str = [NSThread.callStackSymbols objectAtIndex:index];
        [mstr appendString:[str stringByAppendingString:@"\n"]];
    }
    
    [mstr appendString:@"threadInfo:\n"];
    [mstr appendString:[[NSThread currentThread] description]];
    
    // Save the crash log to the sandbox cache directory
    [NWCrashTool saveCrashLog:[NSString stringWithString:mstr] fileName:@"Crash(Signal)"];
    
    NWClearSignalRegister();
    
    // Call the callback function that crashed before
    // Register other people's handlers after their own handlers are processed and deliver them in a proper manner
    previousSignalHandler(signal, info, context);
    
    kill(getpid(), SIGKILL);
}

#pragma mark Signal To Name

static NSString *signalName(int signal) {
    NSString *signalName;
    switch (signal) {
        case SIGABRT:
            signalName = @"SIGABRT";
            break;
        case SIGBUS:
            signalName = @"SIGBUS";
            break;
        case SIGFPE:
            signalName = @"SIGFPE";
            break;
        case SIGILL:
            signalName = @"SIGILL";
            break;
        case SIGPIPE:
            signalName = @"SIGPIPE";
            break;
        case SIGSEGV:
            signalName = @"SIGSEGV";
            break;
        case SIGSYS:
            signalName = @"SIGSYS";
            break;
        case SIGTRAP:
            signalName = @"SIGTRAP";
            break;
        default:
            break;
    }
    return signalName;
}

#pragma mark Previous Signal

static void previousSignalHandler(int signal, siginfo_t *info, void *context) {
    SignalHandler previousSignalHandler = NULL;
    switch (signal) {
        case SIGABRT:
            previousSignalHandler = previousABRTSignalHandler;
            break;
        case SIGBUS:
            previousSignalHandler = previousBUSSignalHandler;
            break;
        case SIGFPE:
            previousSignalHandler = previousFPESignalHandler;
            break;
        case SIGILL:
            previousSignalHandler = previousILLSignalHandler;
            break;
        case SIGPIPE:
            previousSignalHandler = previousPIPESignalHandler;
            break;
        case SIGSEGV:
            previousSignalHandler = previousSEGVSignalHandler;
            break;
        case SIGSYS:
            previousSignalHandler = previousSYSSignalHandler;
            break;
        case SIGTRAP:
            previousSignalHandler = previousTRAPSignalHandler;
            break;
        default:
            break;
    }
    
    if (previousSignalHandler) {
        previousSignalHandler(signal, info, context);
    }
}

#pragma mark Clear

static void NWClearSignalRegister() {
    signal(SIGSEGV,SIG_DFL);
    signal(SIGFPE,SIG_DFL);
    signal(SIGBUS,SIG_DFL);
    signal(SIGTRAP,SIG_DFL);
    signal(SIGABRT,SIG_DFL);
    signal(SIGILL,SIG_DFL);
    signal(SIGPIPE,SIG_DFL);
    signal(SIGSYS,SIG_DFL);
}

@end

Common Unix signals

1.SIGABRT is the signal generated by calling abort(), which may be NSException or Mach exception
2.SIGBUS: illegal address, including memory address alignment error. For example, access a four word integer, but its address is not a multiple of 4. For example:

char *s = "hello world";
*s = 'H';

3.SIGSEGV: try to access the memory not allocated to itself, or try to write data to the memory address without write permission. For example, send a message to an object that has been release d
4.SIGILL: illegal instruction executed It is usually due to an error in the executable itself or an attempt to execute a data segment It is also possible to generate this signal when the stack overflows.
5.SIGPIPE: pipeline rupture. This signal is usually generated in inter process communication. For example, two processes using FIFO (pipeline) communication write to the pipeline without opening or accidental termination, and the writing process will receive SIGPIPE signal. In addition, for the two processes communicating with the Socket, when the write process writes to the Socket, the read process has terminated.
6.SIGSEGV: attempt to access memory not allocated to itself, or attempt to write data to a memory address without write permission
7.SIGSYS: illegal system call.
8. Sigrap: generated by breakpoint instruction or other trap instruction Used by debugger.

Added by schajee on Fri, 31 Dec 2021 06:36:11 +0200