內核穩定性問題複雜多樣,最多見的莫過於「kernel panic」,意爲「內核恐慌,不知所措」。這種狀況下系統天然沒法正常運轉,只能自我結束生命,留下死亡信息。諸如:linux
「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的異常處理流程。架構
1、ARMv8異常簡介app
1.異常級別異步
不一樣於Armv7架構採用CPU模式切換的方式進行異常處理,Armv8架構定義了一組全新的異常級別進行異常處理,即EL0至EL3,有以下特性:函數
若是ELn爲異常級別,則n的值增長表示軟件執行特權增長。oop
EL0處的執行稱爲無特權執行,不能處理異常。
EL2提供對虛擬化的支持。
EL3提供了在兩個安全狀態(安全狀態和非安全狀態)之間切換的支持。
一個實現能夠不包括全部的異常級別,但都必須包括EL0和EL1。EL2和EL3是可選的。
以下是典型的異常級別使用模型:
![](http://static.javashuo.com/static/loading.gif)
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)的首字母。
![](http://static.javashuo.com/static/loading.gif)
(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解讀以下:
![](http://static.javashuo.com/static/loading.gif)
BIT[5:0] DFSC(Data Fault Status Code)解釋了data abort發生的狀態信息:
![](http://static.javashuo.com/static/loading.gif)
*其餘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。
執行移至目標異常級別,並從異常向量定義的地址開始執行。
2、異常處理流程
1.異常向量表
當發生異常時,處理器必須執行與之對應的處理程序。處理程序在內存中的存儲位置稱爲異常向量。在ARM體系結構中,異常向量存儲在一個表中,該表稱爲異常向量表。每一個異常級別都有其本身的向量表,即EL3,EL2和EL1都有一個,該表包含要執行的指令。
每一個表佔128個字節,能夠保存32條指令(arm64的指令長度也是4字節),以linux kernel-4.19/arch/arm64/kernel/entry.S爲例,異常向量表的入口以下圖,一共有4組16個表:
![](http://static.javashuo.com/static/loading.gif)
用另一張表能夠更好理解這個異常向量表的入口:
![](http://static.javashuo.com/static/loading.gif)
好比當前代碼運行在內核空間,發生了data abort,異常向量表的入口地址就是0x200。
2.kernel_ventry
異常發生後,處理器從對應的異常向量表入口地址開始執行,第一條指令是kernel_ventry。kernel_ventry是一個宏定義,先檢查棧空間是否有溢出,而後跳轉到指定的異常處理標籤。
![](http://static.javashuo.com/static/loading.gif)
如下以EL1發生data abort異常爲例介紹異常處理流程。
EL1發生data abort異常後進入對應的異常向量表入口,先檢查棧是否有溢出,而後跳轉至:el1_sync(data abort屬於同步異常)。
![](http://static.javashuo.com/static/loading.gif)
3.elx_sync
(1)保存現場
el1_sync第一條指令執行kernel_entry 1。kernel_entry也是一個宏定義,首先將CPU寄存器保存到棧空間,由於這些寄存器接下來會被覆蓋使用。爲了保證kernel_exit時能恢復準確的現場,這裏有必要對第一現場先作保存。
![](http://static.javashuo.com/static/loading.gif)
其次設置棧幀大小S_FRAM_SIZE,S_FRAM_SIZE根據pt_regs結構體大小而設定。
![](http://static.javashuo.com/static/loading.gif)
pt_regs結構體:
![](http://static.javashuo.com/static/loading.gif)
另外就是讀取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。
![](http://static.javashuo.com/static/loading.gif)
ESR_ELx_EC_DABT_CUR定義在/kernel/msm-4.19/arch/arm64/include/asm/esr.h。
除此以外,還有其餘的同步異常類型,好比:
![](http://static.javashuo.com/static/loading.gif)
(3)跳轉至異常類型標籤
經過esr_el1寄存器值肯定同步異常的具體類型後,跳轉至對應的異常處理標籤el1_da。el1_da第一條指令,mrs x3,far_el1,將far_el1保存到x3。在2.4異常入口章節介紹過若是發生數據停止異常(DataAbort exception),故障的虛擬地址將保存在FAR_ELx中。這裏就是首先將data abort異常發生的虛擬地址第一時間取出,保持在x3中。
![](http://static.javashuo.com/static/loading.gif)
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
![](http://static.javashuo.com/static/loading.gif)
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結構體:
![](http://static.javashuo.com/static/loading.gif)
Fault_info[]數組:
![](http://static.javashuo.com/static/loading.gif)
fault_info 數組中對應的處理函數對當前的異常進一步處理,若是發現當前的data abort確實是屬於非法,沒法處理的,好比paging request 非法地址,就會拋出異常信息,並走到panic流程。
![](http://static.javashuo.com/static/loading.gif)
最後,調用arm64_notify_die,若是是用戶空間發生data abort,輸出異常信息和發送signal給當前進程。若是是異常發生在內核空間,走die流程。
![](http://static.javashuo.com/static/loading.gif)
die函數最終可能會調用到panic。但die函數也不是必定會走到panic,它先是走oops流程告警系統如今的異常,若是異常發生在中斷上下文,走panic。或者若是設定了CONFIG_PANIC_ON_OOPS_VALUE=y,不管是否在中斷上下文均走panic。
![](http://static.javashuo.com/static/loading.gif)
若是這次異常並無走到panic流程,那麼系統仍是要繼續運行,拋出oops警告後系統如何恢復異常發生前的環境?回到el1_da處理指令,do_mem_abort執行完若是不須要panic,跳轉到kernel_exit進行異常退出處理。
![](http://static.javashuo.com/static/loading.gif)
4.kernel_exit
kernel_exit恢復現場。主要恢復kernel_entry保存在棧上的處理器相關寄存器等。至此發生在el1級別的data baort異常處理流程分析結束。
![](http://static.javashuo.com/static/loading.gif)
參考資料
[1]《DDI0487F_a_armv8_arm.pdf》
[2]《DEN0024A_v8_architecture_PG.pdf》
本文分享自微信公衆號 - 人人都是極客(rrgeek)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。