1 內核錯誤處理方式
當內核出現致命錯誤時,只要cpu還能正常運行,那么最重要的就是向用戶輸出詳細的錯誤信息,以及保存問題出現時的錯誤現場。以上致命錯誤可包含以下兩種類型:
(1)硬件能檢測到的錯誤,如非法內存訪問,非法指令等,此時cpu會觸發異常,并進入異常處理流程。在異常處理流程中會觸發oops或panic
(2)內核代碼進入某些代碼無法處理的異常分支,此時程序若繼續執行可能會導致無法預知的后果,此時相關的代碼會主動進入oops或panic
其中panic的含義為驚恐、恐慌,即內核將無法繼續進行,它會根據配置確定是否crash dump 內存,向關心panic事件的模塊發送notifier通知,以及打印panic相關的系統信息,最后將系統掛起或重啟。
oops的嚴重程度低于panic,因此在一般情況下其只是輸出相關的錯誤信息,并退出進程,而并不會掛起內核。但是若oops發生在中斷上下文,或內核配置了panic_on_oops選項,則它也會進入panic。
2 arm64異常信息寄存器
對于arm64架構,若cpu由于內存訪問錯誤等原因進入異常,則可通過esr寄存器獲取異常原因,并通過far寄存器獲取異常內存的地址信息。其中esr寄存器定義如下:
img
上圖中EC表示異常類型,如以下為其中的一些典型取值:
(1)b100000:來自低異常等級的指令錯誤,如用戶態的非法指令。
(2)b100001:當前異常等級的指令錯誤。
(3)b100010:pc對齊錯誤。
(4)b100100:來自低異常等級的data abort異常,如用戶態的內存異常。
(5)b100101:當前異常等級的data abort異常。
(6)b100110:棧指針sp對齊錯誤。
(7)b101111:serror中斷,它屬于異步異常,一般來自外部abort,如內存訪問總線時產生的abort異常等。
IL表示異常發生時的指令長度,其取值如下:
(1)0:表示16位的thumb指令長度
(2)1:表示32位的arm指令長度
ISS表示每種類型的具體原因,它的取值會根據EC的不同而不同,如以EC為data abort為例,其相應的ISS定義如下(具體含義可參考armv8 trm):
img
其中DFSC(data fault status code)用于給出data abort相關的信息,以下為其部分定義:
img
另外對于data abort類型異常,abort地址對于分析異常原因至關重要,因此armv8架構通過far寄存器提供了該地址的值(虛擬地址),其相應的寄存器定義如下:
img
3 異常處理流程
內核發生同步異常后,會根據異常發生時所處的異常等級(在當前異常等級,還是在低于當前異常等級中觸發),和其所使用的棧指針類型(sp_el0還是sp_el1),跳轉到相應的異常處理入口。
異常處理函數在執行一些上下文保存,棧指針切換等基礎工作后,將跳轉到特定類型的handler。如cpu在異常發生時處于arm64模式下,且使用的棧指針為sp_el1時,則其將會跳轉到el1h_64_sync_handler中。
該函數會根據esr_el1寄存器中EC中的值,獲取其對應的異常類型,然后調用特定異常類型相關的處理函數。在該函數中一般會通過esr_el1寄存器中ISS的值獲取其具體的異常原因,并執行相應的處理。
在處理流程中,若異常確實為非法操作引起(異常并不一定是錯誤,如缺頁異常,斷點、單步調試等debug異常都是正常的代碼處理邏輯),則會調用oops或panic向用戶報告錯誤,并退出當前進程或掛起系統。
由于內核的異常種類繁多,而其處理流程又大同小異,因此下面將以arm64模式下,內核非法地址訪問為例。其相應的處理流程如下:
img
3.1 data abort處理流程
el1h_64_sync_handler首先讀取esr_el1寄存器的值,然后解析其中EC的內容,并根據EC值調用其對應的處理函數,如對于data abort將會調用el1_abort,以下為其代碼實現:
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會調用do_mem_abort,該函數會根據esr_el1寄存器中DFSC的值,調用其相應的處理函數,這些函數通過以下所示的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)根據DFSC的值在fault_info數組中選擇其相應的處理函數指針
(2)由于arm64架構可利用虛擬地址空閑的高位bit存儲tag信息,以支持MTE特性。因此在獲取其實際虛擬地址時需要將相應的tag信息先移除
(3)調用fault_info中獲取到的回調函數,對于非法地址訪問錯誤,其相應的回調函數為do_translation_fault
(4)若異常為未知異常,則通過以下流程直接執行錯誤處理
do_translation_fault根據異常是由用戶態觸發還是內核態觸發,分別調用其對應等的處理函數,其代碼如下:
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)用戶態處理函數
(2)內核態處理函數
對于內核態情形,其最終會調用die_kernel_fault執行實際的錯誤處理,其代碼如下:
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)它會解析esr_el1寄存器的值,并分別打印其相關的內容,如EC、IL、DFSC等
(2)該函數會打印異常地址對應的頁表信息,包括pgd、p4d、pud、pmd和pte等
(3)執行實際的die操作,該流程將在下一節重點介紹
(4)殺死當前進程
3.2 die處理流程
die函數主要執行oops相關流程,且若異常為中斷流程中觸發或設置了panic_on_oops選項,則進一步通過panic將系統掛起。其主要流程如下:
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)調用die相關通知鏈對應的通知,使其執行die相關的操作,并打印oops相關的信息
(2)若需要crash系統,則通過該函數啟動一個新的crash內核,并通過新內核將系統內存信息dump出來,以供事后分析。如可通過kdump或ramdump方式配置相應的crash內核
(3)若異常發生在中斷中,或設置了panic_on_oops,則調用panic掛起系統
3.3 panic處理流程
當內核走到panic時表明其已無法繼續運行下去,因此需要執行一些系統掛死前的準備工作,其主要包含以下部分:
(1)在smp系統中,一個cpu正在處理panic時,可能另一個cpu也會觸發panic。而該流程主要用于一些錯誤信息收集、內存轉儲等工作,并不需要也不支持并發操作。因此對于后續觸發的cpu不需要執行該流程
(2)若正在使用kgdb對內核進行調試,則顯然希望調試器能繼續執行調試工作。故此時不會真正將系統掛死,而是將控制權轉交給調試器
(3)若內核配置了kdump等內存轉儲功能,則在panic時將啟動轉儲相關的流程
(4)在smp系統掛死之前,需要停止所有其它cpu的運行,以使系統真正地停下來
(5)最后,打印相關的系統信息后,使系統重啟或進入死循環
其相應的代碼實現如下:
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)若先前已經有cpu正在處理panic流程,則本cpu不再重復處理,只需將當前cpu停止
(2)打印panic原因信息
(3)若panic流程會執行內存轉儲,則所有系統相關信息都會被保存到轉儲文件中,因此就不需要調用后面的通知鏈,因此可直接調用轉儲操作。但是轉儲操作也不是100%保險,因此若不是對其絕對信任,則會設置_crash_kexec_post_notifiers,它會先執行通知鏈調用和log dump相關流程,再調用內核轉儲操作。
__crash_kexec函數會根據當前是否設置了轉儲內核確定是否實際執行轉儲操作,若執行轉儲則會通過kexec將系統切換到新的kdump內核,并且不再會返回。若不執行轉儲則繼續執行后續流程
(4 - 5)停止當前cpu之外的其它cpu運行
(6)調用關心panic事件相關模塊向其注冊的通知
(7)dump內核log buffer中的log信息
(8)若設置了_crash_kexec_post_notifiers,則根據是否設置了kexec內核,確定是否執行內存轉儲操作
(9)若不執行內存轉儲,則打印系統相關信息
(10)若設置了panic_timeout超時值,則執行超時等待操作
(11)若設置了panic_timeout超時值,在超時等待完成后重啟系統
(12)若未設置panic_timeout超時值,則將系統設置為死循環狀態,使其掛死
4 如何手動觸發oops和panic
在編碼流程中,可能有一些非期望的代碼分支,當系統進入這些分支表明出現了一些問題或嚴重錯誤。根據問題嚴重等級的不同,我們可能希望程序能打印一些警告信息,或者將系統設置為oops,甚至panic狀態。
為此,內核提供了一些相關的宏和函數用于支持上述需求,以下為其中一些常用的定義:
(1)WARN_ON():打印警告信息和調用棧,但不會進入oops或panic
(2)BUG_ON():打印bug相關信息,并進入oops流程
(3)panic():該函數將直接出發panic流程,將系統設置為掛死狀態
除了通過編碼方式以外,用戶還可以通過sysrq魔術鍵觸發panic流程,下面為通過proc方式觸發sysrq相關panic流程的命令:
echo?c?>?/proc/sysrq-trigger
編輯:黃飛
評論
查看更多