深入講解ARMv8 異常處理簡介
內(nèi)核穩(wěn)定性問題復(fù)雜多樣,最常見的莫過于“kernel panic”,意為“內(nèi)核恐慌,不知所措”。這種情況下系統(tǒng)自然無法正常運(yùn)轉(zhuǎn),只能自我結(jié)束生命,留下死亡信息。諸如:
“Unable to handle kernel XXX at virtual address XXX”
“undefined instruction XXX”
“Bad mode in Error handler detected on CPUX, code 0xbe000011 -- SError”
......
這些死亡信息是系統(tǒng)在什么狀態(tài)下產(chǎn)生?如何產(chǎn)生?以及如何處理?本文主要從這三個(gè)方面介紹ARMv8架構(gòu)下CPU的異常處理流程。
一、ARMv8異常簡介
1.異常級(jí)別
不同于Armv7架構(gòu)采用CPU模式切換的方式進(jìn)行異常處理,Armv8架構(gòu)定義了一組全新的異常級(jí)別進(jìn)行異常處理,即EL0至EL3,有如下特性:
如果ELn為異常級(jí)別,則n的值增加表示軟件執(zhí)行特權(quán)增加。
EL0處的執(zhí)行稱為無特權(quán)執(zhí)行,不能處理異常。
EL2提供對(duì)虛擬化的支持。
EL3提供了在兩個(gè)安全狀態(tài)(安全狀態(tài)和非安全狀態(tài))之間切換的支持。
一個(gè)實(shí)現(xiàn)可以不包括所有的異常級(jí)別,但都必須包括EL0和EL1。EL2和EL3是可選的。
如下是典型的異常級(jí)別使用模型:

2. 同步異常和異步異常
如果滿足以下所有條件,則將異常描述為同步的:
由于直接執(zhí)行某個(gè)指令而產(chǎn)生異常。
異常處理程序的返回地址可以表明導(dǎo)致該異常的指令。
異常是精確的。
如果滿足以下任一條件,則將異常描述為異步的:
不是因?yàn)橹苯訄?zhí)行某條指令而產(chǎn)生異常。
異常處理程序的返回地址不可以表明導(dǎo)致該異常的指令。
異常是不精確的。
3. 主要寄存器
(1)通用寄存器R0-R30
在基本指令集處理指令時(shí),將使用通用寄存器組。它包括31個(gè)通用寄存器R0-R30。這些寄存器可以作為31個(gè)64位寄存器X0-X30或31個(gè)32位寄存器W0-W30進(jìn)行訪問。
(2)堆棧指針寄存器SP
在AArch64狀態(tài)下,除了通用寄存器外,還為以下每個(gè)異常級(jí)別實(shí)現(xiàn)了專用的堆棧指針寄存器,
堆棧指針寄存器為:
SP_EL0和SP_EL1。
如果實(shí)現(xiàn)包括EL2,則為SP_EL2。
如果實(shí)現(xiàn)包括EL3,則為SP_EL3。
堆棧指針寄存器選擇:
在EL0上執(zhí)行時(shí),處理器使用EL0堆棧指針SP_EL0。在其他任何異常級(jí)別執(zhí)行時(shí),可以將處理器配置為使用SP_EL0或配置為對(duì)應(yīng)該異常級(jí)別的堆棧指針SP_ELx。默認(rèn)情況下,采用目標(biāo)異常級(jí)別的堆棧指針SP_ELx。例如,EL1的異常選擇SP_EL1,軟件可以在目標(biāo)異常級(jí)別執(zhí)行的時(shí)候通過更新PSTATE.SP來指向SP_EL0的堆棧指針。
可以通過異常級(jí)別的堆棧指針后綴表明所選的堆棧指針:
t表明使用SP_EL0堆棧指針。
h表明使用SP_ELx堆棧指針。
t和h后綴基于線程(thread)和處理程序(handler)的首字母。

