1 內(nèi)核錯(cuò)誤處理方式
當(dāng)內(nèi)核出現(xiàn)致命錯(cuò)誤時(shí),只要cpu還能正常運(yùn)行,那么最重要的就是向用戶輸出詳細(xì)的錯(cuò)誤信息,以及保存問題出現(xiàn)時(shí)的錯(cuò)誤現(xiàn)場。以上致命錯(cuò)誤可包含以下兩種類型:
(1)硬件能檢測到的錯(cuò)誤,如非法內(nèi)存訪問,非法指令等,此時(shí)cpu會(huì)觸發(fā)異常,并進(jìn)入異常處理流程。在異常處理流程中會(huì)觸發(fā)oops或panic
(2)內(nèi)核代碼進(jìn)入某些代碼無法處理的異常分支,此時(shí)程序若繼續(xù)執(zhí)行可能會(huì)導(dǎo)致無法預(yù)知的后果,此時(shí)相關(guān)的代碼會(huì)主動(dòng)進(jìn)入oops或panic
其中panic的含義為驚恐、恐慌,即內(nèi)核將無法繼續(xù)進(jìn)行,它會(huì)根據(jù)配置確定是否crash dump 內(nèi)存,向關(guān)心panic事件的模塊發(fā)送notifier通知,以及打印panic相關(guān)的系統(tǒng)信息,最后將系統(tǒng)掛起或重啟。
oops的嚴(yán)重程度低于panic,因此在一般情況下其只是輸出相關(guān)的錯(cuò)誤信息,并退出進(jìn)程,而并不會(huì)掛起內(nèi)核。但是若oops發(fā)生在中斷上下文,或內(nèi)核配置了panic_on_oops選項(xiàng),則它也會(huì)進(jìn)入panic。
2 arm64異常信息寄存器
對于arm64架構(gòu),若cpu由于內(nèi)存訪問錯(cuò)誤等原因進(jìn)入異常,則可通過esr寄存器獲取異常原因,并通過far寄存器獲取異常內(nèi)存的地址信息。其中esr寄存器定義如下:
img
上圖中EC表示異常類型,如以下為其中的一些典型取值:
(1)b100000:來自低異常等級(jí)的指令錯(cuò)誤,如用戶態(tài)的非法指令。
(2)b100001:當(dāng)前異常等級(jí)的指令錯(cuò)誤。
(3)b100010:pc對齊錯(cuò)誤。
(4)b100100:來自低異常等級(jí)的data abort異常,如用戶態(tài)的內(nèi)存異常。
(5)b100101:當(dāng)前異常等級(jí)的data abort異常。
(6)b100110:棧指針sp對齊錯(cuò)誤。
(7)b101111:serror中斷,它屬于異步異常,一般來自外部abort,如內(nèi)存訪問總線時(shí)產(chǎn)生的abort異常等。
IL表示異常發(fā)生時(shí)的指令長度,其取值如下:
(1)0:表示16位的thumb指令長度
(2)1:表示32位的arm指令長度
ISS表示每種類型的具體原因,它的取值會(huì)根據(jù)EC的不同而不同,如以EC為data abort為例,其相應(yīng)的ISS定義如下(具體含義可參考armv8 trm):
img
其中DFSC(data fault status code)用于給出data abort相關(guān)的信息,以下為其部分定義:
img
另外對于data abort類型異常,abort地址對于分析異常原因至關(guān)重要,因此armv8架構(gòu)通過far寄存器提供了該地址的值(虛擬地址),其相應(yīng)的寄存器定義如下:
img
3 異常處理流程
內(nèi)核發(fā)生同步異常后,會(huì)根據(jù)異常發(fā)生時(shí)所處的異常等級(jí)(在當(dāng)前異常等級(jí),還是在低于當(dāng)前異常等級(jí)中觸發(fā)),和其所使用的棧指針類型(sp_el0還是sp_el1),跳轉(zhuǎn)到相應(yīng)的異常處理入口。
異常處理函數(shù)在執(zhí)行一些上下文保存,棧指針切換等基礎(chǔ)工作后,將跳轉(zhuǎn)到特定類型的handler。如cpu在異常發(fā)生時(shí)處于arm64模式下,且使用的棧指針為sp_el1時(shí),則其將會(huì)跳轉(zhuǎn)到el1h_64_sync_handler中。
該函數(shù)會(huì)根據(jù)esr_el1寄存器中EC中的值,獲取其對應(yīng)的異常類型,然后調(diào)用特定異常類型相關(guān)的處理函數(shù)。在該函數(shù)中一般會(huì)通過esr_el1寄存器中ISS的值獲取其具體的異常原因,并執(zhí)行相應(yīng)的處理。
在處理流程中,若異常確實(shí)為非法操作引起(異常并不一定是錯(cuò)誤,如缺頁異常,斷點(diǎn)、單步調(diào)試等debug異常都是正常的代碼處理邏輯),則會(huì)調(diào)用oops或panic向用戶報(bào)告錯(cuò)誤,并退出當(dāng)前進(jìn)程或掛起系統(tǒng)。
由于內(nèi)核的異常種類繁多,而其處理流程又大同小異,因此下面將以arm64模式下,內(nèi)核非法地址訪問為例。其相應(yīng)的處理流程如下:
img
3.1 data abort處理流程
el1h_64_sync_handler首先讀取esr_el1寄存器的值,然后解析其中EC的內(nèi)容,并根據(jù)EC值調(diào)用其對應(yīng)的處理函數(shù),如對于data abort將會(huì)調(diào)用el1_abort,以下為其代碼實(shí)現(xiàn):
asmlinkage?void?noinstr?el1h_64_sync_handler(struct?pt_regs?*regs) { ?unsigned?long?esr?=?read_sysreg(esr_el1);?????????????????? ?switch?(ESR_ELx_EC(esr))?{??????????????????????????? ?case?ESR_ELx_EC_DABT_CUR: ?case?ESR_ELx_EC_IABT_CUR: ??el1_abort(regs,?esr); ??break; ?case?ESR_ELx_EC_PC_ALIGN: ??el1_pc(regs,?esr); ??break; ?… ?default: ??__panic_unhandled(regs,?"64-bit?el1h?sync",?esr); ?} }
el1_abort會(huì)調(diào)用do_mem_abort,該函數(shù)會(huì)根據(jù)esr_el1寄存器中DFSC的值,調(diào)用其相應(yīng)的處理函數(shù),這些函數(shù)通過以下所示的fault_info變量定義:
static?const?struct?fault_info?fault_info[]?=?{ ?… ?{?do_translation_fault,?SIGSEGV,?SEGV_MAPERR,?"level?0?translation?fault"??}, ?{?do_translation_fault,?SIGSEGV,?SEGV_MAPERR,?"level?1?translation?fault"??}, ?{?do_translation_fault,?SIGSEGV,?SEGV_MAPERR,?"level?2?translation?fault"??}, ?{?do_translation_fault,?SIGSEGV,?SEGV_MAPERR,?"level?3?translation?fault"??}, ?{?do_bad,??SIGKILL,?SI_KERNEL,?"unknown?8"???}, ?{?do_page_fault,?SIGSEGV,?SEGV_ACCERR,?"level?1?access?flag?fault"?}, ?{?do_page_fault,?SIGSEGV,?SEGV_ACCERR,?"level?2?access?flag?fault"?}, ?{?do_page_fault,?SIGSEGV,?SEGV_ACCERR,?"level?3?access?flag?fault"?}, ?… }
以下為do_mem_abort的代碼流程:
void?do_mem_abort(unsigned?long?far,?unsigned?int?esr,?struct?pt_regs?*regs) { ?const?struct?fault_info?*inf?=?esr_to_fault_info(esr);??????????(1) ?unsigned?long?addr?=?untagged_addr(far);????????????????????????(2) ?if?(!inf->fn(far,?esr,?regs))???????????????????????????????????(3) ??return; ?if?(!user_mode(regs))?{?????????????????????????????????????????(4) ??pr_alert("Unhandled?fault?at?0x%016lx ",?addr); ??mem_abort_decode(esr); ??show_pte(addr); ?} ?arm64_notify_die(inf->name,?regs,?inf->sig,?inf->code,?addr,?esr); }
(1)根據(jù)DFSC的值在fault_info數(shù)組中選擇其相應(yīng)的處理函數(shù)指針
(2)由于arm64架構(gòu)可利用虛擬地址空閑的高位bit存儲(chǔ)tag信息,以支持MTE特性。因此在獲取其實(shí)際虛擬地址時(shí)需要將相應(yīng)的tag信息先移除
(3)調(diào)用fault_info中獲取到的回調(diào)函數(shù),對于非法地址訪問錯(cuò)誤,其相應(yīng)的回調(diào)函數(shù)為do_translation_fault
(4)若異常為未知異常,則通過以下流程直接執(zhí)行錯(cuò)誤處理
do_translation_fault根據(jù)異常是由用戶態(tài)觸發(fā)還是內(nèi)核態(tài)觸發(fā),分別調(diào)用其對應(yīng)等的處理函數(shù),其代碼如下:
static?int?__kprobes?do_translation_fault(unsigned?long?far, ???????unsigned?int?esr, ???????struct?pt_regs?*regs) { ?… ?if?(is_ttbr0_addr(addr)) ??return?do_page_fault(far,?esr,?regs);???????????????(1) ?do_bad_area(far,?esr,?regs);????????????????????????????????(2) ?return?0; }
(1)用戶態(tài)處理函數(shù)
(2)內(nèi)核態(tài)處理函數(shù)
對于內(nèi)核態(tài)情形,其最終會(huì)調(diào)用die_kernel_fault執(zhí)行實(shí)際的錯(cuò)誤處理,其代碼如下:
static?void?die_kernel_fault(const?char?*msg,?unsigned?long?addr, ????????unsigned?int?esr,?struct?pt_regs?*regs) { ?… ?mem_abort_decode(esr);?????????????????????????????(1) ?show_pte(addr);????????????????????????????????????(2) ?die("Oops",?regs,?esr);????????????????????????????(3) ?bust_spinlocks(0); ?do_exit(SIGKILL);??????????????????????????????????(4) }
(1)它會(huì)解析esr_el1寄存器的值,并分別打印其相關(guān)的內(nèi)容,如EC、IL、DFSC等
(2)該函數(shù)會(huì)打印異常地址對應(yīng)的頁表信息,包括pgd、p4d、pud、pmd和pte等
(3)執(zhí)行實(shí)際的die操作,該流程將在下一節(jié)重點(diǎn)介紹
(4)殺死當(dāng)前進(jìn)程
3.2 die處理流程
die函數(shù)主要執(zhí)行oops相關(guān)流程,且若異常為中斷流程中觸發(fā)或設(shè)置了panic_on_oops選項(xiàng),則進(jìn)一步通過panic將系統(tǒng)掛起。其主要流程如下:
void?die(const?char?*str,?struct?pt_regs?*regs,?int?err) { ?… ?ret?=?__die(str,?err,?regs);??????????????????????????????????(1) ?if?(regs?&&?kexec_should_crash(current)) ??crash_kexec(regs);????????????????????????????????????(2) ?… ?if?(in_interrupt()) ??panic("%s:?Fatal?exception?in?interrupt",?str); ?if?(panic_on_oops)????????????????????????????????????????????(3) ??panic("%s:?Fatal?exception",?str); ?… }
(1)調(diào)用die相關(guān)通知鏈對應(yīng)的通知,使其執(zhí)行die相關(guān)的操作,并打印oops相關(guān)的信息
(2)若需要crash系統(tǒng),則通過該函數(shù)啟動(dòng)一個(gè)新的crash內(nèi)核,并通過新內(nèi)核將系統(tǒng)內(nèi)存信息dump出來,以供事后分析。如可通過kdump或ramdump方式配置相應(yīng)的crash內(nèi)核
(3)若異常發(fā)生在中斷中,或設(shè)置了panic_on_oops,則調(diào)用panic掛起系統(tǒng)
3.3 panic處理流程
當(dāng)內(nèi)核走到panic時(shí)表明其已無法繼續(xù)運(yùn)行下去,因此需要執(zhí)行一些系統(tǒng)掛死前的準(zhǔn)備工作,其主要包含以下部分:
(1)在smp系統(tǒng)中,一個(gè)cpu正在處理panic時(shí),可能另一個(gè)cpu也會(huì)觸發(fā)panic。而該流程主要用于一些錯(cuò)誤信息收集、內(nèi)存轉(zhuǎn)儲(chǔ)等工作,并不需要也不支持并發(fā)操作。因此對于后續(xù)觸發(fā)的cpu不需要執(zhí)行該流程
(2)若正在使用kgdb對內(nèi)核進(jìn)行調(diào)試,則顯然希望調(diào)試器能繼續(xù)執(zhí)行調(diào)試工作。故此時(shí)不會(huì)真正將系統(tǒng)掛死,而是將控制權(quán)轉(zhuǎn)交給調(diào)試器
(3)若內(nèi)核配置了kdump等內(nèi)存轉(zhuǎn)儲(chǔ)功能,則在panic時(shí)將啟動(dòng)轉(zhuǎn)儲(chǔ)相關(guān)的流程
(4)在smp系統(tǒng)掛死之前,需要停止所有其它c(diǎn)pu的運(yùn)行,以使系統(tǒng)真正地停下來
(5)最后,打印相關(guān)的系統(tǒng)信息后,使系統(tǒng)重啟或進(jìn)入死循環(huán)
其相應(yīng)的代碼實(shí)現(xiàn)如下:
void?panic(const?char?*fmt,?...) { ?… ?this_cpu?=?raw_smp_processor_id(); ?old_cpu??=?atomic_cmpxchg(&panic_cpu,?PANIC_CPU_INVALID,?this_cpu); ?if?(old_cpu?!=?PANIC_CPU_INVALID?&&?old_cpu?!=?this_cpu)???????????????????????(1) ??panic_smp_self_stop(); ?… ?pr_emerg("Kernel?panic?-?not?syncing:?%s ",?buf); ?… ?kgdb_panic(buf);???????????????????????????????????????????????????????????????(2) ?if?(!_crash_kexec_post_notifiers)?{ ??printk_safe_flush_on_panic(); ??__crash_kexec(NULL);???????????????????????????????????????????????????(3) ??smp_send_stop();???????????????????????????????????????????????????????(4) ?}?else?{ ??crash_smp_send_stop();?????????????????????????????????????????????????(5) ?} ?atomic_notifier_call_chain(&panic_notifier_list,?0,?buf);??????????????????????(6) ?printk_safe_flush_on_panic(); ?kmsg_dump(KMSG_DUMP_PANIC);????????????????????????????????????????????????????(7) ?if?(_crash_kexec_post_notifiers) ??__crash_kexec(NULL);???????????????????????????????????????????????????(8) ?… ?panic_print_sys_info();????????????????????????????????????????????????????????(9) ?if?(!panic_blink) ??panic_blink?=?no_blink; ?if?(panic_timeout?>?0)?{ ??pr_emerg("Rebooting?in?%d?seconds.. ",?panic_timeout); ??for?(i?=?0;?i?=?i_next)?{ ????i?+=?panic_blink(state?^=?1); ????i_next?=?i?+?3600?/?PANIC_BLINK_SPD; ???} ???mdelay(PANIC_TIMER_STEP);??????????????????????????????????????(10) ??} ?} ?if?(panic_timeout?!=?0)?{ ??if?(panic_reboot_mode?!=?REBOOT_UNDEFINED) ???reboot_mode?=?panic_reboot_mode; ??emergency_restart();???????????????????????????????????????????????????(11) ?} ?… ?pr_emerg("---[?end?Kernel?panic?-?not?syncing:?%s?]--- ",?buf); ?suppress_printk?=?1; ?local_irq_enable(); ?for?(i?=?0;?;?i?+=?PANIC_TIMER_STEP)?{ ??touch_softlockup_watchdog(); ??if?(i?>=?i_next)?{ ???i?+=?panic_blink(state?^=?1); ???i_next?=?i?+?3600?/?PANIC_BLINK_SPD; ??} ??mdelay(PANIC_TIMER_STEP);??????????????????????????????????????????????(12) ?} }
(1)若先前已經(jīng)有cpu正在處理panic流程,則本cpu不再重復(fù)處理,只需將當(dāng)前cpu停止
(2)打印panic原因信息
(3)若panic流程會(huì)執(zhí)行內(nèi)存轉(zhuǎn)儲(chǔ),則所有系統(tǒng)相關(guān)信息都會(huì)被保存到轉(zhuǎn)儲(chǔ)文件中,因此就不需要調(diào)用后面的通知鏈,因此可直接調(diào)用轉(zhuǎn)儲(chǔ)操作。但是轉(zhuǎn)儲(chǔ)操作也不是100%保險(xiǎn),因此若不是對其絕對信任,則會(huì)設(shè)置_crash_kexec_post_notifiers,它會(huì)先執(zhí)行通知鏈調(diào)用和log dump相關(guān)流程,再調(diào)用內(nèi)核轉(zhuǎn)儲(chǔ)操作。
__crash_kexec函數(shù)會(huì)根據(jù)當(dāng)前是否設(shè)置了轉(zhuǎn)儲(chǔ)內(nèi)核確定是否實(shí)際執(zhí)行轉(zhuǎn)儲(chǔ)操作,若執(zhí)行轉(zhuǎn)儲(chǔ)則會(huì)通過kexec將系統(tǒng)切換到新的kdump內(nèi)核,并且不再會(huì)返回。若不執(zhí)行轉(zhuǎn)儲(chǔ)則繼續(xù)執(zhí)行后續(xù)流程
(4 - 5)停止當(dāng)前cpu之外的其它c(diǎn)pu運(yùn)行
(6)調(diào)用關(guān)心panic事件相關(guān)模塊向其注冊的通知
(7)dump內(nèi)核log buffer中的log信息
(8)若設(shè)置了_crash_kexec_post_notifiers,則根據(jù)是否設(shè)置了kexec內(nèi)核,確定是否執(zhí)行內(nèi)存轉(zhuǎn)儲(chǔ)操作
(9)若不執(zhí)行內(nèi)存轉(zhuǎn)儲(chǔ),則打印系統(tǒng)相關(guān)信息
(10)若設(shè)置了panic_timeout超時(shí)值,則執(zhí)行超時(shí)等待操作
(11)若設(shè)置了panic_timeout超時(shí)值,在超時(shí)等待完成后重啟系統(tǒng)
(12)若未設(shè)置panic_timeout超時(shí)值,則將系統(tǒng)設(shè)置為死循環(huán)狀態(tài),使其掛死
4 如何手動(dòng)觸發(fā)oops和panic
在編碼流程中,可能有一些非期望的代碼分支,當(dāng)系統(tǒng)進(jìn)入這些分支表明出現(xiàn)了一些問題或嚴(yán)重錯(cuò)誤。根據(jù)問題嚴(yán)重等級(jí)的不同,我們可能希望程序能打印一些警告信息,或者將系統(tǒng)設(shè)置為oops,甚至panic狀態(tài)。
為此,內(nèi)核提供了一些相關(guān)的宏和函數(shù)用于支持上述需求,以下為其中一些常用的定義:
(1)WARN_ON():打印警告信息和調(diào)用棧,但不會(huì)進(jìn)入oops或panic
(2)BUG_ON():打印bug相關(guān)信息,并進(jìn)入oops流程
(3)panic():該函數(shù)將直接出發(fā)panic流程,將系統(tǒng)設(shè)置為掛死狀態(tài)
除了通過編碼方式以外,用戶還可以通過sysrq魔術(shù)鍵觸發(fā)panic流程,下面為通過proc方式觸發(fā)sysrq相關(guān)panic流程的命令:
echo?c?>?/proc/sysrq-trigger
編輯:黃飛
評(píng)論