iOS_Crash 4: Crash capture and protection

Article directory

  • 1.Crash capture
    • 1.2.NSException
    • 1.2.C++ exceptions
    • 1.3.Mach exception
    • 1.4.Unix signals
  • 2.Crash protection
    • 2.1. Method not implemented
    • 2.2.KVC causes crash
    • 2.3.KVO causes crash
    • 2.4. Collection class causes crash
    • 2.5. Other scenarios that require attention:

1.Crash capture

According to the different sources of Crash, it is divided into the following three categories:

1.2.NSException

Application layer exceptions, uncaught exceptions, cause the program to send the SIGABRT signal to itself and crash, which is controllable by the application itself. Uncaught exceptions can be caught through the try-catch or NSSetUncaughtExceptionHandler() mechanism class.

Common Exceptions:

  • NSInvalidArgumentException: Illegal parameter exception. Strengthen parameter checking to avoid passing in illegal parameters, especially parameters marked as nonull.
  • NSRangeException: out-of-bounds exception
  • NSGenericException: Modify the original collection while traversing
  • NSInternalInconsistencyException: Inconsistency exception. For example, NSDictionary is used when NSMutableNSDictionary is used.
  • NSFileHandleOperationException: File processing exception. A common problem is insufficient storage space
  • NSMallocException: Memory exception. Such as insufficient memory.
    All system-defined Exception see NSExceptionName

Capturing NSExpection:

//Record the previous Crash callback function (if any)
static NSUncaughtExceptionHandler *previousUncaughtExceptionHandler = NULL;

 + (void)registerUncaughtExceptionHandler {<!-- -->
    // Take out the Crash callback previously registered by others and back it up
    previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();
    // Then register your own
    NSSetUncaughtExceptionHandler( & amp;UncaughtExceptionHandler);
}