(3)保存的程序狀態(tài)寄存器SPSR
保存的程序狀態(tài)寄存器SPSR(Saved Program Status Registers)用于在發(fā)生異常時(shí)保存處理器狀態(tài)。在AArch64狀態(tài)下,每個(gè)異常級(jí)別都有一個(gè)SPSR:
SPSR_EL1,發(fā)生在EL1的異常。
如果實(shí)現(xiàn)了EL2,則為SPSR_EL2,發(fā)生在EL2的異常。
如果實(shí)現(xiàn)了EL3,則為SPSR_EL3,發(fā)生在EL3的異常。
注:EL0不能處理異常。
當(dāng)處理器發(fā)生異常時(shí),會(huì)將處理器狀態(tài)從SPSTATE中的PSTATE(Process state)保存到對(duì)應(yīng)異常級(jí)別的SPSR。例如,如果異常發(fā)生在EL1,則將處理器狀態(tài)保存在SPSR_EL1中。
保存處理器狀態(tài)意味著異常處理程序可以:
從異常返回時(shí),將處理器狀態(tài)恢復(fù)到SPSR中存儲(chǔ)的異常級(jí)別的狀態(tài)。例如,異常處理程序從EL1返回時(shí),處理器狀態(tài)恢復(fù)到存儲(chǔ)在SPSR_EL1中的狀態(tài)。
檢查發(fā)生異常時(shí)PSTATE的值,確定引起異常指令的當(dāng)前執(zhí)行狀態(tài)和異常級(jí)別,例如,當(dāng)前執(zhí)行狀態(tài)是AArch64還是AArch32等。
(4)異常鏈接寄存器(ELR)
異常鏈接寄存器ELR(Exception Link Registers)包含異常返回地址。當(dāng)處理器發(fā)生異常時(shí),返回地址將保存在異常級(jí)別對(duì)應(yīng)的ELR中。例如,當(dāng)處理器將異常處理交給EL1處理時(shí),會(huì)將異常返回地址保存在ELR_EL1中。在異常返回時(shí),PC恢復(fù)到存儲(chǔ)在ELR中的地址。例如,從EL1返回時(shí),PC將恢復(fù)到ELR_EL1中存儲(chǔ)的地址。
AArch64狀態(tài)為每個(gè)異常級(jí)別都提供了ELR寄存器:
ELR_EL1,用于EL1的異常。
如果實(shí)現(xiàn)了EL2,ELR_EL2用于EL2的異常。
如果實(shí)現(xiàn)了EL3,ELR_EL3用于EL3的異常。
(5)ESR(Exception Syndrome Register)
異常綜合表征寄存器ESR_ELn包含的異常信息用以異常處理程序確定異常原因。僅針對(duì)同步異常和SError進(jìn)行更新。因?yàn)镮RQ或FIQ中斷處理程序從通用中斷控制器(GIC)寄存器的信息獲取狀態(tài)。
ESR_ELn的BIT[31:26]指示處理程序執(zhí)行對(duì)應(yīng)的異常,比如:
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]構(gòu)成ISS域(Instruction Specific Syndrome),根據(jù)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發(fā)生的狀態(tài)信息:

*其他bit位解釋可以參考ARM v8手冊(cè)<DDI0487F_a_armv8_arm>第10.2.6章節(jié)
4.異常入口
每個(gè)異常都有特定的異常級(jí)別。異常所對(duì)應(yīng)的異常級(jí)別是由軟件編程決定,或者由異常自身性質(zhì)決定的。在任何情況下,異常執(zhí)行時(shí)都不會(huì)移至較低的異常級(jí)別。異常入口的基本執(zhí)行內(nèi)容是:
處理器狀態(tài)保存到目標(biāo)異常級(jí)別的SPSR_ELx中。
返回地址保存到目標(biāo)異常級(jí)別的ELR_ELx中。
如果異常是同步異?;騍Error中斷,異常的表征信息將保存在目標(biāo)異常級(jí)別的ESR_ELx中。
如果是指令止異常(Instruction Abort exception),數(shù)據(jù)中止異常(Data Abort exception,),PC對(duì)齊錯(cuò)誤異常(PC alignment fault exception),故障的虛擬地址將保存在FAR_ELx中。
堆棧指針保存到目標(biāo)異常級(jí)別的專用堆棧指針寄存器SP_ELx。
執(zhí)行移至目標(biāo)異常級(jí)別,并從異常向量定義的地址開始執(zhí)行。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個(gè)人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)??


二、異常處理流程
1.異常向量表
當(dāng)發(fā)生異常時(shí),處理器必須執(zhí)行與之對(duì)應(yīng)的處理程序。處理程序在內(nèi)存中的存儲(chǔ)位置稱為異常向量。在ARM體系結(jié)構(gòu)中,異常向量存儲(chǔ)在一個(gè)表中,該表稱為異常向量表。每個(gè)異常級(jí)別都有其自己的向量表,即EL3,EL2和EL1都有一個(gè),該表包含要執(zhí)行的指令。
每個(gè)表占128個(gè)字節(jié),可以保存32條指令(arm64的指令長度也是4字節(jié)),以linux kernel-4.19/arch/arm64/kernel/entry.S為例,異常向量表的入口如下圖,一共有4組16個(gè)表:

用另外一張表可以更好理解這個(gè)異常向量表的入口:

比如當(dāng)前代碼運(yùn)行在內(nèi)核空間,發(fā)生了data abort,異常向量表的入口地址就是0x200。
2.kernel_ventry
異常發(fā)生后,處理器從對(duì)應(yīng)的異常向量表入口地址開始執(zhí)行,第一條指令是kernel_ventry。kernel_ventry是一個(gè)宏定義,先檢查??臻g是否有溢出,然后跳轉(zhuǎn)到指定的異常處理標(biāo)簽。

以下以EL1發(fā)生data abort異常為例介紹異常處理流程。
EL1發(fā)生data abort異常后進(jìn)入對(duì)應(yīng)的異常向量表入口,先檢查棧是否有溢出,然后跳轉(zhuǎn)至:el1_sync(data abort屬于同步異常)。

