iOS Crash從捕獲到符號化解析分析

目的

  • 探索iOS Crash分類及捕獲流程
  • 了解Crash文件結構及段含義
  • 了解Mach-o文件結構
  • 分析Crash堆棧地址與符號表還原流程

Crash分類

Crash的主要原因是你的應用收到了未處理的信號。未處理信號可能來源于三個地方:kernel、其他進程、以及App本身。因此,crash異常也分為三種:

  • Mach異常:底層內核級異常。用戶態的開發者可以直接通過Mach API設置thread,task,host的異常端口,來捕獲Mach異常。
  • Unix信號:又稱BSD信號。它是UNIX層的異常信號標準,通過方法threadsignal()將信號投遞到出錯線程。可以通過方法signal(x, SignalHandler)來捕獲single。
  • NSException:應用級異常。它是未被捕獲的Objective-C異常,導致程序向自身發送了SIGABRT信號而崩潰,對于未捕獲的Objective-C異常,是可以通過try catch來捕獲的,或者通過NSSetUncaughtExceptionHandler()機制來捕獲。

三者關系

Mach異常、Unix信號、NSException異常是什么?它們之間有什么相互關系?

Darwin是Mac OS和iOS的操作系統,而XNU是Darwin操作系統的內核部分。XNU是混合內核,兼具宏內核和微內核的特性,而Mach即為其微內核。


Mac可執行下述命令查看Darwin版本號

~$ system_profiler SPSoftwareDataType
Software:

    System Software Overview:

      System Version: macOS 10.15 (19A583)
      Kernel Version: Darwin 19.0.0
      Boot Volume: Macintosh HD
      Boot Mode: Normal
      Computer Name: 58的MacBook Pro (2)
      User Name: lltree (lltree)
      Secure Virtual Memory: Enabled
      System Integrity Protection: Enabled
      Time since boot: 5 days 23:48

關系:

  • 1 Mach異常是內核態的異常,屬于底層異常。
  • 2 轉換Unix信號是為了兼容更為流行的POSIX標準(SUS規范),這樣不必了解Mach內核也可以通過Unix信號的方式來兼容開發。
  • 3 因為硬件產生的信號(通過CPU陷阱)被Mach層捕獲,然后才轉換為對應的Unix信號;蘋果為了統一機制,于是操作系統和用戶產生的信號(通過調用kill和pthread_kill)也首先沉下來被轉換為Mach異常,再轉換為Unix信號。

Carsh傳遞流程:
硬件產生信號或者kill或pthread_kill信號 --> Mach異常 --> Unix信號(SIGABRT)

image

Mach異常捕獲

image
  • 1、硬件處理器陷阱產生的信號被Mach層捕獲
  • 2、Mach異常處理程序exception_triage()通過調用exception_deliver()首先嘗試將異常拋給thread端口、然后嘗試拋給task端口,最后再拋給host端口(默認端口),exception_deliver通過調用mach_exception_raise,觸發異常;
  • 3、異常在內核中以消息機制進行處理,通過task_set_exception_posrts()設置自定義的接收Mach異常消息的端口,相當于插入了一個exception處理程序。

實現:mach異常消息機制處理而不是通過函數調用exception messages可以被轉發到先前注冊的Mach exception處理程序。這意味著你可以插入一個exception處理程序,而不干擾現有的無論是調試器或Apple's crash reporter。

知道以上這些,那我們來嘗試撲捉一下Mach異常。系統讓用戶很少關心底層實現細節,因此Mach提供少量API

// 內核中創建一個消息隊列,獲取對應的port
mach_port_allocate();
// 授予task對port的指定權限
mach_port_insert_right();
//將端口設置為接受異常的端口
task_set_exception_ports()

代碼示例:異常捕獲

static bool installExceptionHandler()
{
    KSLOG_DEBUG("Installing mach exception handler.");

    bool attributes_created = false;
    pthread_attr_t attr;

    kern_return_t kr;
    int error;

    //使用mach_task_self獲取當前任務進程
    const task_t thisTask = mach_task_self();
    
    //Mach層異常類型
    exception_mask_t mask = EXC_MASK_BAD_ACCESS |
    EXC_MASK_BAD_INSTRUCTION |
    EXC_MASK_ARITHMETIC |
    EXC_MASK_SOFTWARE |
    EXC_MASK_BREAKPOINT;

    KSLOG_DEBUG("Backing up original exception ports.");
    //備份異常端口
    kr = task_get_exception_ports(thisTask,
                                  mask,
                                  g_previousExceptionPorts.masks,
                                  &g_previousExceptionPorts.count,
                                  g_previousExceptionPorts.ports,
                                  g_previousExceptionPorts.behaviors,
                                  g_previousExceptionPorts.flavors);
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_get_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    if(g_exceptionPort == MACH_PORT_NULL)
    {
        KSLOG_DEBUG("Allocating new port with receive rights.");
        /*
         創建一個具有接受權限的異常端口
         因為Mach異常其實是一個消息轉發的異常,
         所以需要消息接收權限,在初始化異常端口的時候就賦予RECEIVE
         */
        kr = mach_port_allocate(thisTask,
                                MACH_PORT_RIGHT_RECEIVE,
                                &g_exceptionPort);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_allocate: %s", mach_error_string(kr));
            goto failed;
        }

        KSLOG_DEBUG("Adding send rights to port.");
        //給端口添加發送權限
        kr = mach_port_insert_right(thisTask,
                                    g_exceptionPort,
                                    g_exceptionPort,
                                    MACH_MSG_TYPE_MAKE_SEND);
        if(kr != KERN_SUCCESS)
        {
            KSLOG_ERROR("mach_port_insert_right: %s", mach_error_string(kr));
            goto failed;
        }
    }

    KSLOG_DEBUG("Installing port as exception handler.");
    //將端口設置為接受異常的端口
    kr = task_set_exception_ports(thisTask,
                                  mask,
                                  g_exceptionPort,
                                  (int)(EXCEPTION_DEFAULT | MACH_EXCEPTION_CODES),
                                  THREAD_STATE_NONE);
    if(kr != KERN_SUCCESS)
    {
        KSLOG_ERROR("task_set_exception_ports: %s", mach_error_string(kr));
        goto failed;
    }

    
    
    pthread_attr_init(&attr);
    attributes_created = true;
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    
    //創建輔助異常線程
    KSLOG_DEBUG("Creating secondary exception thread (suspended).");
    error = pthread_create(&g_secondaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadSecondary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create_suspended_np: %s", strerror(error));
        goto failed;
    }
    g_secondaryMachThread = pthread_mach_thread_np(g_secondaryPThread);
    ksmc_addReservedThread(g_secondaryMachThread);

    //創建主要異常線程
    KSLOG_DEBUG("Creating primary exception thread.");
    error = pthread_create(&g_primaryPThread,
                           &attr,
                           &handleExceptions,
                           kThreadPrimary);
    if(error != 0)
    {
        KSLOG_ERROR("pthread_create: %s", strerror(error));
        goto failed;
    }
    pthread_attr_destroy(&attr);
    g_primaryMachThread = pthread_mach_thread_np(g_primaryPThread);
    ksmc_addReservedThread(g_primaryMachThread);

    KSLOG_DEBUG("Mach exception handler installed.");
    return true;


    //失敗則退出
failed:
    KSLOG_DEBUG("Failed to install mach exception handler.");
    if(attributes_created)
    {
        pthread_attr_destroy(&attr);
    }
    uninstallExceptionHandler();
    return false;
}

代碼示例:異常處理

static void* handleExceptions(void* const userData)
{
    MachExceptionMessage exceptionMessage = {{0}};
    MachReplyMessage replyMessage = {{0}};
    char* eventID = g_primaryEventID;

    const char* threadName = (const char*) userData;
    pthread_setname_np(threadName);
    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.
         //等待mach exception,否則會阻塞
        kern_return_t kr = mach_msg(&exceptionMessage.header,
                                    MACH_RCV_MSG,
                                    0,
                                    sizeof(exceptionMessage),
                                    g_exceptionPort,
                                    MACH_MSG_TIMEOUT_NONE,
                                    MACH_PORT_NULL);
        
        //捕獲到mach exception,跳出循環
        if(kr == KERN_SUCCESS)
        {
            break;
        }

        // Loop and try again on failure.
        KSLOG_ERROR("mach_msg: %s", mach_error_string(kr));
    }

    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;
        ksmc_suspendEnvironment(&threads, &numThreads);
        g_isHandlingCrash = true;
        kscm_notifyFatalExceptionCaptured(true);

        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(ksthread_self() == g_primaryMachThread)
        {
            KSLOG_DEBUG("This is the primary exception thread. Activating secondary thread.");
// TODO: This was put here to avoid a freeze. Does secondary thread ever fire?
            restoreExceptionPorts();
            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);
        KSCrash_MonitorContext* crashContext = &g_monitorContext;
        crashContext->offendingMachineContext = machineContext;
        kssc_initCursor(&g_stackCursor, NULL, NULL);
        if(ksmc_getContextForThread(exceptionMessage.thread.name, machineContext, true))
        {
            kssc_initWithMachineContext(&g_stackCursor, 100, machineContext);
            KSLOG_TRACE("Fault address %p, instruction address %p",
                        kscpu_faultAddress(machineContext), kscpu_instructionAddress(machineContext));
            if(exceptionMessage.exception == EXC_BAD_ACCESS)
            {
                crashContext->faultAddress = kscpu_faultAddress(machineContext);
            }
            else
            {
                crashContext->faultAddress = kscpu_instructionAddress(machineContext);
            }
        }

        KSLOG_DEBUG("Filling out context.");
        crashContext->crashType = KSCrashMonitorTypeMachException;
        crashContext->eventID = eventID;
        crashContext->registersAreValid = true;
        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(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.
            crashContext->mach.code = KERN_INVALID_ADDRESS;
        }
        crashContext->signal.signum = signalForMachException(crashContext->mach.type, crashContext->mach.code);
        crashContext->stackCursor = &g_stackCursor;

        kscm_handleException(crashContext);

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

    KSLOG_DEBUG("Replying to mach exception message.");
    // Send a reply saying "I didn't handle this exception".
    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;
}

Signal異常捕獲

Signal是Unix標準下的處理機制,讓開發者不必關系底層內核相關。為了維護一個統一的機制,操作系統和用戶嘗試的信號首先被轉換為Mach異常,然后再轉換為信號(Signals)。

Mach 異常在Mach層被捕獲并拋出后,會在BSD層被catch_mach_exception_raise處理,并通過ux_exception()將異常轉換為對應的UNIX信號,并通過threadsignal()將信號投遞到出錯線程,iOS中的 POSIX API 就是通過 Mach 之上的 BSD 層實現的

代碼示例:注冊過程

void InstallSignalHandler(void){

    signal(SIGHUP, SignalExceptionHandler);
    signal(SIGINT, SignalExceptionHandler);
    signal(SIGQUIT, SignalExceptionHandler);
    signal(SIGABRT, SignalExceptionHandler);
    signal(SIGILL, SignalExceptionHandler);
    signal(SIGSEGV, SignalExceptionHandler);
    signal(SIGFPE, SignalExceptionHandler);
    signal(SIGBUS, SignalExceptionHandler);
    signal(SIGPIPE, SignalExceptionHandler);
}

代碼示例:捕獲處理

void SignalExceptionHandler(int signal){

    NSMutableString *mstr = [[NSMutableString alloc] init];
    [mstr appendString:@"Stack:\n"];
    void* callstack[128];
    int i, frames = backtrace(callstack, 128);
    char** strs = backtrace_symbols(callstack, frames);
    for (i = 0; i <frames; ++i) {
        [mstr appendFormat:@"%s\n", strs[I]];
    }
    [SignalHandler saveCreash:mstr];
}

NSException異常捕獲

NSException異常屬于OC層異常。該異常在OC層如果有對應的NSException(OC異常),就轉換成OC異常,OC異常可以在OC層得到處理;如果OC異常一直得不到處理,程序會強行發送SIGABRT信號中斷程序。在OC層如果沒有對應的NSException,就只能讓Unix標準的signal機制來處理了。
代碼示例:注冊過程

NSSetUncaughtExceptionHandler(&handleUncaughtException);

代碼示例:捕獲處理

void HandleException(NSException *exception){

    // 異常的堆棧信息
    NSArray *stackArray = [exception callStackSymbols];
    // 出現異常的原因
    NSString *reason = [exception reason];
    // 異常名稱
    NSString *name = [exception name];
    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception     reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
    NSLog(@"%@", exceptionInfo);
    [UncaughtExceptionHandler saveCreash:exceptionInfo];
}

Crash崩潰堆棧符號化

為探究崩潰堆棧符號化過程,我們提供一個簡單的Demo,點擊屏幕上按鈕執行onCrash方法,主動觸發崩潰。

image

而我們收集iOS的崩潰信息時,獲取到的崩潰堆棧一般是如下的形式,全是十六進制的內存地址形式:

Last Exception Backtrace:
(0x1a8f9580c 0x1a8cbdfa4 0x1a8e9502c 0x102b298b4 0x1acfffab0 
0x1aca378ac 0x1aca37c10 0x1aca36c2c 0x1ad039288 0x1ad03a5c8 
0x1ad016b78 0x1ad08eef8 0x1ad091454 0x1ad08a2c8 0x1a8f137c4 
0x1a8f1371c 0x1a8f12eb4 0x1a8f0e000 0x1a8f0d8a0 0x1b2e65328 
0x1acffe740 0x102b2a3e4 0x1a8d98360)

Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   libsystem_kernel.dylib          0x00000001a8d8dec4 0x1a8d69000 + 151236
1   libsystem_pthread.dylib         0x00000001a8ca9774 0x1a8ca7000 + 10100
2   libsystem_c.dylib               0x00000001a8bfd844 0x1a8b8a000 + 473156
3   libc++abi.dylib                 0x00000001a8d567d4 0x1a8d55000 + 6100
4   libc++abi.dylib                 0x00000001a8d569c4 0x1a8d55000 + 6596
5   libobjc.A.dylib                 0x00000001a8cbe258 0x1a8cb8000 + 25176
6   Simple-Example                  0x0000000102b3e7cc 0x102b24000 + 108492
7   libc++abi.dylib                 0x00000001a8d63304 0x1a8d55000 + 58116
8   libc++abi.dylib                 0x00000001a8d62ed8 0x1a8d55000 + 57048
9   libobjc.A.dylib                 0x00000001a8cbe158 0x1a8cb8000 + 24920
10  CoreFoundation                  0x00000001a8f0d910 0x1a8e6a000 + 669968
11  GraphicsServices                0x00000001b2e65328 0x1b2e62000 + 13096
12  UIKitCore                       0x00000001acffe740 0x1ac5fd000 + 10491712
13  Simple-Example                  0x0000000102b2a3e4 0x102b24000 + 25572
14  libdyld.dylib                   0x00000001a8d98360 0x1a8d97000 + 4960

這樣的格式我們很難看出實際含義,無法定位問題代碼,只有將它們轉化為可讀的形式才有意義:

Bugly異常上報解析結果
Last Exception Backtrace:
0   CoreFoundation                  0x1a8f9580c __exceptionPreprocess + 220
1   libobjc.A.dylib                 0x1a8cbdfa4 objc_exception_throw + 55
2   CoreFoundation                  0x1a8e9502c -[__NSArray0 objectAtIndex:] + 107
3   Simple-Example                  0x102c318b4 -[ViewController onCrash:] + 22708 (ViewController.m:28)
4   UIKitCore                       0x1acfffab0 -[UIApplication sendAction:to:from:forEvent:] + 95
5   UIKitCore                       0x1aca378ac -[UIControl sendAction:to:forEvent:] + 239
6   UIKitCore                       0x1aca37c10 -[UIControl _sendActionsForEvents:withEvent:] + 407
7   UIKitCore                       0x1aca36c2c -[UIControl touchesEnded:withEvent:] + 519
8   UIKitCore                       0x1ad039288 -[UIWindow _sendTouchesForEvent:] + 2323
9   UIKitCore                       0x1ad03a5c8 -[UIWindow sendEvent:] + 3351
10  UIKitCore                       0x1ad016b78 -[UIApplication sendEvent:] + 335
11  UIKitCore                       0x1ad08eef8 __dispatchPreprocessedEventFromEventQueue + 5879
12  UIKitCore                       0x1ad091454 __handleEventQueueInternal + 4923
13  UIKitCore                       0x1ad08a2c8 __handleHIDEventFetcherDrain + 107
14  CoreFoundation                  0x1a8f137c4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 23
15  CoreFoundation                  0x1a8f1371c __CFRunLoopDoSource0 + 79
16  CoreFoundation                  0x1a8f12eb4 __CFRunLoopDoSources0 + 179
17  CoreFoundation                  0x1a8f0e000 __CFRunLoopRun + 1079
18  CoreFoundation                  0x1a8f0d8a0 CFRunLoopRunSpecific + 463
19  GraphicsServices                0x1b2e65328 GSEventRunModal + 103
20  UIKitCore                       0x1acffe740 UIApplicationMain + 1935
21  Simple-Example                  0x102c323e4 main + 25572 (main.m:16)
22  libdyld.dylib                   0x1a8d98360 start + 3
或者Xcode中的打印窗口輸出結果

image

如上所示,我們一眼就能看明白,這次崩潰發生在ViewController.m文件的28行,對應的方法是onCrash:,調用[__NSArray0 objectAtIndex:]引起的崩潰。那么這樣的符號化又是如何實現的呢?

認識符號表

符號表是內存地址與函數名、文件名、行號的映射表。符號表元素如下所示:

<起始地址> <結束地址> <函數> [<文件名>:<行號>],

有了符號表能快速并準確地定位用戶APP發生Crash的代碼位置,
我們可以使用符號表對APP發生Crash的程序堆棧進行解析和還原

什么是dSYM文件?

iOS平臺中,dSYM文件是指具有調試信息的目標文件,文件名通常為: com.公司名.dSYM。如下圖所示:

image

為直觀呈現符號表,使用Bugly中的提取符號表命令從dSYM中提取符號表,類似如下結構:

5848    5880    -[ViewController onCrash:]  ViewController.m:24
5880    58a0    -[ViewController onCrash:]  ViewController.m:26
58a0    58a4    -[ViewController onCrash:]  ViewController.m:26
58a4    58a8    -[ViewController onCrash:]  ViewController.m:28
58a8    58b4    -[ViewController onCrash:]  ViewController.m:28
58b4    58c0    -[ViewController onCrash:]  ViewController.m:28
58c0    58f0    -[ViewController onCrash:]  ViewController.m:29

符號化的依據來自dSYM文件, dSYM文件也是Mach-o格式,我們按照Mach-o格式一步一步解析即可。
Mach-O文件類型分類:

  • Executable:應用可執行的二進制文件,如.m/.h文件經過編譯后會生成對應的Mach-O文件
  • Dylib Library:動態鏈接庫
  • Static Library:靜態鏈接庫
  • Bundle:不能被鏈接 Dylib,只能在運行使用dlopen()加載
  • Relocatable Object File:可重定向文件類型
image

從圖上我們可以大概的看出Mach-O可以分為3個部分

  • Header
  • Segment
  • section

如圖所示,header后面是segment,然后再跟著section,而一個segment是可以包含多個section的。為更加直觀的呈現dSYM內容,我們把dSYM文件放入可視化工具:


image

其中的LC_SEGMENT_64(_DEARF)Section64 Header(_debug_line)則是我們目標分析對象。

Crash日志格式:

iOS的crash報告日志可以分為:

  • 頭部(Header)
  • 異常信息(Exception Information)
  • 診斷信息(Additional Diagnostic Information)
  • 線程堆棧(Backtraces)
  • 線程狀態(Thread State)
  • 庫信息(Binary Images)

這個六個部分。實例如下:


image
image

符號化解析流程

我們從剛才不管是上報到存儲到手機端的Crash文件或者Xcode崩潰打印的信息中,在回溯堆棧中都離不開調用堆棧地址。如下所示:

Last Exception Backtrace:
(0x1a8f9580c 0x1a8cbdfa4 0x1a8e9502c 0x102b298b4 0x1acfffab0 
0x1aca378ac 0x1aca37c10 0x1aca36c2c 0x1ad039288 0x1ad03a5c8 
0x1ad016b78 0x1ad08eef8 0x1ad091454 0x1ad08a2c8 0x1a8f137c4 
0x1a8f1371c 0x1a8f12eb4 0x1a8f0e000 0x1a8f0d8a0 0x1b2e65328 
0x1acffe740 0x102b2a3e4 0x1a8d98360)

我們如何把實際運行中的調用堆棧地址通過dSYM文件關聯起來,解析出來崩潰信息呢?例如
0x102b298b4
與解析完的
3 Simple-Example 0x102c318b4 -[ViewController onCrash:] + 22708 (ViewController.m:28)
如何形成對應關系?

蘋果為了杜絕黑客攻擊App,因此在App啟動的時候,引入

ASLR地址空間布局隨機化

ASLR:Address space layout randomization,將可執行程序隨機裝載到內存中,這里的隨機只是偏移,而不是打亂,具體做法就是通過內核將 Mach-O的平移某個隨機系數。slide 正是ASLR引入的偏移

每次使用隨機偏移地址加載App程序,這樣針對該行代碼ViewController.m:28每次App運行的時候實際運行地址(例如:0x102b298b4)都是不一樣的。

Crash崩潰信息中,存在每個庫信息,示例如下:

Binary Images:
0x102b24000 - 0x102baffff Simple-Example arm64  <86bf04850a8a344d8a426ed6859383e3> /var/containers/Bundle/Application/3D7D3552-E687-4007-B166-055B28C53F4C/Simple-Example.app/Simple-Example
0x102d14000 - 0x102d1ffff libobjc-trampolines.dylib arm64  <ef2f193f10af36428d9b573e1d256368> /usr/lib/libobjc-trampolines.dylib
0x102dcc000 - 0x102e2ffff dyld arm64  <330227f4c8da3dcebfea057e7770ae9a> /usr/lib/dyld

庫信息都存在,程序內存占用地址空間例如Simple-Example本次隨機偏移后的地址為0x102b24000 - 0x102baffff。因此我們可以根據dSYM文件中程序加載地址計算出偏移slide

偏移slide = 0x102b24000 - 0x100000000 = 0x2b24000

根據實際崩潰地址減去偏移slide則可計算除符號表中地址0x1000058b4

0x102c318b4 -  0x2b24000 =  0x1000058b4

可以通過以下命令查詢崩潰信息
~ lltree$ dwarfdump --arch arm64 --lookup 0x1000058b4 Simple-Example.app.dSYM
查詢結果如下

Simple-Example.app.dSYM/Contents/Resources/DWARF/Simple-Example:    file format Mach-O arm64
0x00046f0d: Compile Unit: length = 0x00000783 version = 0x0002 abbr_offset = 0x0000 addr_size = 0x08 (next unit at 0x00047694)

0x00046f18: DW_TAG_compile_unit
              DW_AT_producer    ("Apple clang version 11.0.0 (clang-1100.0.33.12)")
              DW_AT_language    (DW_LANG_ObjC)
              DW_AT_name    ("/Users/XXX/Desktop/iOS\346\272\220\344\273\243\347\240\201\347\240\224\347\251\266/KSCrash-master-2/iOS/Simple-Example/ViewController.m")
              DW_AT_stmt_list   (0x0000a5bc)
              DW_AT_comp_dir    ("/Users/XXX/Desktop/iOS\346\272\220\344\273\243\347\240\201\347\240\224\347\251\266/KSCrash-master-2/iOS")
              DW_AT_GNU_pubnames    (0x01)
              DW_AT_APPLE_major_runtime_vers    (0x02)
              DW_AT_low_pc  (0x0000000100005710)
              DW_AT_high_pc (0x00000001000059b0)

0x00047081:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000100005848)
                DW_AT_high_pc   (0x00000001000058f0)
                DW_AT_frame_base    (DW_OP_reg29 W29)
                DW_AT_object_pointer    (0x000470a0)
                DW_AT_name  ("-[ViewController onCrash:]")
                DW_AT_decl_file ("/Users/XXX/Desktop/iOS XXX/iOS/Simple-Example/ViewController.m")
                DW_AT_decl_line (24)
                DW_AT_prototyped    (0x01)
Line info: file 'ViewController.m', line 28, column 8, start line 24

該信息如何在dSYM文件中體現呢?

DWARF 中的調試信息被放在一個叫作 .debug_info 的段中,該段與 DWARF 中其它的段類似,可以看成是一個表格狀的結構,表中每一條記錄叫作一個 DIE(debugging information entry), 一個 DIE 由一個 tag 及 很多 attribute 組成,其中 tag 用于表示當前的 DIE 的類型,類型指明當前 DIE 用于描述什么東西,如函數,變量,類型等,而 attribute 則是一對對的 key/value 用于描述其它一些信息,在 linux 下我們可以用如下命令來查看 ELF 中的調試信息:

image

為此我們使用如下命令,提取出
dwarfdump -p -debug-info /Users/XXX/Desktop/crash分析/Simple-Example.app.dSYM/Contents/Resources/DWARF/Simple-Example >line-e.txt

line-e.txt結果如下:

image

該文件中,該DIE段信息中與共計7段信息,與從dSYM中使用Bugly提取符號表命令提取符號表段信息相符

5848    5880    -[ViewController onCrash:]  ViewController.m:24
5880    58a0    -[ViewController onCrash:]  ViewController.m:26
58a0    58a4    -[ViewController onCrash:]  ViewController.m:26
58a4    58a8    -[ViewController onCrash:]  ViewController.m:28
58a8    58b4    -[ViewController onCrash:]  ViewController.m:28
58b4    58c0    -[ViewController onCrash:]  ViewController.m:28
58c0    58f0    -[ViewController onCrash:]  ViewController.m:29

目前遺留問題:
每段信息58b4與虛擬內存起始地址0x0000000100005848之間關系,如何計算出來,暫時未知?

至此:結束

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。