In the last article, we analyzed the application loading process, and we are already familiar with the application loading process. We have already seen the flow of map_images and load_images when we call the dyld library function _dyLD_objC_notify_register from the _objc_init function in ObjC source code. Load_images is actually a call to OC and its class overridden load method, but what does the call to map_images do? We don’t know yet, so it’s important to explore the code logic in map_images. Let’s take a look!

This section focuses on

Print and use ObjC environment variables

ObjC exception handling

Read_Images function process exploration

1. Parsing the _objc_init function code

Before exploring the logic of the map_images function code, let’s first explore what the function calls in _objc_init do. Perhaps related to the map_images function call, the code looks like this:

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    runtime_init();
    exception_init();
#if __OBJC2__
    cache_t::init();
#endif
    _imp_implementationWithBlock_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);

#if __OBJC2__
    didCallDyldNotifyRegister = true;
#endif
}
Copy the code

The _objc_init function first defines a local static variable. If it is initialized, the next call to _objc_init will return it directly, ensuring that all other functions called in _objc_init will only be executed once.

1.1 environ_init function

Let’s first look at what happens in the environ_init function. The code looks like this:

/*********************************************************************** * environ_init * Read environment variables that affect the runtime. * Also print environment variable help, if requested. **********************************************************************/ void environ_init(void) { if (issetugid()) { // All environment variables are silently ignored when setuid or setgid // This includes OBJC_HELP and OBJC_PRINT_OPTIONS themselves. return; } // Turn off autorelease LRU coalescing by default for apps linked against // older SDKs. LRU coalescing can reorder releases and certain older apps // are accidentally relying on the ordering. // rdar://problem/63886091 // if (! dyld_program_sdk_at_least(dyld_fall_2020_os_versions)) // DisableAutoreleaseCoalescingLRU = true; bool PrintHelp = false; bool PrintOptions = false; bool maybeMallocDebugging = false; // Scan environ[] directly instead of calling getenv() a lot. // This optimizes the case where none are set. for (char **p = *_NSGetEnviron(); *p ! = nil; p++) { if (0 == strncmp(*p, "Malloc", 6) || 0 == strncmp(*p, "DYLD", 4) || 0 == strncmp(*p, "NSZombiesEnabled", 16)) { maybeMallocDebugging = true; } if (0 ! = strncmp(*p, "OBJC_", 5)) continue; if (0 == strncmp(*p, "OBJC_HELP=", 10)) { PrintHelp = true; continue; } if (0 == strncmp(*p, "OBJC_PRINT_OPTIONS=", 19)) { PrintOptions = true; continue; } if (0 == strncmp(*p, "OBJC_DEBUG_POOL_DEPTH=", 22)) { SetPageCountWarning(*p + 22); continue; } const char *value = strchr(*p, '='); if (! *value) continue; value++; for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) { const option_t *opt = &Settings[i]; if ((size_t)(value - *p) == 1+opt->envlen && 0 == strncmp(*p, opt->env, opt->envlen)) { *opt->var = (0 == strcmp(value, "YES")); break; } } } // Special case: enable some autorelease pool debugging // when some malloc debugging is enabled // and OBJC_DEBUG_POOL_ALLOCATION is not  set to something other than NO. if (maybeMallocDebugging) { const char *insert = getenv("DYLD_INSERT_LIBRARIES"); const char *zombie = getenv("NSZombiesEnabled"); const char *pooldebug = getenv("OBJC_DEBUG_POOL_ALLOCATION"); if ((getenv("MallocStackLogging") || getenv("MallocStackLoggingNoCompact") || (zombie && (*zombie == 'Y' || *zombie == 'y')) || (insert && strstr(insert, "libgmalloc"))) && (! pooldebug || 0 == strcmp(pooldebug, "YES"))) { DebugPoolAllocation = true; } } // if (! os_feature_enabled_simple(objc4, preoptimizedCaches, true)) { // DisablePreoptCaches = true; // } // Print OBJC_HELP and OBJC_PRINT_OPTIONS output. if (PrintHelp || PrintOptions) { if (PrintHelp) { _objc_inform("Objective-C runtime debugging. Set variable=YES to enable."); _objc_inform("OBJC_HELP: describe available environment variables"); if (PrintOptions) { _objc_inform("OBJC_HELP is set"); } _objc_inform("OBJC_PRINT_OPTIONS: list which options are set"); } if (PrintOptions) { _objc_inform("OBJC_PRINT_OPTIONS is set"); } for (size_t i = 0; i < sizeof(Settings)/sizeof(Settings[0]); i++) { const option_t *opt = &Settings[i]; if (PrintHelp) _objc_inform("%s: %s", opt->env, opt->help); if (PrintOptions && *opt->var) _objc_inform("%s is set", opt->env); }}}Copy the code

We know from the comment that this function is used to initialize environment variables, that it can read environment variables that affect the runtime, and that it can print environment variable help if it sends related requests, but we don’t know how to send these requests. You can then copy the following snippet of code (i.e. the code from the last for loop with the if statement removed) outside the if statement, as shown in the figure below:

Compile and run the ObjC source code, and you’ll see the following print:

objc[9899]: OBJC_PRINT_IMAGES: log image and library names as they are loaded
objc[9899]: OBJC_PRINT_IMAGES is set
objc[9899]: OBJC_PRINT_IMAGE_TIMES: measure duration of image loading steps
objc[9899]: OBJC_PRINT_IMAGE_TIMES is set
objc[9899]: OBJC_PRINT_LOAD_METHODS: log calls to class and category +load methods
objc[9899]: OBJC_PRINT_LOAD_METHODS is set
objc[9899]: OBJC_PRINT_INITIALIZE_METHODS: log calls to class +initialize methods
objc[9899]: OBJC_PRINT_INITIALIZE_METHODS is set
objc[9899]: OBJC_PRINT_RESOLVED_METHODS: log methods created by +resolveClassMethod: and +resolveInstanceMethod:
objc[9899]: OBJC_PRINT_RESOLVED_METHODS is set
objc[9899]: OBJC_PRINT_CLASS_SETUP: log progress of class and category setup
objc[9899]: OBJC_PRINT_CLASS_SETUP is set
objc[9899]: OBJC_PRINT_PROTOCOL_SETUP: log progress of protocol setup
objc[9899]: OBJC_PRINT_PROTOCOL_SETUP is set
objc[9899]: OBJC_PRINT_IVAR_SETUP: log processing of non-fragile ivars
objc[9899]: OBJC_PRINT_IVAR_SETUP is set
objc[9899]: OBJC_PRINT_VTABLE_SETUP: log processing of class vtables
objc[9899]: OBJC_PRINT_VTABLE_SETUP is set
objc[9899]: OBJC_PRINT_VTABLE_IMAGES: print vtable images showing overridden methods
objc[9899]: OBJC_PRINT_VTABLE_IMAGES is set
objc[9899]: OBJC_PRINT_CACHE_SETUP: log processing of method caches
objc[9899]: OBJC_PRINT_CACHE_SETUP is set
objc[9899]: OBJC_PRINT_FUTURE_CLASSES: log use of future classes for toll-free bridging
objc[9899]: OBJC_PRINT_FUTURE_CLASSES is set
objc[9899]: OBJC_PRINT_PREOPTIMIZATION: log preoptimization courtesy of dyld shared cache
objc[9899]: OBJC_PRINT_PREOPTIMIZATION is set
objc[9899]: OBJC_PRINT_CXX_CTORS: log calls to C++ ctors and dtors for instance variables
objc[9899]: OBJC_PRINT_CXX_CTORS is set
objc[9899]: OBJC_PRINT_EXCEPTIONS: log exception handling
objc[9899]: OBJC_PRINT_EXCEPTIONS is set
objc[9899]: OBJC_PRINT_EXCEPTION_THROW: log backtrace of every objc_exception_throw()
objc[9899]: OBJC_PRINT_EXCEPTION_THROW is set
objc[9899]: OBJC_PRINT_ALT_HANDLERS: log processing of exception alt handlers
objc[9899]: OBJC_PRINT_ALT_HANDLERS is set
objc[9899]: OBJC_PRINT_REPLACED_METHODS: log methods replaced by category implementations
objc[9899]: OBJC_PRINT_REPLACED_METHODS is set
objc[9899]: OBJC_PRINT_DEPRECATION_WARNINGS: warn about calls to deprecated runtime functions
objc[9899]: OBJC_PRINT_DEPRECATION_WARNINGS is set
objc[9899]: OBJC_PRINT_POOL_HIGHWATER: log high-water marks for autorelease pools
objc[9899]: OBJC_PRINT_POOL_HIGHWATER is set
objc[9899]: OBJC_PRINT_CUSTOM_CORE: log classes with custom core methods
objc[9899]: OBJC_PRINT_CUSTOM_CORE is set
objc[9899]: OBJC_PRINT_CUSTOM_RR: log classes with custom retain/release methods
objc[9899]: OBJC_PRINT_CUSTOM_RR is set
objc[9899]: OBJC_PRINT_CUSTOM_AWZ: log classes with custom allocWithZone methods
objc[9899]: OBJC_PRINT_CUSTOM_AWZ is set
objc[9899]: OBJC_PRINT_RAW_ISA: log classes that require raw pointer isa fields
objc[9899]: OBJC_PRINT_RAW_ISA is set
objc[9899]: OBJC_DEBUG_UNLOAD: warn about poorly-behaving bundles when unloaded
objc[9899]: OBJC_DEBUG_UNLOAD is set
objc[9899]: OBJC_DEBUG_FRAGILE_SUPERCLASSES: warn about subclasses that may have been broken by subsequent changes to superclasses
objc[9899]: OBJC_DEBUG_FRAGILE_SUPERCLASSES is set
objc[9899]: OBJC_DEBUG_NIL_SYNC: warn about @synchronized(nil), which does no synchronization
objc[9899]: OBJC_DEBUG_NIL_SYNC is set
objc[9899]: OBJC_DEBUG_NONFRAGILE_IVARS: capriciously rearrange non-fragile ivars
objc[9899]: OBJC_DEBUG_NONFRAGILE_IVARS is set
objc[9899]: OBJC_DEBUG_ALT_HANDLERS: record more info about bad alt handler use
objc[9899]: OBJC_DEBUG_ALT_HANDLERS is set
objc[9899]: OBJC_DEBUG_MISSING_POOLS: warn about autorelease with no pool in place, which may be a leak
objc[9899]: OBJC_DEBUG_MISSING_POOLS is set
objc[9899]: OBJC_DEBUG_POOL_ALLOCATION: halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
objc[9899]: OBJC_DEBUG_POOL_ALLOCATION is set
objc[9899]: OBJC_DEBUG_DUPLICATE_CLASSES: halt when multiple classes with the same name are present
objc[9899]: OBJC_DEBUG_DUPLICATE_CLASSES is set
objc[9899]: OBJC_DEBUG_DONT_CRASH: halt the process by exiting instead of crashing
objc[9899]: OBJC_DEBUG_DONT_CRASH is set
objc[9899]: OBJC_DEBUG_POOL_DEPTH: log fault when at least a set number of autorelease pages has been allocated
objc[9899]: OBJC_DEBUG_POOL_DEPTH is set
objc[9899]: OBJC_DISABLE_VTABLES: disable vtable dispatch
objc[9899]: OBJC_DISABLE_VTABLES is set
objc[9899]: OBJC_DISABLE_PREOPTIMIZATION: disable preoptimization courtesy of dyld shared cache
objc[9899]: OBJC_DISABLE_PREOPTIMIZATION is set
objc[9899]: OBJC_DISABLE_TAGGED_POINTERS: disable tagged pointer optimization of NSNumber et al.
objc[9899]: OBJC_DISABLE_TAGGED_POINTERS is set
objc[9899]: OBJC_DISABLE_TAG_OBFUSCATION: disable obfuscation of tagged pointers
objc[9899]: OBJC_DISABLE_TAG_OBFUSCATION is set
objc[9899]: OBJC_DISABLE_NONPOINTER_ISA: disable non-pointer isa fields
objc[9899]: OBJC_DISABLE_NONPOINTER_ISA is set
objc[9899]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY: disable safety checks for +initialize after fork
objc[9899]: OBJC_DISABLE_INITIALIZE_FORK_SAFETY is set
objc[9899]: OBJC_DISABLE_FAULTS: disable os faults
objc[9899]: OBJC_DISABLE_FAULTS is set
objc[9899]: OBJC_DISABLE_PREOPTIMIZED_CACHES: disable preoptimized caches
objc[9899]: OBJC_DISABLE_PREOPTIMIZED_CACHES is set
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING: disable coalescing of autorelease pool pointers
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING is set
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU: disable coalescing of autorelease pool pointers using look back N strategy
objc[9899]: OBJC_DISABLE_AUTORELEASE_COALESCING_LRU is set
Copy the code

We can see the names of these environment variables by printing them out. If you set these environment variables in the main project, you can follow the prompts to get relevant information or change some data structure, for example: Let’s randomly create an iOS project that creates a Person class that inherits from NSObject, implements the class method load in the Person class, Set the two environment variables OBJC_DISABLE_NONPOINTER_ISA (whether NONPOINTER_ISA is disabled) to NO, and OBJC_PRINT_LOAD_METHODS (whether to print out the information when the LOAD method is called) to NO, as shown in the figure below:

In the ViewController’s ViewDidLoad method, write code with a breakpoint, link to the real machine, run the program, print the ISA for the P object, find the isa is NONPOINTER_ISA, pass the breakpoint, the program crashes, as shown below:

Set OBJC_DISABLE_NONPOINTER_ISA and OBJC_PRINT_LOAD_METHODS to YES, as shown in the figure below:

Compile and run the program and print isa for the P object, as shown in the figure below:

The environment variable OBJC_PRINT_LOAD_METHODS outputs information about all classes that call the load class method, helping us figure out which classes in the main program and framework call load. You can also use the terminal command (export OBJC_HELP=1) to print out the environment variables in ObjC, as shown in the figure below:

1.2 Environ_init and static_init

Tls_init () : binds the thread key (such as the per-thread data destructor), which looks like this:

void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
    pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
    _objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
Copy the code

Static_init () : run the C++ static constructor. Before dyld calls our static constructor, libc calls _objc_init(), so we have to call it ourselves. The code looks like this:

/*********************************************************************** * static_init * Run C++ static constructor functions. * libc calls _objc_init() before dyld would call our static constructors, * so we have to do it ourselves. **********************************************************************/ static void static_init() { size_t count; auto inits = getLibobjcInitializers(&_mh_dylib_header, &count); for (size_t i = 0; i < count; i++) { inits[i](); } auto offsets = getLibobjcInitializerOffsets(&_mh_dylib_header, &count); for (size_t i = 0; i < count; i++) { UnsignedInitializer init(offsets[i]); init(); }}Copy the code

1.3 Exception_init function

Exception_init () : initializes liBOBJC’s exception_init() system with the following code:

/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
    old_terminate = std::set_terminate(&_objc_terminate);
}
Copy the code

The code for the _objc_terminate function looks like this:

/***********************************************************************
* _objc_terminate
* Custom std::terminate handler.
*
* The uncaught exception callback is implemented as a std::terminate handler. 
* 1. Check if there's an active exception
* 2. If so, check if it's an Objective-C exception
* 3. If so, call our registered callback with the object.
* 4. Finally, call the previous terminate handler.
**********************************************************************/
static void (*old_terminate)(void) = nil;
static void _objc_terminate(void)
{
    if (PrintExceptions) {
        _objc_inform("EXCEPTIONS: terminating");
    }

    if (! __cxa_current_exception_type()) {
        // No current exception.
        (*old_terminate)();
    }
    else {
        // There is a current exception. Check if it's an objc exception.
        @try {
            __cxa_rethrow();
        } @catch (id e) {
            // It's an objc object. Call Foundation's handler, if any.
            (*uncaught_handler)((id)e);
            (*old_terminate)();
        } @catch (...) {
            // It's not an objc object. Continue to C++ terminate.
            (*old_terminate)();
        }
    }
}
Copy the code

We know from the code and its comments that if an OC exception occurs, the uncaught_handler function will be called to handle the exception. Therefore, a global search for uncaught_handler will find the following code:

/***********************************************************************
* _objc_default_uncaught_exception_handler
* Default uncaught exception handler. Expected to be overridden by Foundation.
**********************************************************************/
static void _objc_default_uncaught_exception_handler(id exception)
{
}
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
Copy the code

If we don’t override uncaught_handler using a function in the Foundation library, It will be assigned by default to the address of the static function _objc_default_uncaught_Exception_handler, so let’s do a global search to see if there is no function assigned to uncaught_handler, so we can find the relevant function call, Find the relevant code as follows:


/***********************************************************************
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions. 
* Returns the previous handler. 
**********************************************************************/
objc_uncaught_exception_handler 
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
    objc_uncaught_exception_handler result = uncaught_handler;
    uncaught_handler = fn;
    return result;
}
Copy the code

