內核穩定性問題復雜多樣,最常見的莫過於“kernel panic”,意為“內核恐慌,不知所措”。這種情況下系統自然無法正常運轉,只能自我結束生命,留下死亡信息。諸如:
“Unable to handle kernel XXX at virtual address XXX”
“undefined instruction XXX”
“Bad mode in Error handler detected on CPUX, code 0xbe000011 -- SError”
......
這些死亡信息是系統在什么狀態下產生?如何產生?以及如何處理?本文主要從這三個方面介紹ARMv8架構下CPU的異常處理流程。
一、ARMv8異常簡介
1.異常級別
不同於Armv7架構采用CPU模式切換的方式進行異常處理,Armv8架構定義了一組全新的異常級別進行異常處理,即EL0至EL3,有如下特性:
-
如果ELn為異常級別,則n的值增加表示軟件執行特權增加。
-
EL0處的執行稱為無特權執行,不能處理異常。
-
EL2提供對虛擬化的支持。
-
EL3提供了在兩個安全狀態(安全狀態和非安全狀態)之間切換的支持。
一個實現可以不包括所有的異常級別,但都必須包括EL0和EL1。EL2和EL3是可選的。
如下是典型的異常級別使用模型:
2. 同步異常和異步異常
如果滿足以下所有條件,則將異常描述為同步的:
-
由於直接執行某個指令而產生異常。
-
異常處理程序的返回地址可以表明導致該異常的指令。
-
異常是精確的。
如果滿足以下任一條件,則將異常描述為異步的:
-
不是因為直接執行某條指令而產生異常。
-
異常處理程序的返回地址不可以表明導致該異常的指令。
-
異常是不精確的。
3. 主要寄存器
(1)通用寄存器R0-R30
在基本指令集處理指令時,將使用通用寄存器組。它包括31個通用寄存器R0-R30。這些寄存器可以作為31個64位寄存器X0-X30或31個32位寄存器W0-W30進行訪問。
(2)堆棧指針寄存器SP
在AArch64狀態下,除了通用寄存器外,還為以下每個異常級別實現了專用的堆棧指針寄存器,
堆棧指針寄存器為:
-
SP_EL0和SP_EL1。
-
如果實現包括EL2,則為SP_EL2。
-
如果實現包括EL3,則為SP_EL3。
堆棧指針寄存器選擇:
在EL0上執行時,處理器使用EL0堆棧指針SP_EL0。在其他任何異常級別執行時,可以將處理器配置為使用SP_EL0或配置為對應該異常級別的堆棧指針SP_ELx。默認情況下,采用目標異常級別的堆棧指針SP_ELx。例如,EL1的異常選擇SP_EL1,軟件可以在目標異常級別執行的時候通過更新PSTATE.SP來指向SP_EL0的堆棧指針。
可以通過異常級別的堆棧指針后綴表明所選的堆棧指針:
t表明使用SP_EL0堆棧指針。
h表明使用SP_ELx堆棧指針。
t和h后綴基於線程(thread)和處理程序(handler)的首字母。
(3)保存的程序狀態寄存器SPSR
保存的程序狀態寄存器SPSR(Saved Program Status Registers)用於在發生異常時保存處理器狀態。在AArch64狀態下,每個異常級別都有一個SPSR:
-
SPSR_EL1,發生在EL1的異常。
-
如果實現了EL2,則為SPSR_EL2,發生在EL2的異常。
-
如果實現了EL3,則為SPSR_EL3,發生在EL3的異常。
注:EL0不能處理異常。
當處理器發生異常時,會將處理器狀態從SPSTATE中的PSTATE(Process state)保存到對應異常級別的SPSR。例如,如果異常發生在EL1,則將處理器狀態保存在SPSR_EL1中。
保存處理器狀態意味着異常處理程序可以:
-
從異常返回時,將處理器狀態恢復到SPSR中存儲的異常級別的狀態。例如,異常處理程序從EL1返回時,處理器狀態恢復到存儲在SPSR_EL1中的狀態。
-
檢查發生異常時PSTATE的值,確定引起異常指令的當前執行狀態和異常級別,例如,當前執行狀態是AArch64還是AArch32等。
(4)異常鏈接寄存器(ELR)
異常鏈接寄存器ELR(Exception Link Registers)包含異常返回地址。當處理器發生異常時,返回地址將保存在異常級別對應的ELR中。例如,當處理器將異常處理交給EL1處理時,會將異常返回地址保存在ELR_EL1中。在異常返回時,PC恢復到存儲在ELR中的地址。例如,從EL1返回時,PC將恢復到ELR_EL1中存儲的地址。
AArch64狀態為每個異常級別都提供了ELR寄存器:
-
ELR_EL1,用於EL1的異常。
-
如果實現了EL2,ELR_EL2用於EL2的異常。
-
如果實現了EL3,ELR_EL3用於EL3的異常。
(5)ESR(Exception Syndrome Register)
異常綜合表征寄存器ESR_ELn包含的異常信息用以異常處理程序確定異常原因。僅針對同步異常和SError進行更新。因為IRQ或FIQ中斷處理程序從通用中斷控制器(GIC)寄存器的信息獲取狀態。
-
ESR_ELn的BIT[31:26]指示處理程序執行對應的異常,比如:
EC == 0b100010,PC alignment fault exception.
EC == 0b100101,Data Abort taken without a change in Exception level.
EC == 0b101111,SError interrupt.
-
位[25]表示被捕獲指令的長度(0為16位指令,1為32位指令)
-
位[24:0]構成ISS域(Instruction Specific Syndrome),根據EC域指定的不同異常類型,ISS有不用的解釋。有:
ISS encoding for an exception from an Instruction Abort
ISS encoding for an exception from a Data Abort
ISS encoding for an SError interrupt
ISS encoding for an exception from a WFI or WFE instruction.
等等。
以 Data Abort為例,ISS解讀如下:
BIT[5:0] DFSC(Data Fault Status Code)解釋了data abort發生的狀態信息:
*其他bit位解釋可以參考ARM v8手冊<DDI0487F_a_armv8_arm>第10.2.6章節
4.異常入口
每個異常都有特定的異常級別。異常所對應的異常級別是由軟件編程決定,或者由異常自身性質決定的。在任何情況下,異常執行時都不會移至較低的異常級別。異常入口的基本執行內容是:
-
處理器狀態保存到目標異常級別的SPSR_ELx中。
-
返回地址保存到目標異常級別的ELR_ELx中。
-
如果異常是同步異常或SError中斷,異常的表征信息將保存在目標異常級別的ESR_ELx中。
-
如果是指令止異常(Instruction Abort exception),數據中止異常(Data Abort exception,),PC對齊錯誤異常(PC alignment fault exception),故障的虛擬地址將保存在FAR_ELx中。
-
堆棧指針保存到目標異常級別的專用堆棧指針寄存器SP_ELx。
-
執行移至目標異常級別,並從異常向量定義的地址開始執行。
二、異常處理流程
1.異常向量表
當發生異常時,處理器必須執行與之對應的處理程序。處理程序在內存中的存儲位置稱為異常向量。在ARM體系結構中,異常向量存儲在一個表中,該表稱為異常向量表。每個異常級別都有其自己的向量表,即EL3,EL2和EL1都有一個,該表包含要執行的指令。
每個表占128個字節,可以保存32條指令(arm64的指令長度也是4字節),以linux kernel-4.19/arch/arm64/kernel/entry.S為例,異常向量表的入口如下圖,一共有4組16個表:
用另外一張表可以更好理解這個異常向量表的入口:
比如當前代碼運行在內核空間,發生了data abort,異常向量表的入口地址就是0x200。
2.kernel_ventry
異常發生后,處理器從對應的異常向量表入口地址開始執行,第一條指令是kernel_ventry。kernel_ventry是一個宏定義,先檢查棧空間是否有溢出,然后跳轉到指定的異常處理標簽。
以下以EL1發生data abort異常為例介紹異常處理流程。
EL1發生data abort異常后進入對應的異常向量表入口,先檢查棧是否有溢出,然后跳轉至:el1_sync(data abort屬於同步異常)。
3.elx_sync
(1)保存現場
el1_sync第一條指令執行kernel_entry 1。kernel_entry也是一個宏定義,首先將CPU寄存器保存到棧空間,因為這些寄存器接下來會被覆蓋使用。為了保證kernel_exit時能恢復准確的現場,這里有必要對第一現場先做保存。
其次設置棧幀大小S_FRAM_SIZE,S_FRAM_SIZE根據pt_regs結構體大小而設定。
pt_regs結構體:
另外就是讀取elr_el1和spsr_el1等寄存器值。總之,kernel_entry主要將CPU寄存器按照pt_regs結構體的定義將異常第一現場保存到棧上。
(2)判斷異常類型
kernel_entry保存完第一現場之后,接下來讀取esr_el1寄存器的值,並判斷異常的具體類型。如2.3.5章節所描述的ESR寄存器定義,ESR包含的異常信息主要用於異常處理程序確定異常原因,其中ESR_ELn的BIT[31:26] EC域指示處理程序執行的對應異常類型。
發生DataAbort時,EC = 0b100101,即ESR_ELx_EC_DABT_CUR=0x25,el1_syn將跳轉至el1_da。
ESR_ELx_EC_DABT_CUR定義在/kernel/msm-4.19/arch/arm64/include/asm/esr.h。
除此之外,還有其他的同步異常類型,比如:
(3)跳轉至異常類型標簽
通過esr_el1寄存器值確定同步異常的具體類型后,跳轉至對應的異常處理標簽el1_da。el1_da第一條指令,mrs x3,far_el1,將far_el1保存到x3。在2.4異常入口章節介紹過如果發生數據中止異常(DataAbort exception),故障的虛擬地址將保存在FAR_ELx中。這里就是首先將data abort異常發生的虛擬地址第一時間取出,保持在x3中。
el1_da 跳轉到異常處理程序do_mem_abort之前,為其設置好了三個入參:
-
x0:產生DataAbort的故障虛擬地址。
-
x1:esr_el1,異常綜合表征寄存器值,在el1_sync第二條指令已經保存。
-
x2:stack frame 地址,即pt_regs結構體的首地址,在kernel_entry已對其填充。
x0~x2分別對應do_mem_abort函數的三個參數:addr,esr,*regs。
(4)跳轉至異常處理程序
do_mem_abort 函數位於/kernel/msm-4.19/arch/arm64/mm/fault.c
do_mem_abort首先根據esr寄存器獲取data abort fault_info。在2.3.5章節介紹了ESR寄存器BIT[24:0]的ISS域,它記錄了data abort的具體類型。這里將用到ISS域,也就是fault_info中的name變量。我們通常看到的“do_page_fault”、“do_translation_fault”等data abort下細分的調用棧就是由這里的ISS域區分而來。
fault_info結構體:
Fault_info[]數組:
fault_info 數組中對應的處理函數對當前的異常進一步處理,如果發現當前的data abort確實是屬於非法,無法處理的,比如paging request 非法地址,就會拋出異常信息,並走到panic流程。
最后,調用arm64_notify_die,如果是用戶空間發生data abort,輸出異常信息和發送signal給當前進程。如果是異常發生在內核空間,走die流程。
die函數最終可能會調用到panic。但die函數也不是一定會走到panic,它先是走oops流程告警系統現在的異常,如果異常發生在中斷上下文,走panic。或者如果設定了CONFIG_PANIC_ON_OOPS_VALUE=y,無論是否在中斷上下文均走panic。
如果此次異常並沒有走到panic流程,那么系統還是要繼續運行,拋出oops警告后系統如何恢復異常發生前的環境?回到el1_da處理指令,do_mem_abort執行完如果不需要panic,跳轉到kernel_exit進行異常退出處理。
4.kernel_exit
kernel_exit恢復現場。主要恢復kernel_entry保存在棧上的處理器相關寄存器等。至此發生在el1級別的data baort異常處理流程分析結束。
參考資料
[1]《DDI0487F_a_armv8_arm.pdf》
[2]《DEN0024A_v8_architecture_PG.pdf》

“內核工匠”微信公眾號
Linux 內核黑科技 | 技術文章 | 精選教程