Kstenerud /KSCrash The Ultimate Crash Reporter!

We’ll go into more detail on how the KSCrash framework gets the call stack, how to catch and handle Unix Signals, and how to catch and handle Objective-C exceptions.

Static void* handleExceptions(void* const userData) {static void* handleExceptions(void* const userData) {… }.

handleExceptions

The handleExceptions function defined in the kscrashMonitor_machException. c file is very long and verbose. The core is to wait for exception messages, uninstall our exception port, log exceptions, and write crash reports.

/** Our exception handler thread routine. Wait for an exception message, uninstall our exception port, record the exception information, and write a report. */

static void* handleExceptions(void* const userData) {

    The MachExceptionMessage structure is used to describe a Mach exception message (as defined in ux_exception.c in xnu-1699.22.81).
    Mach header, Mach message content data, exception thread, exception task, network data, exception code, exception code, subcode, etc
    MachExceptionMessage exceptionMessage = {{0}};
    
    The MachReplyMessage structure is used to describe a Reply Mach message (as defined in ux_exception.c in xnu-1699.22.81).
    MachReplyMessage replyMessage = {{0}};
    
    // The UUID generated earlier, which represents the unique EVnet ID
    char* eventID = g_primaryEventID;
    
    // Get the current thread name
    const char* threadName = (const char*) userData;
    pthread_setname_np(threadName);
    
    // Suspend (thread_suspend) if the current thread is a secondary thread for Mach exception handling.
    if (threadName == kThreadSecondary) {
        KSLOG_DEBUG("This is the secondary thread. Suspending.");
        
        thread_suspend((thread_t)ksthread_self());
        eventID = g_secondaryEventID;
    }

    for(;;) {
        KSLOG_DEBUG("Waiting for mach exception");

        // Wait for a message.
        
        The mach_msg function blocks here, waiting for the kernel to send a Mach exception message to the g_exceptionPort when an exception occurs
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0.sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        if (kr == KERN_SUCCESS) {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s".mach_error_string(kr));
    }
    
    // Catch Mach exception codes and subcodes
    KSLOG_DEBUG("Trapped mach exception code 0x%llx, subcode 0x%llx", exceptionMessage.code[0], exceptionMessage.code[1]);
    
    if (g_isEnabled) {
        thread_act_array_t threads = NULL;
        mach_msg_type_number_t numThreads = 0;
        
        // Suspend all non-current threads of the current task as well as reserved threads in g_reservedThreads
        ksmc_suspendEnvironment(&threads, &numThreads);
        
        // Static global variables that record custom handling of Mach exceptions
        g_isHandlingCrash = true;
        
        // Notify that a fatal exception was caught
        kscm_notifyFatalExceptionCaptured(true);
        
        // The exception handler has been installed, continue with exception handling...
        KSLOG_DEBUG("Exception handler is installed. Continuing exception handling.");


        // Switch to the secondary thread if necessary, or uninstall the handler to avoid a death loop.
        // If necessary, switch to the worker thread or unload the handler to avoid the death loop.
        
        // Restore the Mach exception handler thread if it is the main thread
        if (ksthread_self() == g_primaryMachThread) {
        
            KSLOG_DEBUG("This is the primary exception thread. Activating secondary thread."); j// TODO: This was put here to avoid a freeze. Does secondary thread ever fire?
            restoreExceptionPorts(a);if (thread_resume(g_secondaryMachThread) ! = KERN_SUCCESS) {KSLOG_DEBUG("Could not activate secondary thread. Restoring original exception ports."); }}else {
            KSLOG_DEBUG("This is the secondary exception thread.");// Restoring original exception ports.");
// restoreExceptionPorts();
        }

        // Fill out crash information
        KSLOG_DEBUG("Fetching machine state.");
        
        // KSMC_NEW_CONTEXT(machineContext) equivalent to:
        // Create temporary variable ksmc_machineContext_storage is a char array of length that can hold MachineContext data (a KSMachineContext structure).
        // Then create a pointer to KSMachineContext and point it to ksmc_machineContext_storage.
        // char ksmc_machineContext_storage[ksmc_contextSize()];
        // struct KSMachineContext* machineContext = (struct KSMachineContext*)ksmc_machineContext_storage;
        
        KSMC_NEW_CONTEXT(machineContext);
        
        // KSCrash_MonitorContext is a structure with many member variables. It records a lot of information internally, such as crash type information/APP state at crash time/system information...
        G_monitorContext is a static global variable of this type: static KSCrash_MonitorContext g_monitorContext,
        KSCrash_MonitorContext* crashContext = &g_monitorContext;
        
        // offendingMachineContext is the machine context of an event
        crashContext->offendingMachineContext = machineContext;
        
        // static KSStackCursor g_stackCursor; G_stackCursor is a static global variable, KSStackCursor is a structure that describes the stack trace.
        // The most important is: cursor->symbolicate = kssymbolicator_symbolicate; Symbolicate in the KSStackCursor structure is a function pointer to a function that will attempt to symbolise the current address,
        // Used to fill in fields in the nested stackEntry structure defined in the KSStackCursor structure.
        // kssc_initCursor is a stackEntry structure that is used to describe the corresponding symbol information of a stack trace.
        kssc_initCursor(&g_stackCursor, NULL.NULL);
        
        // Get the context information of the thread in which the Mach exception occurred
        if (ksmc_getContextForThread(exceptionMessage.thread.name, machineContext, true)) {
            
            // Initialize g_stackCursor, a static global variable, based on the obtained context information
            kssc_initWithMachineContext(&g_stackCursor, KSSC_MAX_STACK_DEPTH, machineContext);
            
            // faultAddress instructionAddress
            KSLOG_TRACE("Fault address %p, instruction address %p".kscpu_faultAddress(machineContext), kscpu_instructionAddress(machineContext));
            
            // Assign the faultAddress attribute based on the exception type
            if (exceptionMessage.exception == EXC_BAD_ACCESS) {
                // return context->machineContext.__es.__faultvaddr; I don't understand this trip
                crashContext->faultAddress = kscpu_faultAddress(machineContext);
            } else {
                // return context->machineContext.__ss.__rip; I don't understand this trip
                crashContext->faultAddress = kscpu_instructionAddress(machineContext); }}KSLOG_DEBUG("Filling out context.");
        
        // Crash context information
        crashContext->crashType = KSCrashMonitorTypeMachException;
        crashContext->eventID = eventID;
        crashContext->registersAreValid = true;
        
        // Mach exception type, code, subcode
        crashContext->mach.type = exceptionMessage.exception;
        crashContext->mach.code = exceptionMessage.code[0] & (int64_t)MACH_ERROR_CODE_MASK;
        crashContext->mach.subcode = exceptionMessage.code[1] & (int64_t)MACH_ERROR_CODE_MASK;
        
        // If it is a Stack overflow Mach exception
        if (crashContext->mach.code == KERN_PROTECTION_FAILURE && crashContext->isStackOverflow) {
            // A stack overflow should return KERN_INVALID_ADDRESS, but
            // when a stack blasts through the guard pages at the top of the stack,
            // it generates KERN_PROTECTION_FAILURE. Correct for this.
            
            // Stack overflow should return KERN_INVALID_ADDRESS, but when the stack passes the guard page at the top of the stack, it generates KERN_PROTECTION_FAILURE. Perform this operation correctly.
            crashContext->mach.code = KERN_INVALID_ADDRESS;
        }
        
        Convert to the corresponding Unix Signal according to the Mach exception type
        crashContext->signal.signum = signalForMachException(crashContext->mach.type, crashContext->mach.code);
        // Function stack "probe"
        crashContext->stackCursor = &g_stackCursor;
        
        // Record the exception type
        kscm_handleException(crashContext);

        KSLOG_DEBUG("Crash handling complete. Restoring original handlers.");
        g_isHandlingCrash = false;
        
        // Restore the thread
        ksmc_resumeEnvironment(threads, numThreads);
    }

    KSLOG_DEBUG("Replying to mach exception message.");
    
    // Send a reply saying "I didn't handle this exception".
    // Send a reply stating "I did not handle this exception". After we have finished processing ourselves, we need to throw the Mach exception message
    
    replyMessage.header = exceptionMessage.header;
    replyMessage.NDR = exceptionMessage.NDR;
    replyMessage.returnCode = KERN_FAILURE;

    mach_msg(&replyMessage.header,
             MACH_SEND_MSG,
             sizeof(replyMessage),
             0,
             MACH_PORT_NULL,
             MACH_MSG_TIMEOUT_NONE,
             MACH_PORT_NULL);

    return NULL;
}
Copy the code

kssc_initCursor

The prefix SC in KSSC is the acronym of Stack Cursor. So what is a Stack Cursor?

When the kssc_initCursor function is called from within the handleExceptions function above, the cursor argument passes a static global variable static KSStackCursor g_stackCursor, This variable is initialized during the execution of the kssc_initCursor function, and g_stackCursor is used as a subsequent Stack Cursor.

Focusing on the ksstackCursor.h /.c pair of files, it’s actually pretty clear:

  1. The stackEntry structure of KSStackCursor describes the contents of a stack frame (function call) in a function stack. The internal member variables include: Stack trace The current address, the name of the image corresponding to the address, the start address of the image, the name of the symbol closest to the current address (if any), and the address of the symbol closest to the current address (both addresses).
  2. The state structure then describes the current state of the traversal function stack, with internal member variables including: the current depth of the traversal function stack (based on 1), and whether the traversal stack has been abandoned.
  3. Further down are three function Pointers: ResetCursor Resets the Stack Cursor (i.e. sets all member variables of the above stackEntry and state structures to 0/false/NULL), advanceCursor advances the Stack Cursor to the next Stack Entry, symbolicate attempts to symbolize the current address and record the value in member variables in the stackEntry structure.
  4. The final context is an array of void Pointers of length 100 that stores the internal information of the context.

We can see that some Stack cursors are used for function Stack traceback.

kssc_initCursor(&g_stackCursor, NULL.NULL);
Copy the code
void kssc_initCursor(KSStackCursor *cursor,
                     void (*resetCursor)(KSStackCursor*),
                     bool (*advanceCursor)(KSStackCursor*)) {
    // Assign the symbolicpointer to kssymbolicator_symbolicate by default
    cursor->symbolicate = kssymbolicator_symbolicate;
    
    // The default is g_advanceCursorcursor->advanceCursor = advanceCursor ! =NULL ? advanceCursor : g_advanceCursor;
    
    // The default is kssc_resetCursorcursor->resetCursor = resetCursor ! =NULL ? resetCursor : kssc_resetCursor;
    
    // Call the resetCursor function to set the cursor member variables to 0, false, and NULL
    cursor->resetCursor(cursor);
}
Copy the code

The kssc_initCursor function kssymbolicator_symbolicate function is the most important function in the list above.

kssymbolicator_symbolicate

The kssymbolicator_symbolicate function is used to symbolize the Stack Cursor. Does Dl_info look familiar to us, as well as its corresponding dladdr function, which we studied in detail when we studied Mach-o and Fishhook? Save data to specified segments and sections

The function dladdr(const void *, Dl_info *) is used to obtain symbolic information about an address. The dladdr is used to obtain symbolic information about the address closest to the definition. Dladdr determines whether the specified address is in a load module (executable or shared library) in the address space that makes up the process. If an address lies between the base address on which the loaded module is mapped and the highest virtual address mapped for the loaded module (including both ends), the address is considered to be in the range of the loaded module. If a loaded module meets this condition, its dynamic symbol table is searched for the symbol closest to the specified address. The nearest symbol is the symbol whose value is equal to or closest but less than the specified address.

The Dl_info * argument in the dlADDR function is a pointer to the Dl_info structure that must be created and assigned by the user. If the address specified by the dlADDR function is within the range of one of the loading modules, The value of the Dl_info structure member variable is set by the dlADDR function.

We first get the address of the description function of the NSArray class by the class_getMethodImplementation function. The dlADDR function is then used to obtain the symbolic information of the description function.

#import <dlfcn.h>
#include <objc/objc.h>
#include <objc/runtime.h>
#include <stdio.h>

int main(int argc, const char * argv[]) {
    
    Dl_info info;
    IMP imp = class_getMethodImplementation(objc_getClass("NSArray"), sel_registerName("description"));
    
    printf("✳ ️ ✳ ️ ✳ ️ pointer p \ % n", imp);
    
    if (dladdr((const void *)(imp), &info)) {
        printf("✳ ️ ✳ ️ ✳ ️ dli_fname: % s \ n", info.dli_fname);
        printf("✳ ️ ✳ ️ ✳ ️ dli_fbase: % p \ n", info.dli_fbase);
        printf("✳ ️ ✳ ️ ✳ ️ dli_sname: % s \ n", info.dli_sname);
        printf("✳ ️ ✳ ️ ✳ ️ dli_saddr: % p \ n", info.dli_saddr);
    } else {
        printf("error: can't find that symbol.\n");
    }
    
    return 0;
}

// ⬇️ The console prints the following information:✳ ️ ✳ ️ ✳ ️ pointer0x7fff203f44dd✳ ️ ✳ ️ ✳ ️ dli_fname: / System/Library/Frameworks/CoreFoundation framework Versions/A/CoreFoundation ✳ ️ ✳ ️ ✳ ️ dli_fbase:0x7fff20387000✳ ️ ✳ ️ ✳ ️ dli_sname: - [NSArray description] ✳ ️ ✳ ️ ✳ ️ dli_saddr:0x7fff203f44dd
Copy the code

Member variables of the Dl_info structure:

  • Dli_fname A char pointer to the path of the loaded module (image) containing the specified address.
  • Dli_fbase loads a handle (address) to the module (image) that can be used as the first argument to DLSYM.
  • Dli_sname A char pointer to the name of the symbol closest to the specified address, either with the same address or with the lowest address.
  • Dli_saddr specifies the actual address of the symbol closest to the address.

The dlADDR function returns a value of type int. If the specified address is not in the range of one of the loaded modules, 0 is returned. The Dl_info structure member variables are not modified, otherwise, a non-zero value is returned and the Dl_info structure member variables are set.

/* * Structure filled in by dladdr(). */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

extern int dladdr(const void *, Dl_info *);
Copy the code
bool kssymbolicator_symbolicate(KSStackCursor *cursor) {
    // Record the symbolic information of the specified address
    Dl_info symbolsBuffer;
    
    // Return a non-zero value if cursor->stackEntry.address is within the range of a loaded module
    if (ksdl_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(cursor->stackEntry.address), &symbolsBuffer)) {
    
        // Find the symbol information for cursor->stackEntry.address, assign the value to each member variable of cursor->stackEntry
        cursor->stackEntry.imageAddress = (uintptr_t)symbolsBuffer.dli_fbase;
        cursor->stackEntry.imageName = symbolsBuffer.dli_fname;
        cursor->stackEntry.symbolAddress = (uintptr_t)symbolsBuffer.dli_saddr;
        cursor->stackEntry.symbolName = symbolsBuffer.dli_sname;
        
        return true;
    }
    
    // Set to 0 if not found
    cursor->stackEntry.imageAddress = 0;
    cursor->stackEntry.imageName = 0;
    cursor->stackEntry.symbolAddress = 0;
    cursor->stackEntry.symbolName = 0;
    
    return false;
}
Copy the code

ksdl_dladdr

Ksdl_dladdr does not encapsulate dladdr. Ksdl_dladdr does not encapsulate dladdr. Directly find the address of the image -> find LC_SYMTAB segment -> compare symbol address.

The ksdl_dlADDR function is an asynchronous secure version of DLADDR. This method searches the Dynamic Loader for information about any image that contains the specified address. It may not be able to find the information completely successfully, in which case any fields of the info parameter will be set to NULL if it can’t find the symbol. Unlike DLADDR, this method does not use locks and does not call asynchronous unsafe functions.

Ksdl_dladdr (Mach Object File Format) {ipA (Mach Object File Format);

Each line of code below is clearly commented.

bool ksdl_dladdr(const uintptr_t address, Dl_info* const info) {
    info->dli_fname = NULL;
    info->dli_fbase = NULL;
    info->dli_sname = NULL;
    info->dli_saddr = NULL;
    
    UINT_MAX (#define UINT_MAX (__INT_MAX__ *2U +1U))
    Load_command = LC_SEGMENT/LC_SEGMENT_64 load_command = LC_SEGMENT/LC_SEGMENT_64 load_command
    // Then compare the address of the load_command and return the index of the corresponding image.
    const uint32_t idx = imageIndexContainingAddress(address);
    if (idx == UINT_MAX) {
        return false;
    }
    
    // Get the address of the header of the image to which address belongs
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // Get slide of image virtual address
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    
    const uintptr_t segmentBase = segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    
    if (segmentBase == 0) {
        return false;
    }

    // Assign a value to the dli_fname member variable to record the path of the image containing address
    info->dli_fname = _dyld_get_image_name(idx);
    // Assign a value to the dli_fbase member variable. Record the starting address of the image to which address belongs
    info->dli_fbase = (void*)header;

    // Find symbol tables and get whichever symbol is closest to the address.
    // Look up the symbol table and get the symbol closest to the address.
    
    // typedef struct nlist_64 nlist_t; 
    // nlist_64 is a structure that describes the structure of each entry in the symbol Table, such as the index of the string containing the symbol name, the symbol type, the section number, the symbol offset value, and so on.
    // bestMatch temporary variable records the symbol closest to address
    const nlist_t* bestMatch = NULL;
    uintptr_t bestDistance = ULONG_MAX;
    
    // Get the address of the first load command after the header
    uintptr_t cmdPtr = firstCmdAfterHeader(header);
    
    if (cmdPtr == 0) { return false; }
    
    // Start iterating through load_command
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // find load_command of type LC_SYMTAB, which is where the symbol table is located
        if (loadCmd->cmd == LC_SYMTAB) {
            // The symtab_command contains The offsets and sizes of The link-edit 4.3bsd "stab" style symbol table information as described in the header files 
      
        and 
       
        .
       
      
            
            // Convert the load_command pointer to the symtab_command pointer
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            
            // Locate the symbol table and string table according to the offset distance recorded in the load command
            const nlist_t* symbolTable = (nlist_t*)(segmentBase + symtabCmd->symoff);
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;

            // symtabCmd->nsyms is the number of symbol table entries
            // Iterate through the symbol table entries to find the symbol closest to address and record it in bestMatch
            for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
                // If n_value is 0, the symbol refers to an external object.
                // If n_value is 0, the symbol references an external object.
                
                if(symbolTable[iSym].n_value ! =0) {
                    
                    // Get the address of the symbol
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    // Get the distance between symbol and address
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    
                    if((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) {
                        // Record the currently matched symbol
                        bestMatch = symbolTable + iSym;
                        // Update the nearest distancebestDistance = currentDistance; }}}// When a matching symbol is found, each value of the info parameter is updated to record information about the matching symbol
            if(bestMatch ! =NULL) {
                // Record the actual address of the symbol closest to address
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                
                // The build does not contain the symbol table, cannot get the corresponding symbol name
                if (bestMatch->n_desc == 16) {
                    // This image has been stripped. The name is meaningless, and almost certainly resolves to "_mh_execute_header"
                    info->dli_sname = NULL;
                } else {
                    // Find the name of the symbol in the string table
                    info->dli_sname = (char((*)intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                    if (*info->dli_sname == '_') { info->dli_sname++; }}break; }}// cmdPtr pointer is offset to the next load_command
        cmdPtr += loadCmd->cmdsize;
    }
    
    return true;
}
Copy the code

This concludes the ksdL_DLADDR function, which should be particularly clear if you’re familiar with Mach-O, and kssymbolicator_symbolicate as a public function for function stack traceback symbolization in case of any type of exception.

Although I can understand Mach-O based on my current knowledge, I am still confused about function stack backtracking and am ready to learn more about the function call stack.

KSCrash’s handling of different types of exceptions is centralized in the Monitors folder and won’t be listed here.

The KSCrash framework won’t stop here, so let’s focus on ios-crash-dump-analysis-book/zh. ⛽ ️ ⛽ ️

Refer to the link

Reference link :🔗

  • NSThead and kernel thread conversion
  • A brief introduction to function call stack
  • Dynamic link library load pickups &dladdr function used
  • The difference between thread_local and __thread
  • ios-crash-dump-analysis-book
  • ios-crash-dump-analysis-book/zh
  • Obtain iOS Crash/ Crash/ exception stack