We will be in the objc_setUncaughtExceptionHandler function and assign uncaught_handler this handle, see objc_setUncaughtExceptionHandler this function, We can naturally think of OC NSSetUncaughtExceptionHandler in this function, And objc_setUncaughtExceptionHandler this function will be lower NSSetUncaughtExceptionHandler function implementation?

First we write the following code in the viewController.m in the iOS project we randomly created:

#import "ViewController.h" @interface ViewController () @property (nonatomic, strong) NSArray *dataArray; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; Self. DataArray = @[@]; self. DataArray = @[@]; self. DataArray = @[@]; self. } - (IBAction)buttonClick:(id)sender { NSLog(@"%@", self.dataArray[5]); } @endCopy the code

If we click the button, we will throw an out-of-bounds exception and crash, as shown below:

However, we can also catch the information about handling these exceptions ourselves by creating a class called UncaughtExceptionHandle to catch the exception, as shown below:

The code looks like this:

//UncaughtExceptionHandle.h 文件中代码如下
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface UncaughtExceptionHandle : NSObject

+ (void)installUncaughtSignalExceptionHandler;

@end

NS_ASSUME_NONNULL_END



//UncaughtExceptionHandle.m 文件中代码如下
#import "UncaughtExceptionHandle.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
#include <stdatomic.h>

