One, foreword

In daily development or testing, our application may Crash. We have zero tolerance for this type of problem, because if it happens online, it can seriously affect the user experience.

If a Crash occurs during development, the developer can locate the cause of the problem based on Xcode’s call stack or console output. However, if it is in the testing process, it is more troublesome. Two common solutions are:

  1. Connect the test phone directly to Xcode and view the logs in the device information.
  2. It is necessary for the test student to give the reoccurrence path of Crash, and then the developer to reproduce it in the debugging process.

However, neither method is very convenient. So the question is, is there a better way to view Crash logs? The answer, of course, is yes. The Crash viewing function in DoraemonKit’s common tool set solves this problem. You can directly view Crash logs on the APP side. Let’s introduce the implementation of the Crash viewing function below.

Two, technical implementation

During the development of iOS, there will be a variety of crashes, so how to capture these different crashes? In fact, common Crash can be divided into two categories, one is Objective-C exception and the other is Mach exception. Some common exceptions are shown in the figure below:

Let’s look at how these two types of exceptions should be caught.

Objective – 2.1 C

As the name suggests, Objective-C exceptions are exceptions that occur at the OC level (when errors occur in iOS libraries, third-party libraries). Before we look at how to catch Objective-C exceptions let’s look at what common Objective-C exceptions are.

2.1.1 Common Objective-C exceptions

In general, common Objective-C exceptions include the following:

  • NSInvalidArgumentException illegal parameters (abnormal)

The main reason for this type of exception is that there is no validation of arguments, most commonly passing nil as arguments. For example, NSMutableDictionary adds an object with a key of nil. The test code is as follows:

NSString *key = nil;
NSString *value = @"Hello";
NSMutableDictionary *mDic = [[NSMutableDictionary alloc] init];
[mDic setObject:value forKey:key];
Copy the code

The console outputs logs after running:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', 
reason: '*** -[__NSDictionaryM setObject:forKey:]: key cannot be nil'
Copy the code
  • NSRangeException

The main reason for this kind of exception is that there is no validity check on the index, causing the index to fall outside the legitimate scope of the collection data. For example, if the index is out of range of the array, the array is out of range, the test code is as follows:

    NSArray *array = @[@0, @1, @2];
    NSUInteger index = 3;
    NSNumber *value = [array objectAtIndex:index];
Copy the code

The console outputs logs after running:

*** Terminating app due to uncaught exception 'NSRangeException', 
reason: '*** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]'
Copy the code
  • NSGenericException (generic exception)

This type of exception is most likely to occur in foreach operations, mainly because elements are modified during traversal. For example, in a for in loop, if you modify the array being iterated over, this will cause the problem. The test code is as follows:

    NSMutableArray *mArray = [NSMutableArray arrayWithArray:@[@0, @1, @2]];
    for (NSNumber *num in mArray) {
        [mArray addObject:@3];
    }
Copy the code

The console outputs logs after running:

*** Terminating app due to uncaught exception 'NSGenericException', 
reason: '*** Collection <__NSArrayM: 0x600000c08660> was mutated while being enumerated.'
Copy the code
  • NSMallocException (Memory allocation exception)

The main cause of such exceptions is the inability to allocate sufficient memory space. For example, allocating a large chunk of memory can cause this type of exception. The test code is as follows:

    NSMutableData *mData = [[NSMutableData alloc] initWithCapacity:1];
    NSUInteger len = 1844674407370955161;
    [mData increaseLengthBy:len];
Copy the code

The console outputs logs after running:

*** Terminating app due to uncaught exception 'NSMallocException', 
reason: 'Failed to grow buffer'
Copy the code
  • Exception handling NSFileHandleOperationException (file)

The main cause of this type of exception is that an exception occurs during file operations, for example, the mobile phone does not have enough storage space or the file read/write permission problem. For example, to write to a file with read permission only, the test code is as follows:

NSString *cacheDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSString *filePath = [cacheDir stringByAppendingPathComponent:@"1.txt"]; if (! [[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSString *str1 = @"Hello1"; NSData *data1 = [str1 dataUsingEncoding:NSUTF8StringEncoding]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:data1 attributes:nil]; } NSFileHandle *fileHandle = [NSFileHandle fileHandleForReadingAtPath:filePath]; [fileHandle seekToEndOfFile]; NSString *str2 = @"Hello2"; NSData *data2 = [str2 dataUsingEncoding:NSUTF8StringEncoding]; [fileHandle writeData:data2]; [fileHandle closeFile];Copy the code

The console outputs logs after running:

*** Terminating app due to uncaught exception 'NSFileHandleOperationException', 
reason: '*** -[NSConcreteFileHandle writeData:]: Bad file descriptor'
Copy the code

Now that we’ve covered a few common Objective-C exceptions, let’s look at how to catch them.

2.1.2 Catching Objective-C Exceptions

If the Crash is caused by an Objective-C exception during development, the console of Xcode will output the type, cause and call stack of the exception. Based on this information, we can quickly locate the cause of the exception and repair it.

How do we catch these exceptions, if not during development?

Apple has actually gives us catch exceptions Objective – C API, is NSSetUncaughtExceptionHandler. Let’s take a look at what the official document says:

Sets the top-level error-handling function where you can perform last-minute logging before the program terminates.

This means that after the exception handler is set up through the API, it can be logged at the last moment before the program terminates. This feature is exactly what we want and is relatively simple to use. The code is as follows:

+ (void)registerHandler {
    NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler);
}
Copy the code

Parameter DoraemonUncaughtExceptionHandler here is the exception handler, which are defined as follows:

/ / collapse when the callback function static void DoraemonUncaughtExceptionHandler (NSException * exception) {/ / exception stack information NSArray * stackArray = [exception callStackSymbols]; NSString * reason = [exception reason]; NSString * name = [exception name]; NSString * exceptionInfo = [NSString StringWithFormat: @ "= = = = = = = = uncaughtException exception error reporting = = = = = = = = \ nname: % @ \ nreason: \ n % @ \ ncallStackSymbols: \ n % @", name, reason, [stackArray componentsJoinedByString:@"\n"]]; / / save the Crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog: exceptionInfo fileName: @ "Crash (Uncaught)"]. }Copy the code

As you can see from the above code, when an exception occurs, the exception name, the reason for the exception, and the exception stack information are all available. Once you get the information, save it to the sandbox’s cache directory, where you can view it directly.

To note here is that for an APP, could integrate multiple Crash collection tool, if everyone’s call NSSetUncaughtExceptionHandler to register the exception handler, then after registered will cover off the front of, lead to registered earlier exception handler can’t work normally.

So how do you solve this coverage problem? Actually very simple ideas, before we call NSSetUncaughtExceptionHandler registered exception handling function, get the existing exception handler and preserved. Then, after our handler is executed, we can call the saved handler. In this way, the later registrations will not affect the previous registrations.

The idea has, how should realize? Through Apple’s document can know, there is a before the exception handler API, is NSGetUncaughtExceptionHandler, through which we can get before the exception handler, the code is as follows:

/ / record before the collapse of the callback function static NSUncaughtExceptionHandler * previousUncaughtExceptionHandler = NULL; + (void)registerHandler { // Backup original handler previousUncaughtExceptionHandler = NSGetUncaughtExceptionHandler();  NSSetUncaughtExceptionHandler(&DoraemonUncaughtExceptionHandler); }Copy the code

Before we set up our own, let’s save our existing exception handlers. When handling an exception, after our own exception handler is finished, we need to throw the exception to the previously saved exception handler. The code is as follows:

/ / collapse when the callback function static void DoraemonUncaughtExceptionHandler (NSException * exception) {/ / exception stack information NSArray * stackArray = [exception callStackSymbols]; NSString * reason = [exception reason]; NSString * name = [exception name]; NSString * exceptionInfo = [NSString StringWithFormat: @ "= = = = = = = = uncaughtException exception error reporting = = = = = = = = \ nname: % @ \ nreason: \ n % @ \ ncallStackSymbols: \ n % @", name, reason, [stackArray componentsJoinedByString:@"\n"]]; / / save the Crash log to the sandbox cache directory [DoraemonCrashTool saveCrashLog: exceptionInfo fileName: @ "Crash (Uncaught)"]. / / call before the collapse of the callback function if (previousUncaughtExceptionHandler) {previousUncaughtExceptionHandler (exception); }}Copy the code

At this point, you’re almost done catching objective-C exceptions.

2.2 Mach abnormal

In the last section we looked at Objective-C exceptions. In this section we’ll look at Mach exceptions. What exactly are Mach exceptions? Before we answer that question, let’s take a look at some facts about it.

2.2.1 Concepts related to Mach

The figure above is from Apple’s Mac Technology Overview. For the Kernel and Device Drivers layer, OS X and iOS architecture are generally the same. The kernel part is XNU, and Mach is the microkernel core of XNU.

Mach’s responsibilities include process and thread abstraction, virtual memory management, task scheduling, interprocess communication, and messaging mechanisms.

There are several basic concepts in the Mach microkernel:

  • Task: An object with a set of system resources that allows thread to execute.
  • Thread: The basic unit of execution that owns the context of a task and shares its resources.
  • Port: a set of protected message queues for communication between tasks. Tasks can send/receive data to any port.
  • Message: a collection of typed data objects that can only be sent to port.

The BSD layer is on top of Mach, providing a reliable and more modern API that provides POSIX compatibility.

2.2.2 Mach Exceptions and Unix Signals

Now that we know about Mach, what are Mach exceptions? Here’s a reference to the Explanation of Mach exceptions in The iOS Crash Collection Framework.

The Exception Type item usually contains two elements: Mach Exception and Unix signal.

Mach exceptions: Allows in-process or out-of-process handling, with handlers called through Mach RPC.

Unix signals: Handled only in the process, the handler is always called on the thread where the error occurred.

Mach exceptions are the lowest level kernel-level exceptions defined under < Mach/Exception_types.h >. Each thread, task, and host has an array of exception ports. Part of the Mach API is exposed to the user state. The user state developer 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 converted to the appropriate Unix signal at the Host layer by ux_Exception and posted to the offending thread via ThreadSignal. The POSIX API in iOS is implemented through the BSD layer on top of Mach. As shown below:

For example,Exception Type: EXC_BAD_ACCESS (SIGSEGV)Mach layerEXC_BAD_ACCESSException is converted at the host layer toSIGSEGVThe signal is delivered to the error thread. The following figure shows the conversion from a Mach exception to a Unix signal:

Now that the error thread is finally sent as a signal, you can register a signalHandler to catch the signal:

signal(SIGSEGV,signalHandler);
Copy the code

The Crash event can be caught by capturing Mach anomalies or Unix signals. Here, we use Unix signals to capture Crash events. The main reasons are as follows:

  1. There is no convenient way to catch a Mach exception, and since it will eventually be converted to a signal, we can also catch Crash events by catching a signal.
  2. The conversion of Unix signals was intended to be compatible with the more popular POSIX standard (SUS specification), so that the Mach kernel could be developed compatible with Unix signals without knowing anything about it.

For these reasons, we have chosen a Unix signal-based approach to catch exceptions.

2.2.3 Signal interpretation

There are many types of Unix signals, and detailed definitions can be found in

. Here are some common signals we monitor and what they mean:

  • SIGABRT: Signal generated by calling abort.
  • SIGBUS: indicates an invalid address, including an incorrect alignment of the memory address. Such as accessing a four-word integer whose address is not a multiple of 4. It differs from SIGSEGV in that the latter is triggered by illegal access to a valid storage address (such as access to a non-own storage space or read-only storage space).
  • SIGFPE: Issued when a fatal arithmetic error occurs. This includes not only floating-point errors, but also overflow and all other arithmetic errors such as divisor zero.
  • SIGILL: Illegal instructions were executed. This is usually due to an error in the executable itself or an attempt to execute a data segment. This signal can also be generated when the stack overflows.
  • SIGPIPE: Pipe rupture. This signal is usually generated in interprocess communication, such as when two processes communicate using FIFO(pipe). If the reading pipe is not opened or terminates unexpectedly, the writing process receives the SIGPIPE signal. In addition, for the two processes that use the Socket to communicate, the writing process terminates while writing the Socket.
  • SIGSEGV: Attempts to access unallocated memory, or attempts to write data to a memory address that has no write permission.
  • SIGSYS: Invalid system call.
  • SIGTRAP: Generated by breakpoint instructions or other trap instructions and used by the debugger.

See iOS Exception Catching for more definitions of signals.

2.2.4 Capturing Unix signals

Similar to catching Objective-C exceptions in the previous section, register an exception handler to monitor the signal. The code is as follows:

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

static void DoraemonSignalRegister(int signal) {
    // Register Signal
    struct sigaction action;
    action.sa_sigaction = DoraemonSignalHandler;
    action.sa_flags = SA_NODEFER | SA_SIGINFO;
    sigemptyset(&action.sa_mask);
    sigaction(signal, &action, 0);
}
Copy the code

The DoraemonSignalHandler is an exception handler that monitors signals. It is defined as follows:

static void DoraemonSignalHandler(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"]; For (NSUInteger index = 1; 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 Crash logs to sandbox cache directory [DoraemonCrashTool saveCrashLog:[NSString stringWithString: MSTR] fileName:@"Crash(Signal)"]; DoraemonClearSignalRigister(); }Copy the code

One thing to note here is that the first log line is filtered out. This is because a callback method that registers a crash signal is called by the system and is logged on the call stack, so this line is filtered out to avoid confusion.

As you can see from the above code, the signal name, call stack, thread information, and so on are available when an exception occurs. Once you get the information, save it to the sandbox’s cache directory, where you can view it directly.

Similar to the problem that may occur when capturing Objective-C exceptions, when integrating multiple Crash collection tools, if everyone registers exception handlers for the same signal, the latter will overwrite the earlier ones, causing the earlier ones to fail to work properly.

We can also save the existing exception handler and then call the previously saved exception handler after our exception handler executes. The specific implementation code is as follows:

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; + (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; }}Copy the code

One thing to note here is that we save the previous exception handler for all the signals we are listening for.

When handling an exception, after our own exception handler is finished, we need to throw the exception to the previously saved exception handler. The code is as follows:

static void DoraemonSignalHandler(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"]; For (NSUInteger index = 1; 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 Crash logs to sandbox cache directory [DoraemonCrashTool saveCrashLog:[NSString stringWithString: MSTR] fileName:@"Crash(Signal)"]; DoraemonClearSignalRigister(); // Call the previously crashed callback previousSignalHandler(signal, info, context); }Copy the code

At this point, the capture of Unix signals is basically complete.

2.3 summary

From the previous introduction, I believe you have a certain understanding of how to capture CrashMach ExceptionsA summary of the previous content is shown below:

Three, stepped on the pit

The previous two sections described how to catch Objective-C and Mach exceptions, respectively. This section summarizes some of the problems encountered in the implementation process.

3.1 The problem of catching Objective-C exceptions through Unix signals

You might think that if Unix signals can catch low-level Mach exceptions, why not Objective-C exceptions? It can be caught, but for application-level exceptions like this, you will find that your code is not in the call stack to locate the problem. For example, the code for an objective-C exception such as array out of bounds looks like this:

    NSArray *array = @[@0, @1, @2];
    NSUInteger index = 3;
    NSNumber *value = [array objectAtIndex:index];
Copy the code

If we use Unix signals to capture, the resulting Crash log is as follows:

Signal Exception: Signal SIGABRT was raised. Call Stack: 1 libsystem_platform.dylib 0x00000001a6df0a20 <redacted> + 56 2 libsystem_pthread.dylib 0x00000001a6df6070 <redacted> + 380 3 libsystem_c.dylib 0x00000001a6cd2d78 abort + 140 4 libc++abi.dylib 0x00000001a639cf78 __cxa_bad_cast + 0 5 libc++abi.dylib 0x00000001a639d120 <redacted> + 0 6 libobjc.A.dylib 0x00000001a63b5e48 <redacted> + 124 7 libc++abi.dylib 0x00000001a63a90fc <redacted> + 16 8 libc++abi.dylib 0x00000001a63a8cec __cxa_rethrow + 144 9 libobjc.A.dylib 0x00000001a63b5c10 objc_exception_rethrow + 44 10 CoreFoundation 0x00000001a716e238 CFRunLoopRunSpecific  + 544 11 GraphicsServices 0x00000001a93e5584 GSEventRunModal + 100 12 UIKitCore 0x00000001d4269054 UIApplicationMain + 212 13 DoraemonKitDemo 0x00000001024babf0 main + 124 14 libdyld.dylib 0x00000001a6c2ebb4 <redacted> + 4 threadInfo: <NSThread: 0x280f01400>{number = 1, name = main}Copy the code

As you can see, we cannot locate the problem with the above call stack. Therefore, we need to get the NSException that caused the Crash and get the name, cause, and call stack of the exception so that we can locate the problem exactly.

So, we have adopted in DoraemonKit NSSetUncaughtExceptionHandler capture for abnormal Objective – C.

3.2 Coexistence of two exceptions

Since we caught both Objective-C and Mach exceptions, two Crash logs appear when an Objective-C exception occurs.

A is through setting exception handler NSSetUncaughtExceptionHandler generated log, another is by capturing the Unix signal generated log. The two log, through Unix signal capture log is unable to locate a problem, so we only need NSSetUncaughtExceptionHandler exception handler log can be generated.

So what can be done to prevent the generation of logs that capture Unix signals? The method adopted in DoraemonKit is to actively call exit(0) or kill(getpid(), SIGKILL) to exit the program after the Objective-C exception catches Crash.

3.3 Debugging Problems

Debugging with Xcode allows you to see the flow of the invocation clearly when catching Objective-C exceptions. The test code that caused the Crash was called first, and then the exception handler was entered to capture the Crash log.

However, when you debug the capture of Unix signals, you find that no exception handlers are entered. What’s going on here? Is our capture of Unix signals not working? That’s not the case. Mainly because the Xcode debugger takes precedence over our capture of Unix signals, signals thrown by the system are caught by the Xcode debugger and no longer thrown up to our exception handlers.

Therefore, if we want to debug the capture of Unix signals, we can not debug directly in the Xcode debugger.

  1. Xcode looks at the Device Logs of the Device to get the Logs we printed.
  2. Save Crash directly to the sandbox and view it.

In DoraemonKit, we save Crash directly into the cache directory of the sandbox and view it.

3.4 Coexistence of Multiple Crash Collection Tools

As mentioned earlier, integrating multiple Crash collection tools into the same APP may cause forced overwriting, that is, the newly registered exception handlers will overwrite the previously registered exception handlers.

To ensure that DoraemonKit does not affect other Crash collection tools, the exception handlers that have been registered are saved before they are registered. Then, after our handler has executed, we call the previously saved handler. In this way, DoraemonKit does not affect the Crash collection tool registered before.

3.5 Some special crashes

Even if the process of capturing Crash is fine, there will still be some uncaptured situations. For example, if memory increases rapidly in a short period of time, the APP will be killed by the system. However, the Unix signal at this point is SIGKILL, which is used to terminate the program immediately and cannot be blocked, processed, or ignored. Therefore, the signal cannot be captured. MLeaksFinder: MLeaksFinder Is recommended for iOS memory leak detection

Some Crash can be collected, but there is no code in the log, so it is very difficult to locate. In this case, it is recommended to refer to “How to locate obJ-C wild pointer random Crash” series articles: “How to locate OBJ-C wild pointer random Crash(1) : First improve the wild pointer Crash rate” “How to locate OBJ-C wild pointer random Crash(2) : Obj-c wild pointer random Crash(3) : How to make Crash self-report

Four,

The main purpose of this article is to give you a quick understanding of the Crash viewing tool in DoraemonKit. Due to the time is short, the personal level is limited, if there is any mistake welcome to criticize and correct.

At present, Crash viewing only realizes the most basic functions, which need to be improved continuously in the future. If you have any good ideas or find any bugs in our project, welcome to submit Issues or Pull requests directly on Github. We will deal with them immediately. You can also join our QQ group for communication. Do more perfect.

If you think our project is ok, click on a star.

DoraemonKit project address: github.com/didi/Doraem…

DoraemonKit project screenshot:

5. References

Rambling iOS Crash Collection Framework

IOS Exception Catching

IOS Insider: Crash

IOS Mach Exceptions and Signal

Mach Exceptions

IOS Monitor Programming Crash Monitor

Mach Exceptions