3.elx_sync
(1)保存現(xiàn)場
el1_sync第一條指令執(zhí)行kernel_entry 1。kernel_entry也是一個(gè)宏定義,首先將CPU寄存器保存到棧空間,因?yàn)檫@些寄存器接下來會(huì)被覆蓋使用。為了保證kernel_exit時(shí)能恢復(fù)準(zhǔn)確的現(xiàn)場,這里有必要對(duì)第一現(xiàn)場先做保存。

其次設(shè)置棧幀大小S_FRAM_SIZE,S_FRAM_SIZE根據(jù)pt_regs結(jié)構(gòu)體大小而設(shè)定。

pt_regs結(jié)構(gòu)體:

另外就是讀取elr_el1和spsr_el1等寄存器值。總之,kernel_entry主要將CPU寄存器按照pt_regs結(jié)構(gòu)體的定義將異常第一現(xiàn)場保存到棧上。
(2)判斷異常類型
kernel_entry保存完第一現(xiàn)場之后,接下來讀取esr_el1寄存器的值,并判斷異常的具體類型。如2.3.5章節(jié)所描述的ESR寄存器定義,ESR包含的異常信息主要用于異常處理程序確定異常原因,其中ESR_ELn的BIT[31:26] EC域指示處理程序執(zhí)行的對(duì)應(yīng)異常類型。
發(fā)生DataAbort時(shí),EC = 0b100101,即ESR_ELx_EC_DABT_CUR=0x25,el1_syn將跳轉(zhuǎn)至el1_da。

ESR_ELx_EC_DABT_CUR定義在/kernel/msm-4.19/arch/arm64/include/asm/esr.h。
除此之外,還有其他的同步異常類型,比如:

(3)跳轉(zhuǎn)至異常類型標(biāo)簽
通過esr_el1寄存器值確定同步異常的具體類型后,跳轉(zhuǎn)至對(duì)應(yīng)的異常處理標(biāo)簽el1_da。el1_da第一條指令,mrs x3,far_el1,將far_el1保存到x3。在2.4異常入口章節(jié)介紹過如果發(fā)生數(shù)據(jù)中止異常(DataAbort exception),故障的虛擬地址將保存在FAR_ELx中。這里就是首先將data abort異常發(fā)生的虛擬地址第一時(shí)間取出,保持在x3中。

el1_da 跳轉(zhuǎn)到異常處理程序do_mem_abort之前,為其設(shè)置好了三個(gè)入?yún)ⅲ?/p>
x0:產(chǎn)生DataAbort的故障虛擬地址。
x1:esr_el1,異常綜合表征寄存器值,在el1_sync第二條指令已經(jīng)保存。
x2:stack frame 地址,即pt_regs結(jié)構(gòu)體的首地址,在kernel_entry已對(duì)其填充。
x0~x2分別對(duì)應(yīng)do_mem_abort函數(shù)的三個(gè)參數(shù):addr,esr,*regs。
(4)跳轉(zhuǎn)至異常處理程序
do_mem_abort 函數(shù)位于/kernel/msm-4.19/arch/arm64/mm/fault.c

do_mem_abort首先根據(jù)esr寄存器獲取data abort fault_info。在2.3.5章節(jié)介紹了ESR寄存器BIT[24:0]的ISS域,它記錄了data abort的具體類型。這里將用到ISS域,也就是fault_info中的name變量。我們通??吹降摹癲o_page_fault”、“do_translation_fault”等data abort下細(xì)分的調(diào)用棧就是由這里的ISS域區(qū)分而來。
fault_info結(jié)構(gòu)體:

Fault_info[]數(shù)組:

fault_info 數(shù)組中對(duì)應(yīng)的處理函數(shù)對(duì)當(dāng)前的異常進(jìn)一步處理,如果發(fā)現(xiàn)當(dāng)前的data abort確實(shí)是屬于非法,無法處理的,比如paging request 非法地址,就會(huì)拋出異常信息,并走到panic流程。

最后,調(diào)用arm64_notify_die,如果是用戶空間發(fā)生data abort,輸出異常信息和發(fā)送signal給當(dāng)前進(jìn)程。如果是異常發(fā)生在內(nèi)核空間,走die流程。

die函數(shù)最終可能會(huì)調(diào)用到panic。但die函數(shù)也不是一定會(huì)走到panic,它先是走oops流程告警系統(tǒng)現(xiàn)在的異常,如果異常發(fā)生在中斷上下文,走panic?;蛘呷绻O(shè)定了CONFIG_PANIC_ON_OOPS_VALUE=y,無論是否在中斷上下文均走panic。

如果此次異常并沒有走到panic流程,那么系統(tǒng)還是要繼續(xù)運(yùn)行,拋出oops警告后系統(tǒng)如何恢復(fù)異常發(fā)生前的環(huán)境?回到el1_da處理指令,do_mem_abort執(zhí)行完如果不需要panic,跳轉(zhuǎn)到kernel_exit進(jìn)行異常退出處理。

4.kernel_exit
kernel_exit恢復(fù)現(xiàn)場。主要恢復(fù)kernel_entry保存在棧上的處理器相關(guān)寄存器等。至此發(fā)生在el1級(jí)別的data baort異常處理流程分析結(jié)束。

原文作者:內(nèi)核工匠