//异常名
NSString * const kUncaughtExceptionHandlerSignalExceptionName = @"LGUncaughtExceptionHandlerSignalExceptionName";
//异常原因
NSString * const kUncaughtExceptionHandlerSignalExceptionReason = @"LGUncaughtExceptionHandlerSignalExceptionReason";

//异常签名
NSString * const kUncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
//异常产生地址(唯一)
NSString * const kUncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey";
//异常保存文件名
NSString * const kUncaughtExceptionHandlerFileKey = @"UncaughtExceptionHandlerFileKey";
//异常函数调用符号信息
NSString * const kUncaughtExceptionHandlerCallStackSymbolsKey = @"UncaughtExceptionHandlerCallStackSymbolsKey";

//atomic_int在库stdatomic中定义
atomic_int      kUncaughtExceptionCount = 0; // 未捕获的异常数量
//
const int32_t   kUncaughtExceptionMaximum = 8; //未捕获的异常数量的最大值

//从堆栈信息读取信息的起始位置,值为4是因为要跳过UncaughtExceptionHandle调用方法以及函数的堆栈信息
const NSInteger kUncaughtExceptionHandlerSkipAddressCount = 4;
//从堆栈信息读取信息的数量,获取5个就可以了
const NSInteger kUncaughtExceptionHandlerReportAddressCount = 5;

