一文讀懂崩潰原理

debug能力是程序員重要能力,也能真實反映程序員的水平,也是晉升重要的能力!尤爲崩潰排查分析解決能力!這節課將透過現象看透崩潰的本質!git

崩潰長什麼樣子

好比斷點調試崩潰:程序員

App Store上應用崩潰,可經過Xcode中的Oragnizer來獲取,以下圖:github

或者直接鏈接崩潰的設備經過Xcode->Device->View Device Logs來獲取,以下圖:objective-c

對於Mac App可經過控制檯中的」崩潰報告「來獲取,以下圖:xcode

還能夠經過第三方崩潰平臺來獲取,好比bugly、友盟、KSCrash等,以下圖: markdown

崩潰爲何會發生

那崩潰發生有哪些具體緣由:網絡

  • cpu沒法執行的代碼架構

    好比無效指令或操做、訪問無效地址及不具備權限的內存地址、除以0等;app

    有多是蘋果bug,會產生非法指令錯誤,以下圖所示:框架

    殭屍對象,以下圖:

    好比64位系統,訪問0~4GB地址空間會報無效地址,就會報段錯誤Segmentation fault,以下圖:

64位系統對應的__PAGEZERO段地址空間爲0~4GB,在這個範圍內全部訪問權限-讀、寫和執行-都被撤銷,所以若訪問該地址就會引起MMU的硬件頁錯誤,進而產生一個異常。

代碼以下:

小技巧:那如何調試過程當中直接讓應用崩潰呢?是由於自己工程默認開啓了Debug excutable選項,具體選項如圖:

關閉該選項就能夠直接讓應用崩潰產生崩潰日誌報告。

除以0,就會致使算術異常,致使EXC_ARITHMETIC錯誤,以下圖:

  • 被系統強殺

    • 應用內存消耗太高OOM

    • 主線程長時間沒法響應ANR

      爲了防止一個應用佔用過多的系統資源,開發iOS的蘋果工程師門設計了一個「看門狗」Watchdog的機制。在不一樣的場景下,「看門狗」會監測應用的性能。若是超出了該場景所規定的運行時間,「看門狗」就會強制終結這個應用的進程。開發者們在crashlog裏面,會看到諸如0x8badf00d這樣的錯誤代碼。

      Watchdog機制是iOS爲了保持用戶界面的響應引入的一種機制。若是咱們的應用未能及時的響應一些用戶界面事件,如啓動、暫停、恢復和終止,Watchdog就會殺死程序並生成一個Watchdog超時崩潰報告。Watchdog超時時間並無明文規定,但一般會少於網絡超時。

      好比App應用啓動時同步請求啓動配置數據,如在網絡差的狀況下就會致使被Watchdog強殺,須要在真機上運行應用且不能xcode調試模式;

    • 資源異常

      • 線程頻繁喚醒

        Wakeups是「資源異常」下的一個子類,指的是頻繁喚醒線程,消耗cpu資源並增長功耗,在超過閾值並處於FATAL CONDITION的條件下觸發崩潰;若是300秒內的總wakeup數超過45000(300 * 150)就會被斷定爲超出閾值。

      • 進程中的線程過多的佔用了cpu,限制爲 50%,時間不超過 180秒

      • 線程短期過多的磁盤寫入

    • 死鎖

      好比互斥鎖同一線程屢次上鎖就會致使死鎖

      NSLock *_lock = [[NSLock alloc]init];
      [_lock lock];
      [_lock lock];//屢次上鎖
      複製代碼
    • 非法的應用簽名

    • 後臺執行超時

      App退至後臺後若執行時間過長就會致使被系統被殺,好比Backgroud Task方式能夠在後臺執行3min,若超過3min還未運行完成就會被系統強殺,實例代碼:

      - (void)applicationDidEnterBackground:(UIApplication *)application {
          //經過backgroud task方法延長後臺時間 這個方法必須與endBackgroundTask一一對象
          UIBackgroundTaskIdentifier _backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
          }];
          
          //此處爲執行任務代碼 一般用來保存應用程序關鍵數據數據
          //執行關鍵任務
          
          //當任務執行完成時 調用endBackgroundTask方法 調用後就會將app掛起
          //若是在最後時間到了以前仍然沒有調用endBackgroundTask方法 就會執行此回調
          //一般時間爲3分鐘,若是時間到期以前調用endBackgroundTask方法 就會強制殺掉進程,就會形成崩潰
          [application endBackgroundTask:_backgroundTaskIdentifier];
      }
      複製代碼

    • 設備總內存緊張

      由於Mac平臺存在內存交換機制,而iOS平臺沒有,就致使整個設備內存吃緊的時候,系統就會殺掉優先級不高且佔用內存多大的應用;

    • 設備過熱

      通常見於低端設備

  • 語言觸發異常

    • OC語言拋出異常

      具體的異常以下:

      常見的以下:

      NSArray越界訪問產生的異常NSRangeException,調試下會看到以下消息:

      *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 0]'
      複製代碼

      未找到的方法或者NSDictionary添加nil對象等致使的非法參數異常NSInvalidArgumentException,如:

      *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController hello]: unrecognized selector sent to instance 0x7fc65b604e30'
      複製代碼

      kvc未找到相對應的key拋出NSUnknownKeyException,結果以下:

      *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<ViewController 0x7fd3f6806450> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key hello.'
      複製代碼
    • C++拋出異常

    語言異常拋出後最後都會調用到abort來終止應用,調用棧以下圖所示:

  • 開發者觸發

好比斷言,NSAssert或者assert函數;

崩潰如何發生

說了這麼多場景,那崩潰時如何發生的呢?其底層機制是如何運做的呢?只有你理解了崩潰的機制才能更有效的防禦及排查!接下來給你們一一剖析!首先,要明白「中斷」是什麼?

中斷

中斷是重要的異步處理事件機制,不然對於外設須要經過輪詢來確認外設的事件,太浪費cpu。中斷的引入讓外設可以「主動」通知操做系統,及打斷操做系統和應用的正常執行,讓操做系統完成外設的相關處理,而後在恢復操做系統和應用的正常執行,同時也是實現進程/線程搶佔式調度的一個重要基石。

(不論是用戶態仍是內核態)程序運行時,若中斷髮生就會打斷如今的程序,進行上下文切換轉入內核態並進入中斷服務程序,中斷服務程序執行完成後恢復被打斷的程序繼續執行,以下圖所示:

中斷的類型及中斷服務程序由「中斷向量表」來決定,在系統啓動時由內核負責加載這個向量。中斷類型又分爲:

  • 外部中斷

由CPU外部設備引發的外部事件如I/O中斷、時鐘中斷等是異步產生的(即產生的時刻不肯定),與CPU的執行無關,咱們稱之爲異步中斷(asynchronous interrupt)也稱外部中斷,簡稱中斷。(interrupt)

  • 異常

    此異常非語言中的異常,雖然語言異常會轉化稱爲中斷異常;

    把在CPU執行指令期間檢測到不正常的或非法的條件(如除零錯、地址訪問越界)所引發的內部事件稱做同步中斷(synchronous interrupt),也稱內部中斷,簡稱異常(exception)

    Intel架構中,中斷向量表中前20個單元定義爲異常,具體以下:

    從表中能夠看出,異常有如下類型:

    • 錯誤(fault)

      指令遇到一個能夠糾正的異常,而且處理器能夠從新啓動這條出現異常的指令,這種異常稱爲錯誤,好比「頁錯誤」。

    • 陷阱(trap)

      相似於錯誤,可是錯誤處理完成後返回發生陷阱指令以後的那條指令。

    • 停止(abort)

      不可重啓指令,好比上圖中的#8同一條指令發生兩次錯誤。

  • 系統調用

    把在程序中使用請求系統服務的系統調用而引起的事件,稱做陷入中斷(trap interrupt),也稱軟中斷(soft interrupt),**系統調用(system call)**簡稱trap。

    好比write函數;

XNU的中斷一般爲「陷阱」,這不一樣於異常中的「異常」;

自願的內核轉換,包括異常、系統調用,intel架構能夠經過INT指令觸發,在INT指令的參數傳入異常的編號便可(64位架構使用SYSCALL指令),arm架構經過SVC/SWI指令來觸發。好比INT 3指令(是一條單獨的指令),能夠來觸發斷點

一張圖就能夠了解總體過程,以下圖所示:

摘自清華大學《操做系統學習》課程

那中斷中的異常和崩潰有何關係?

程序的崩潰都會轉換爲異常被cpu經過中斷向量表指定的異常類型捕獲,進而觸發異常處理程序處理,好比cpu無效指令、無效的地址或者無權限的訪問,這些都是硬件產生的異常;被系統強殺的崩潰最終會調用到kill函數發送SIGKILL信號進而引起應用被強殺;語言及開發者觸發崩潰最終會經過abort函數毅然會最終調用到kill函數發送SIGABRT信號引起應用被殺,這都是軟件引起的異常

有哪些異常?

那崩潰具體有哪些異常?下面帶你們來一一探索,以下:

  • Mach異常

    這個後面跟你們詳解,暫且不用管

  • BSD Signal信號

    信號是什麼?信號是一種異步處理的軟中斷,內核會發送給進程某些異步事件,這些異步事件可能來自硬件,好比除0或者訪問了非法地址;也可能來自其餘進程或用戶輸入,好比ctrl+c,就會產生SIGINT信號由內核發送至當前終端執行進程,若進程未處理該信號,就會致使進程退出;ctrl+\就會產生SIGQUIT退出信號來終止進程執行,而且會產生崩潰日誌報告,以下圖所示:

    具體的demo以下:

    #include <stdio.h>
    #include <signal.h>
    #include <errno.h>
    #include <string.h>
    
    #define BUF_LEN 1024
    //信號處理函數
    static void sig_int(int signo) {
        printf("handler signo:%d \n", signo);
    }
    
    int main(int argc, char **argv) {
        char buf[BUF_LEN] = {0};
        //處理信號,SIGINT默認終止進程
        if (signal(SIGINT, sig_int) == SIG_ERR) {
            printf("signal error:%s \n", strerror(errno));
        }
        
        //接收終端輸入並打印輸出
        while (fgets(buf, sizeof(buf), stdin) != NULL) {
            if (fputs(buf, stdout) == EOF) {
                printf("fputs error:%s \n", strerror(errno));
                return 1;
            }
        }
        
        return 0;
    }
    複製代碼
  • 語言異常,OC/C++

    前面已提到語言異常最終會轉化爲信號SIGABRT進而引起應用終止;

  • 用戶拋出的異常

    好比前面提到的斷言也會調用abort函數轉化爲SIGABRT異常終止信號,該信號默認是終止進程並生成崩潰日誌報告,以下圖:

那最終不論是硬件異常仍是語言異常及用戶拋出異常都會產生信號,那信號與Mach異常有何關係?首先來帶你們瞭解下什麼是Mach異常。

Mach異常是什麼?

從名字來看就包含了Mach和「異常」,那Mach是什麼?你們有沒有這樣的疑問,我這裏給你們講解一下iOS/Mac操做系統框架,以下圖所示:

整個操做系統核心包括了Mach微內核、BSD層(你們熟悉的POSIX接口就在這一層)、I/O Kit設備驅動框架以及核心庫,其中Mach微內核負責進程和線程抽象、虛擬內存管理、任務管理以及進程間通訊和消息傳遞機制,因此Mach微內核就是整個操做系統的核心。

鴻蒙系統也是基於微內核

與你們熟知的Runloop中的Mach port消息就是基於Mach微內核的消息機制的體現,那Mach異常是什麼呢?以下圖所示:

RunLoop 的核心就是一個 mach_msg() ,RunLoop 調用這個函數去接收消息,若是沒有別人發送 port 消息過來,內核會將線程置於等待狀態。例如你在模擬器裏跑起一個 iOS 的 App,而後在 App 靜止時點擊暫停,你會看到主線程調用棧是停留在 mach_msg_trap() 這個地方。

Mach異常是在已有的消息傳遞架構上實現的一種獨有的異常處理方法,是一種輕量級的架構

聽起來很抽象,其實很簡單,一句話歸納就是整個的異常機制是構建在Mach異常之上的,全部的硬件/軟件異常都會首先轉換爲Mach異常,進而轉換爲信號。

咱們看到的崩潰日誌報告中的異常錯誤碼是否是包含了EXC_xxx,這裏對應的就是Mach異常,以下圖:

Mach異常是如何轉化爲BSD信號的呢?

Mach異常與信號轉換機制

一張圖概述:

流程主要分三個步驟:

  • 異常封裝、轉換、發送

    • 異常(好比硬件錯誤、軟件異常)發生時,就會由內核異常處理程序同步接收;
    • 內核異常處理程序不會針對不一樣異常單獨處理,而是統一經過exception_triage來處理,該函數會將異常信息(好比所在任務、線程、異常類型等)轉化爲Mach消息;
    • 再調用exception_deliver函數封裝封裝異常端口(包括線程、任務異常端口)、異常、異常錯誤碼、線程寄存器狀態等信息;
    • 再調用mach_exception_raise函數,該函數會經過mach_msg發送mach異常消息到異常端口;
  • 異常消息接收處理

    • 內核啓動建立第一個用戶態進程launchd的同時,會將進程的Mach異常消息重定向到異常端口

      因用戶態進程都是經過launchd進程fork克隆出來,同時也隨之繼承了異常端口;

    • 調用ux_handler_init來建立一個內核線程開啓ux_hanlder異常處理函數;

    • ux_handler函數會開啓消息循環來接收異常線程的Mach異常消息;

  • 異常消息轉換爲BSD信號

    • 內核線程循環接收異常消息,當接收到異常消息後,會調用mach_exc_server函數;
    • 該函數會調用catch_mach_exception_raise函數來捕獲異常消息,同時會調用ux_exception函數將異常消息轉換爲BSD信號;
    • 經過threadsignal函數來拋出信號;

其中關鍵點就是Mach異常消息經過消息發送的形式,發送到指定的異常端口,而該異常端口被內核線程持有,進而接收異常消息並轉換爲BSD信號。

信號處理

那最終信號如何處理呢?

上面咱們講到用戶態進程若指定了信號處理函數(好比SIGINT)則能夠本身來處理,若未指定呢?比較有意思的地方開始了,內核發現信號未存在異常處理函數,就會將其拋給崩潰報告守護進程ReportCrash,這裏能夠查看Mac的進程就會發現該進程,以下: 該進程負責來獲取異常消息及信號信息來生成崩潰日誌報告。那問題來了,既然異常消息是經過Mach消息的形式發送出去的,那我是否是能夠截獲這個消息呢? 答案是確定的!具體就是來註冊本身的異常端口來截獲異常消息,具體見demo

相關文章
相關標籤/搜索