//Callback function on crash
static void UncaughtExceptionHandler(NSException * exception) {<!-- -->
    //Exception stack information
    NSArray *stackInfo = [exception callStackSymbols];
    //The reason for the exception
    NSString *reason = [exception reason];
    //Exception name
    NSString *name = [exception name];
    // Exception error reporting
    NSString *exceptionInfo = [NSString stringWithFormat:@"uncaughtException exception error report:\\
 name:%@\\
 reason:\\
 %@\\
 callStackSymbols:\\
 %@", name, reason, [stackInfo componentsJoinedByString:@"\\
"]];
    // Save Crash logs to the sandbox cache directory
    [SKTool cacheCrashLog:exceptionInfo name:@"CrashLog(UncaughtException)"];
    // After your own handler is processed, remember to register other people's handlers back to form a standardized SOP.
    if (previousUncaughtExceptionHandler) {<!-- -->
        previousUncaughtExceptionHandler(exception);
    }
    // Kill the program to prevent the SIGABRT thrown at the same time from being caught by the Signal exception.
    kill(getpid(), SIGKILL);
}

1.2.C++ exception

After the system catches the C++ exception, it will convert it into an OC exception and throw it. The call stack at this time is the leader when the exception occurs; but if the conversion fails, __cxa_throw will be called to throw the exception. The call captain at this time is the stack that handles the exception, causing the original exception call stack to be lost.
Catching C++ exceptions:

  1. Set exception handling function:
g_originalTerminateHandler = std::set_terminate(CPPExceptionTerminate);

Call set_terminate(CPPExceptionTerminate) to set the new global termination handler function and keep the old one.

  1. Override __cxa_throw:
void __cxa_throw(void* thrown_exception, std::type_info* tinfo, void (*dest)(void*)) {<!-- -->
    // Get the call stack and store it
    // Call the original __cxa_throw function again
}
  1. Exception handling function
    __cxa_throw is executed later and enters the exception sorting function set by set_terminate. If it is judged to be an OC exception, there will be nothing much and let the OC exception mechanism handle it; otherwise, the exception information will be obtained.

1.3.Mach exception

Kernel layer exceptions. User-mode developers can capture Mach exceptions by setting the exception ports of thread, task, and hot through the Mach API.

  • tasks: Resource ownership unit. Each task consists of a virtual address space, a port permission name control, and one or more threads. (similar to process)
  • threads: The unit of CPU execution in the task
  • ports: Secure simplex communication channel, accessible only through the send and receive functions.

APIs related to Mach exceptions are:

  • task_get_exception_ports: Get the exception port of the task
  • task_set_exception_ports: Set the exception port of the task
  • mach_port_allocate: Creates a caller-specified port permission type
  • mach_port_insert_right: Insert the specified port into the target task

Note: Avoid listening during Xcode joint debugging, as it will cause deadlock.

1.4.Unix signals

Also known as BSD signal, if the developer does not catch the Mach exception, the host layer method ux_exception() will convert the exception into the corresponding Unix signal and pass the method threadsignal() Delivers the signal to the erroring thread. Can be used with signal(x, SignalHandler) to capture signal.

Signal table:

  1. SIGHUP: hang
  2. SIGINT: Program termination signal interrupt, issued when the user types the INTR character (usually Ctrl-C), is used to notify the foreground process group to terminate the process.
  3. SIGQUIT: The program exits the signal quit, controlled by the QUIT character (usually Ctrl-). The program will generate a core file when it receives this signal and exits.
  4. SIGILL: Execute illegal instruction
  5. SIGTRAP: by breakpoint instruction or trap instruction
  6. SIGABRT: Program interrupt signal abort.
  7. SIGBUS: illegal address
  8. SIGFPE: Fatal arithmetic error
  9. SIGKILL: End the program immediately. Cannot be blocked, processed and ignored.
  10. SIGUSR1: User signal 1
  11. SIGSEGV: Invalid memory access
  12. SIGUSR2: User signal 2
  13. SIGPIPE: Pipe burst. Inter-process communication, such as abnormal reading and writing of pipes.
  14. SIGALRM: signal sent by alarm
  15. SIGTERM: Termination signal, can be blocked and processed. Usually used to require the program to exit normally on its own
  16. SIGSTKFLT: stack overflow
  17. SIGCHLD: Child process exits
  18. SIGCONT: Process continues
  19. SIGSTOP: process stopped
  20. SIGTSTP: Process stopped
  21. SIGTTIN: The process stops and the background process reads data from the terminal.
  22. SIGTTOU: The process stops and the background process wants to write data to the terminal.
  23. SIGURG: I/O urgent data reaches the current process
  24. SIGXCPU: The CPU time of the process has expired
  25. SIGXFSZ: File size exceeds upper limit
  26. SIGVTALRM: Virtual clock timeout
  27. SIGPROF: profile clock timeout
  28. SIGWINVH: window size changed
  29. SIGIO: I/O related
  30. SIGPWR: Shut down
  31. SIGSYS: Illegal system call

Tips: Enter kill -l in the terminal to view all signal signals.

Capture the signal:

//General signals that need to be captured
static const int g_fatalSignals[] = {<!-- -->
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGPIPE,
    SIGSEGV,
    SIGSYS,
    SIGTRAP,
};
void installSignalHandler() {<!-- -->
    stack_t ss;
    struct sigaction sa;
    struct timespec req, rem;
    long ret;
    //Apply a memory space for use as an optional signal processing function stack
    ss.ss_flags = 0;
    ss.ss_size = SIGSTKSZ;
    ss.ss_sp = malloc(ss.ss_size);
    // Use the sigaltstack function to notify the system of the existence and location of the optional signal processing stack frame
    sigaltstack(&ss, NULL);
    //Specify the SA_ONSTACK flag to notify the system that this signal processing function should execute the registered signal processing function on the optional stack frame
    memset( & amp;sa, 0, sizeof(sa));
    sa.sa_handler = handleSignalException;
    sa.sa_flags = SA_ONSTACK;
    sigaction(SIGABRT, &sa, NULL);
}

void XXXHandleSignalException(int signal) {<!-- -->
    // print stack
    NSMutableString *crashInfo = [[NSMutableString alloc] init];
    [crashInfo appendString:[NSString stringWithFormat:@"signal:%d\\
",signal]];
    [crashInfo appendString:@"Stack:\\
"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; + + i) {<!-- -->
        [crashInfo appendFormat:@"%s\\
", strs[I]];
    }
    NSLog(@"%@", crashInfo);
    //Remove other Crash listeners to prevent deadlock
    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);
}

2.Crash protection

2.1. Method not implemented

The implementation of the method cannot be found: unrecognized selector sent to instance. For details of the search process, please see: iOS_Objective-C message sending (message search and message forwarding) process

solution:
Add a new category to NSObject and implement several methods of message forwarding to avoid Crash:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {<!-- -->
    if ([self respondsToSelector:aSelector]) {<!-- --> // No processing has been implemented
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {<!-- -->
    NSLog(@"%@ can't responds %@", NSStringFromClass([self class]), NSStringFromSelector(anInvocation.selector));
}
 + (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {<!-- -->
    if ([self respondsToSelector:aSelector]) {<!-- --> // No processing has been implemented
        return [self methodSignatureForSelector:aSelector];
    }
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
 + (void)forwardInvocation:(NSInvocation *)anInvocation {<!-- -->
    NSLog(@"%@ can't responds %@", NSStringFromClass([self class]), NSStringFromSelector(anInvocation.selector));
}

2.2.KVC causes crash

Details of KVC’s search mode can be found at: iOS_KVC: Key-Value Coding-2 (visitor search mode). When the corresponding key is not found in the end, it will cause a crash.

Common scenarios:

  • Scenario 1: key does not exist
XXXClass * obj = [[XXXClass alloc] init];
[obj setValue:nil forKey:@"xxx"];
// reason: '[<XXXClass 0x2810bfa80> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xxx.'

id value = [obj valueForKey:@"xxx"];
// Thread 1: "[<MOPerson 0x600000c76c10> valueForUndefinedKey:]: this class is not key value coding-compliant for the key xxx."
  • Scenario 2: key is nil
XXXClass* obj = [[XXXClass alloc] init];
[obj setValue:@"value" forKey:nil];
// reason: '*** -[XXXClass setValue:forKey:]: attempt to set a value for a nil key'

// In addition: value will not crash if it is nil
[obj setValue:nil forKey:@"name"];

Solution: Override the implementation where the system throws an exception:

- (id)valueForUndefinedKey:(NSString *)key {<!-- -->
  NSLog(@"Error: valueForUndefinedKey: %@", key);
  return nil;
}
- (void)setValue:(id)value forUndefinedKey:(NSString *)key {<!-- -->
  NSLog(@"Error: setValue:%@ forUndefinedKey: %@", value, key);
}

2.3.KVO causes crash

Scenes:

  • Observer/observed are local variables
  • Not implemented observeValueForKeyPath:ofObject:changecontext:
  • Remove unregistered observers (e.g. repeated removal)

Tips: Adding observers repeatedly will not crash, but will call back multiple times.

solution:

  • addObserver and removeObserver must appear in pairs
  • Implemented using Facebook’s KVOController

2.4. Collection class causes crash

Common scenarios:

  • Cross the line
NSArray *arr = [NSArray array];
id value = [arr objectAtIndex:1];
// Thread 1: "*** -[__NSArray0 objectAtIndex:]: index 1 beyond bounds for empty array"
  • insert nil
NSMutableArray *arr = [NSMutableArray array];
[arr addObject:nil];
// Thread 1: "*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil"

NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setObject:nil forKey:@"xxx"];
// Thread 1: "*** -[__NSDictionaryM setObject:forKey:]: object cannot be nil (key: xxx)"

solution:

  • Use runtime to add null processing before calling these modification methods. For details, see: Demo

2.5. Other scenarios that require attention:

  • performSelector: must first determine respondsToSelector:
  • Before calling the delegate method, you must first determine respondsToSelector:
  • The id type cannot be forcibly converted, you must first determine isKindOfClass:
  • When accessing UIKit, be sure to dispatch to the main queue
  • An example. When thread access security cannot be guaranteed, remember to add a read-write lock.
  • dispatch_group_leave must appear in pairs with dispatch_group_enter
  • Check how attributes are modified (assign/strong/weak/copy)
  • Block must be empty before calling
  • Do not modify combined type objects while traversing them
  • Time-consuming operations must be dispatched to sub-threads to avoid triggering watchDog
  • Debug mode turns on zombie mode to facilitate immediate problem discovery.
  • Use Xcode‘s Address Sanitizer to detect address access out of bounds

refer to:
iOS Crash/Crash/Exception capture
Linux signal list
A brief discussion on Crash capture and protection in iOS
Summary of common Crash in iOS