@implementation UncaughtExceptionHandle

//异常处理的函数
void ExceptionHandlers(NSException *exception) {
    NSLog(@"%s", __func__);
    
    //获取异常的数量
    int32_t exceptionCount = atomic_fetch_add_explicit(&kUncaughtExceptionCount, 1, memory_order_relaxed);
    
    //异常数量大于最大值,就不处理了。
    if (exceptionCount > kUncaughtExceptionMaximum) {
        return;
    }
    
    //获取堆栈信息 - model 编程思想
    NSArray *callStack = [UncaughtExceptionHandle backtrace];
    NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
    
    NSLog(@"异常名: %@", exception.name);
    NSLog(@"异常原因: %@", exception.reason);
    NSLog(@"异常调用栈信息: %@", exception.callStackSymbols);
    
    [userInfo setObject:exception.name forKey:kUncaughtExceptionHandlerSignalExceptionName];
    
    [userInfo setObject:exception.callStackReturnAddresses forKey:kUncaughtExceptionHandlerSignalExceptionReason];
    
    [userInfo setObject:callStack forKey:kUncaughtExceptionHandlerAddressesKey];
    
    [userInfo setObject:exception.callStackSymbols forKey:kUncaughtExceptionHandlerCallStackSymbolsKey];
    
    [userInfo setObject:@"DemoApp_Exceptions" forKey:kUncaughtExceptionHandlerFileKey];
    
    //保存崩溃日志或上传到服务器
    [[[UncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo] waitUntilDone:YES];
}


- (void)handleException:(NSException *)exception {
    
    NSDictionary *userInfo = [exception userInfo];
    //保存本地并上传到服务器
    [self saveCrash:exception file:[userInfo objectForKey:kUncaughtExceptionHandlerFileKey]];
    
    //在这里也可弹出提示框提示用户相关信息
}

- (void)saveCrash:(NSException *)exception file:(NSString *)fileName {
    
    //获取发送异常的堆栈信息
    NSArray *stackArray = [[exception userInfo] objectForKey:kUncaughtExceptionHandlerCallStackSymbolsKey];
    
    //异常名称
    NSString *exceptionName = [exception name];
    //异常原因
    NSString *exceptionReason = [exception reason];
    
    //直接使用代码输出崩溃信息日志
    NSLog(@"crash: %@", exception);
    //获取保存到本地的文件路径(沙盒缓存路径)
    NSString *exceptionFilePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:fileName];
    
    //如果文件路径不存在,就创建这个路径
    if (![[NSFileManager defaultManager] fileExistsAtPath:exceptionFilePath]) {
        [[NSFileManager defaultManager] createDirectoryAtPath:exceptionFilePath withIntermediateDirectories:YES attributes:nil error:nil];
    }
    
    //获取异常出现时的时间戳,写到文件名后缀中,以便排序及查阅
    NSDate *date = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval timeInterval = [date timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", timeInterval];
    
    NSString *savePath = [exceptionFilePath stringByAppendingFormat:@"/crash-error-%@.log", timeString];
    
    //获取异常信息
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception Reason: %@\nException Name: %@\nException stack: %@", exceptionName, exceptionReason, stackArray];
    
    //将异常信息保存到路径中
    BOOL isSuccess = [exceptionInfo writeToFile:savePath atomically:YES encoding:(NSUTF8StringEncoding) error:nil];
    
    NSLog(@"保存崩溃日志 success: %d, %@", isSuccess, savePath);
}

+ (void)installUncaughtSignalExceptionHandler {
    
    //objc_setUncaughtExceptionHandler()
    NSSetUncaughtExceptionHandler(&ExceptionHandlers);
}

+ (NSArray *)backtrace {
    void * callStack[128];
    //用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
    int frames = backtrace(callStack, 128);
    //从backtrace函数获取的信息转化为一个字符串数组
    char **strs = backtrace_symbols(callStack, frames);
    int i;
    NSMutableArray *backstrace = [NSMutableArray arrayWithCapacity:frames];
    
    for (i = kUncaughtExceptionHandlerSkipAddressCount; i < kUncaughtExceptionHandlerSkipAddressCount + kUncaughtExceptionHandlerReportAddressCount; i++) {
        
        NSString *infoStr = [NSString stringWithUTF8String:strs[i]];
        
        NSLog(@"current idx: %d, %@", i, infoStr);
        
        [backstrace addObject:infoStr];
    }
    
    free(strs);
    
    return backstrace;
}

@end



//在应用启动的时候进行调用
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [LGUncaughtExceptionHandle installUncaughtSignalExceptionHandler];
    return YES;
}
Copy the code

