ARMv8 異常處理簡介


內核穩定性問題復雜多樣,最常見的莫過於“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 內核黑科技 | 技術文章 | 精選教程


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM