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
是什麼?你們有沒有這樣的疑問,我這裏給你們講解一下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
信號的呢?
一張圖概述:
流程主要分三個步驟:
異常封裝、轉換、發送
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。