We know how to catch an exception, but we also need to know how to catch an exception. Actually the key lies in installUncaughtSignalExceptionHandler this class method is how to implement the code, the way we view this function details, will find the following information:

We can’t find any more information than that, so we put the breakpoint in ExceptionHandlers, compile and run the program again, click the button, execute to the breakpoint, and print the function call stack, as shown below:

According to the printed information, the process is clear. The exception system in ObjC is first initialized in _objc_init, so that the _objc_terminate function in ObjC is called every time there is an exception. If it’s OC, it calls uncaught_handler, which defaults to _objc_default_UNCaught_exception_handler, which has no body. If the primary engineering want to assignment of uncaught_handler this handle, NSSetUncaughtExceptionHandler will need to call this function to the assignment, In fact NSSetUncaughtExceptionHandler exposed by this function is CoreFoundation repository interfaces, And the interface in CoreFoundation actually called ObjC objc_setUncaughtExceptionHandler the library function, according to the incoming parameters for assignment of the uncaught_handler, this process is also called the handle.

1.4 Other initialization functions

Lock_init () : no overrides, C++ features.

Runtime_init () : Runtime runtime environment initialization, which consists of initialization of unattchedCategories and allocatedClasses, which will be discussed later in this article. The code looks like this:

void runtime_init(void)
{
    objc::unattachedCategories.init(32);
    objc::allocatedClasses.init();
}
Copy the code

Cache_init () : Cache condition initialization. The code looks like this:

void cache_t::init()
{
#if HAVE_TASK_RESTARTABLE_RANGES
    mach_msg_type_number_t count = 0;
    kern_return_t kr;

    while (objc_restartableRanges[count].location) {
        count++;
    }

    kr = task_restartable_ranges_register(mach_task_self(),
                                          objc_restartableRanges, count);
    if (kr == KERN_SUCCESS) return;
    _objc_fatal("task_restartable_ranges_register failed (result 0x%x: %s)",
                kr, mach_error_string(kr));
#endif // HAVE_TASK_RESTARTABLE_RANGES
}
Copy the code

_imp_implementationWithBlock_init() : Starts the callback mechanism. Normally, this doesn’t do anything because all initialization is lazy, but for a memorizing process, we can’t wait to load trampolines dylib. The trampolines dylib code looks like this:

/// Initialize the trampoline machinery. Normally this does nothing, as
/// everything is initialized lazily, but for certain processes we eagerly load
/// the trampolines dylib.
void
_imp_implementationWithBlock_init(void)
{
#if TARGET_OS_OSX
    // Eagerly load libobjc-trampolines.dylib in certain processes. Some
    // programs (most notably QtWebEngineProcess used by older versions of
    // embedded Chromium) enable a highly restrictive sandbox profile which
    // blocks access to that dylib. If anything calls
    // imp_implementationWithBlock (as AppKit has started doing) then we'll
    // crash trying to load it. Loading it here sets it up before the sandbox
    // profile is enabled and blocks it.
    //
    // This fixes EA Origin (rdar://problem/50813789)
    // and Steam (rdar://problem/55286131)
    if (__progname &&
        (strcmp(__progname, "QtWebEngineProcess") == 0 ||
         strcmp(__progname, "Steam Helper") == 0)) {
        Trampolines.Initialize();
    }
#endif
}
Copy the code

2. Parsing the map_images function code flow

Now that we have a good understanding of the logic in _objc_init, let’s look at the implementation of the logic in map_images. The code looks like this:

/*********************************************************************** * map_images * Process the given images which are being mapped in by dyld. * Calls ABI-agnostic code after taking ABI-specific locks. * * Locking: write-locks runtimeLock **********************************************************************/ void map_images(unsigned  count, const char * const paths[], const struct mach_header * const mhdrs[]) { mutex_locker_t lock(runtimeLock); return map_images_nolock(count, paths, mhdrs); }Copy the code

Map_images_nolock: map_images_nolock: map_images_nolock

Has 157 lines of code in this function, or longer, and this function has no return value, then we can probably look at the entire function, found _read_images (read binary files) this function, we assume that the function should be included in the important function, so we click to view the function implementation, The code in this function is also very long, with 360 lines of code and again no return value. The code summary is shown below:

In this function, it’s all about sorting the schema, and all we really need to care about is the code for each FixUp and the loading of the classes, so let’s go through it one by one.

2.1 Conditional control for a load

The code is as follows:

// This is a misnomer: gdb_objc_realized_classes is actually a list of 
// named classes not in the dyld shared cache, whether realized or not.
// This list excludes lazily named classes, which have to be looked up
// using a getClass hook.

NXMapTable *gdb_objc_realized_classes;  // exported for debuggers in objc-gdb.h
Copy the code

The NXMapTable is actually a table that holds all non-lazily loaded classes, that is, classes that have been implemented and hooked using getClass.

if (! doneOnce) { doneOnce = YES; launchTime = YES; . . . if (PrintConnecting) { _objc_inform("CLASS: found %d classes during launch", totalClasses); } // namedClasses // Preoptimized classes don't go in this table. // 4/3 is NXMapTable's load factor Int namedClassesSize = (isPreoptimized()? unoptimizedTotalClasses : totalClasses) * 4 / 3; gdb_objc_realized_classes = NXCreateMapTable(NXStrValueMapPrototype, namedClassesSize); ts.log("IMAGE TIMES: first time tasks"); }Copy the code

In this function, NXCreateMapTable is called. The code is as follows:

NXMapTable *NXCreateMapTable(NXMapTablePrototype prototype, unsigned capacity) {
    return NXCreateMapTableFromZone(prototype, capacity, malloc_default_zone());
}
Copy the code

In this function, a table of type NXMapTable is created and returned. It is used to store non-lazily loaded classes. The creation of the NXMapTable table is written here to ensure that the table of type is created only once. The table allocatedClasses created earlier in the runtime_init function called from the _objc_init function actually stores all the assigned classes (including metaclases). The code and comments are as follows:

/***********************************************************************
* allocatedClasses
* A table of all classes (and metaclasses) which have been allocated
* with objc_allocateClassPair.
**********************************************************************/
namespace objc {
static ExplicitInitDenseSet<Class> allocatedClasses;
}
Copy the code

2.2 Fixing the precompile phase@selectorThe problem of confusion

The code looks like this:

// Fix up @selector references static size_t UnfixedSelectors; { mutex_locker_t lock(selLock); for (EACH_HEADER) { if (hi->hasPreoptimizedSelectors()) continue; bool isBundle = hi->isBundle(); SEL *sels = _getObjc2SelectorRefs(hi, &count); UnfixedSelectors += count; for (i = 0; i < count; i++) { const char *name = sel_cname(sels[i]); SEL sel = sel_registerNameNoLock(name, isBundle); if (sels[i] ! = sel) { sels[i] = sel; } } } } ts.log("IMAGE TIMES: fix up selector references");Copy the code

First, put a breakpoint on the code, run ObjC source code, and print the following information:

You can see that sel[I] and sel have the same method number, but they have different addresses, but why do we assign sel[I] to sel? The reason is that the address data of each SEL in the SEL list obtained by using the _getObjc2SelectorRefs function is actually the data stored in the mirror file, and the address of each SEL in the mirror file is the offset value relative to the first address of the current mirror file. When the image file is loaded into the memory through ALSR and other security mechanisms, the first address of the image file will change randomly. In fact, the address of each SEL has also changed. Therefore, it is necessary to rebase the SEL in the SEL list obtained from the image file to obtain its actual address in memory at this moment. So sel_registerNameNoLock is used to rebase the sel address information in the image file and then assign sel to sel[I].

Sel_registerNameNoLock actually calls _dyLD_get_objc_selector from dyld library to get the sel address in memory, as shown below: sel_registerNameNoLock

SEL sel_registerNameNoLock(const char *name, bool copy) { return __sel_registerName(name, 0, copy); // NO lock, maybe copy } static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) { SEL result = 0; if (shouldLock) selLock.assertUnlocked(); else selLock.assertLocked(); if (! name) return (SEL)0; result = search_builtins(name); if (result) return result; conditional_mutex_locker_t lock(selLock, shouldLock); auto it = namedSelectors.get().insert(name); if (it.second) { // No match. Insert. *it.first = (const char *)sel_alloc(name, copy); } return (SEL)*it.first; } static SEL search_builtins(const char *name) { #if SUPPORT_PREOPT if (SEL result = (SEL)_dyld_get_objc_selector(name))  return result; #endif return nil; }Copy the code

2.3 Confusing class handling of errors

The code looks like this:

// Discover classes. Fix up unresolved future classes. Mark bundle classes. bool hasDyldRoots = dyld_shared_cache_some_image_overridden(); for (EACH_HEADER) { if (! mustReadClasses(hi, hasDyldRoots)) { // Image is sufficiently optimized that we need not call readClass() continue; } classref_t const *classlist = _getObjc2ClassList(hi, &count); bool headerIsBundle = hi->isBundle(); bool headerIsPreoptimized = hi->hasPreoptimizedClasses(); for (i = 0; i < count; i++) { Class cls = (Class)classlist[i]; Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized); if (newCls ! = cls && newCls) { // Class was moved but not deleted. Currently this occurs // only when the new class resolved a future class. // Non-lazily realize the class below. resolvedFutureClasses = (Class *) realloc(resolvedFutureClasses, (resolvedFutureClassCount+1) * sizeof(Class)); resolvedFutureClasses[resolvedFutureClassCount++] = newCls; } } } ts.log("IMAGE TIMES: discover classes");Copy the code

In this section of the code, we find a function readClass. This function may have some code logic we want to know about, so let’s look at the code in this function. It looks like this:

/*********************************************************************** * readClass * Read a class and metaclass as written by a compiler. * Returns the new class pointer. This could be: * - cls * - nil (cls has a missing weak-linked superclass) * - something else (space for this class was reserved by a future class) * * Note that all work performed by this function is preflighted by * mustReadClasses(). Do not change this function without updating that one. * * Locking: runtimeLock acquired by map_images or objc_readClassPair **********************************************************************/ Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized) { const char *mangledName = cls->nonlazyMangledName(); if (missingWeakSuperclass(cls)) { // No superclass (probably weak-linked). // Disavow any knowledge of this subclass. if  (PrintConnecting) { _objc_inform("CLASS: IGNORING class '%s' with " "missing weak-linked superclass", cls->nameForLogging()); } addRemappedClass(cls, nil); cls->setSuperclass(nil); return nil; } cls->fixupBackwardDeployingStableSwift(); Class replacing = nil; if (mangledName ! = nullptr) { if (Class newCls = popFutureNamedClass(mangledName)) { // This name was previously allocated as a future class. // Copy objc_class to future class's struct. // Preserve future's rw data block. if (newCls->isAnySwift()) { _objc_fatal("Can't complete future class request for '%s' " "because the real class is too big.", cls->nameForLogging()); } class_rw_t *rw = newCls->data(); const class_ro_t *old_ro = rw->ro(); memcpy(newCls, cls, sizeof(objc_class)); // Manually set address-discriminated ptrauthed fields // so that newCls gets the correct signatures. newCls->setSuperclass(cls->getSuperclass()); newCls->initIsa(cls->getIsa()); rw->set_ro((class_ro_t *)newCls->data()); newCls->setData(rw); freeIfMutable((char *)old_ro->getName()); free((void *)old_ro); addRemappedClass(cls, newCls); replacing = cls; cls = newCls; } } if (headerIsPreoptimized && ! replacing) { // class list built in shared cache // fixme strict assert doesn't work because of duplicates // ASSERT(cls  == getClass(name)); ASSERT(mangledName == nullptr || getClassExceptSomeSwift(mangledName)); } else { if (mangledName) { //some Swift generic classes can lazily generate their names addNamedClass(cls, mangledName, replacing); } else { Class meta = cls->ISA(); const class_ro_t *metaRO = meta->bits.safe_ro(); ASSERT(metaRO->getNonMetaclass() && "Metaclass with lazy name must have a pointer to the corresponding nonmetaclass."); ASSERT(metaRO->getNonMetaclass() == cls && "Metaclass nonmetaclass pointer must equal the original class."); } addClassTableEntry(cls); } // for future reference: shared cache never contains MH_BUNDLEs if (headerIsBundle) { cls->data()->flags |= RO_FROM_BUNDLE; cls->ISA()->data()->flags |= RO_FROM_BUNDLE; } return cls; }Copy the code

First of all, the return value of the code in this function is CLS, so let’s take a look at where the CLS is created and operated. We find the familiar part, that is, the CLS ro, rW, rWE are assigned, and the CLS ISA and superclass are set, as shown in the figure below:

But is this really the part of the code that loads the class? To verify this, we create a Person class in the source code and a Person object in the main function, as shown below:

@interface Person : NSObject - (void)say; @end @implementation Person - (void)say { } @end int main(int argc, const char * argv[]) { Person *p = [[Person alloc] init]; @autoreleasepool {NSLog(@" execute main "); } return 0; }Copy the code

To exclude the effects of system classes, write some code and set a breakpoint before the readClass function is called in the code, as shown in the figure below:

Compile and run the program, go to the breakpoint, and print the current CLS, as follows:

Then hit the following breakpoint in the readClass function and step through the program as follows:

Step through the program to see which code statements are executed. The result is as follows:

Instead, the following branch is executed:

CLS is printed before and after execution, and the output is as follows:

Step through and execute the code in the addClassTableEntry function, as shown in the figure below:

The code for this function looks like this:

/*********************************************************************** * addClassTableEntry * Add a class to the table  of all classes. If addMeta is true, * automatically adds the metaclass of the class as well. * Locking: runtimeLock must be held by the caller. **********************************************************************/ static void addClassTableEntry(Class cls, bool addMeta = true) { runtimeLock.assertLocked(); // This class is allowed to be a known class via the shared cache or via // data segments, but it is not allowed to be in the dynamic table already. auto &set = objc::allocatedClasses.get(); ASSERT(set.find(cls) == set.end()); if (! isKnownClass(cls)) set.insert(cls); if (addMeta) addClassTableEntry(cls->ISA(), false); }Copy the code

The logic is to insert CLS into the assigned class table, and it will also insert its metaclass into the assigned class table through a recursive call, which will return CLS directly. You can see that for the Person class we created, the readClass function just assigns the class name. There is no assignment for ro, RW, RWE, or anything else.

2.4 Fixed remapping of some classes that were not loaded by the image file

The code looks like this:

// Fix up remapped classes // Class list and nonlazy class list remain unremapped. // Class refs and super refs are remapped for message dispatching. if (! noClassesRemapped()) { for (EACH_HEADER) { Class *classrefs = _getObjc2ClassRefs(hi, &count); for (i = 0; i < count; i++) { remapClassRef(&classrefs[i]); } // fixme why doesn't test future1 catch the absence of this? classrefs = _getObjc2SuperRefs(hi, &count); for (i = 0; i < count; i++) { remapClassRef(&classrefs[i]); } } } ts.log("IMAGE TIMES: remap classes");Copy the code

2.5 Fixed some messages

The code looks like this:

#if SUPPORT_FIXUP
    // Fix up old objc_msgSend_fixup call sites
    for (EACH_HEADER) {
        message_ref_t *refs = _getObjc2MessageRefs(hi, &count);
        if (count == 0) continue;

        if (PrintVtables) {
            _objc_inform("VTABLES: repairing %zu unsupported vtable dispatch "
                         "call sites in %s", count, hi->fname());
        }
        for (i = 0; i < count; i++) {
            fixupMessageRef(refs+i);
        }
    }

    ts.log("IMAGE TIMES: fix up objc_msgSend_fixup");
#endif
Copy the code

3. Summary

Exception handling flowchart:

This article mainly explores some environment variables and the process of exception handling that we may encounter in project development, and also explores the code process in map_images function, and the important function in map_images process is read_images function. The read_images function did a lot of fixing. We thought it might be the information about the class loaded in read_class, but when we debug and run the code, we only assigned the class name in read_class and added the class to the table of assigned classes. This is a lot of content, so I put the exploration of the second half of the code in the read_images function into the next article. Thanks for reading!