Arm架構異常處理流程之缺頁異常
本部分簡單介紹下為何要寫此文?
1開發除錯工具:完成動態監測io地址是否被修改的功能。
2假設你程式中的一塊記憶體地址被篡改,你是否希望動態監測到何時何地被修改?
本文介紹的內容,就是實現該功能的理論基礎之一。
3基本設計思路:
第一步:將io地址重新對映為只讀。
第二步:在arm的dabt異常中 dump出棧資訊。Dabt
第三步:考慮中斷異常中發生dabt異常的情況。
4本文的目的不是為了介紹工具的設計方法,而是介紹該工具所依賴的arm異常處理流程。
【正文一】linux系統arm缺頁異常處理之彙編階段
1 為了介紹方便介紹,先列出兩個知識點。
1.1 linux系統為實現異常處理引入了棧幀的概念:
1.2 Arm的幾種模式介紹:#define ARM_cpsr uregs[16] #define ARM_pc uregs[15] #define ARM_lr uregs[14] #define ARM_sp uregs[13] #define ARM_ip uregs[12] #define ARM_fp uregs[11] #define ARM_r10 uregs[10] #define ARM_r9 uregs[9] #define ARM_r8 uregs[8] #define ARM_r7 uregs[7] #define ARM_r6 uregs[6] #define ARM_r5 uregs[5] #define ARM_r4 uregs[4] #define ARM_r3 uregs[3] #define ARM_r2 uregs[2] #define ARM_r1 uregs[1] #define ARM_r0 uregs[0] #define ARM_ORIG_r0 uregs[17]
處理器模式縮寫對應的M[4:0]編碼 Privilegelevel
User usr 10000 PL0
FIQ fiq 10001 PL1
IRQ irq 10010 PL1
Supervisor svc 10011 PL1
Monitor mon 10110 PL1
Abort abt 10111 PL1
Hyp hyp 11010 PL2
Undefined und 11011 PL1
System sys 11111 PL1
1.3 ARM 異常處理總入口(entry-armv.s):
/*註釋:
1)Arm架構異常處理向量表起始地址__vectors_start。
2)Arm架構定義7種異常包括中斷、系統呼叫、缺頁異常等,發生異常時處理器會跳轉到相應入口。
3)異常向量表起始位置有cp15協處理器的控制暫存器c1的bit13
決定:v=0時,異常向量起始於0xffff0000;v=1時起始於0x0.
4)舉例:當IRQ發生時跳轉到0x18這個虛擬地址上(核心態虛擬地址)
head.s中設定了cp15暫存器器(proc-v7.s->__v7_setup()函式設定的)
*/
__vectors_start:
W(b) vector_rst
W(b) vector_und
/*
系統呼叫入口點:
__vectors_start + 0x1000=__stubs_start
此時pc指向系統呼叫異常 的處理入口:vector_swi
使用者態通過swi指令產生軟中斷。
因為系統呼叫異常程式碼編譯到其他檔案,其入口地址與異常向量相隔
較遠,使用b指令無法跳轉過去(b指令只能相對當前pc跳轉+/-32M範圍)
*/
W(ldr) pc, __vectors_start + 0x1000
/* 取指令異常 */
W(b) vector_pabt
/* 資料異常--缺頁異常 */
W(b) vector_dabt
W(b) vector_addrexcptn
/* 中斷異常 */
W(b) vector_irq
W(b) vector_fiq
2 缺頁異常處理(vector_dabt):
2.1 vector_dabt : 它是通過vetcor_stub巨集定義的。
vector_stub dabt, ABT_MODE, 8
.long __dabt_usr @ 0 (USR_26 / USR_32)
.long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
.long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
.long __dabt_svc @ 3 (SVC_26 / SVC_32)
2.2 vecotr_stub 巨集定義。
1) vector_stub 巨集尤為關鍵,arm任何異常都是通過其將r0/lr/spsr儲存到異常模式的棧中。
2)vector_stub通過 vector_\name實現其功能:
/*註釋:
該介面負責儲存異常發生前一時刻cpu暫存器到異常mode的棧中,儲存r0,lr,spsr暫存器的值到sp_dabt或sp_irq上
注意:
1 此時的sp是異常狀態下的sp,這個棧只有12byte大小,在cpu_init()中初始化。
2 arm在IRQ/svc/ABORT幾種模式下sp是不能共用的,詳見:A2.5.7
3 此時lr中儲存的實際上是異常的返回地址,異常發生,切換到svc模式後,會將lr儲存到svc模式棧中(pt_regs->pc)
最後從異常返回時再將pt_regs->pc載入入arm暫存器pc中,實現異常返回。
本函式只是其中一個步驟,即為將異常發生時刻lr儲存到svc模式棧中(pt_regs->pc)做準備。
4 spsr是異常發生那一刻(即進入異常模式前是什麼模式)的cpsr狀態,如核心態下發生中斷:則spsr是svc模式10111;
如使用者態下發生中斷,則spsr是user模式10000.
5 此時cpu正處於異常狀態(如中斷),此時cpsr為10010;
6 要進行真正的異常處理,需要退出異常模式進入svc模式.
*/
.macro vector_stub, name, mode, correction=0
/*強制cacheline=32byte對齊*/
.align 5
vector_\name:
.if\correction
/*註釋:
需要調整返回值,對應irq異常將lr減去4
因為異常發生時,arm將pc地址+4賦值給了lr
*/
sub lr, lr, #\correction
.endif
@
@Save r0, lr_<exception> (parent PC) and spsr_<exception>
@(parent CPSR)
@spsr中儲存異常發生時刻的cpsr ; book:A2.6 Exceptions;
@注意此時的棧sp是異常時(abt mode或irq mode)的棧sp和svc mode裡的棧sp不同
@dabt異常時的sp只有12byte大小 ;cpu_init中初始化
@save r0, lr;將r0和lr儲存到異常模式的棧上[sp]=r0;[sp+4]=lr_dabt;stmia sp,{r0, lr}沒有sp!,因此sp不變
@r0也要入棧,以為r0會用作傳遞引數(異常狀態下的sp)
stmia sp, {r0, lr}
/*將spsr儲存到異常模式的棧上[sp+8]=spsr*/
mrs lr, spsr @ read spsr to lr
str lr, [sp, #8] @save spsr //sp+8=lr=(spsr_dabt)
@
@Prepare for SVC32 mode. IRQs remaindisabled.
@cpsr中儲存的是異常模式:如中斷10010;dabt10111
mrs r0, cpsr
/*
注意:
1 dabt處理時:r0=r0^(0x17^0x13)=r0^0x4,bit3取反之後10011變為svc模式;
2 IRQ處理時:r0=10010=r0^(0x12^0x13)=r0^0x1=10011變為svc模式
*/
eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)
msr spsr_cxsf, r0
@
@the branch table must immediately follow this code
@
and lr, lr, #0x0f /* 使用者態(user mode)lr=0;核心態(svn mode)lr=3; */
/*
r0=sp;
注意:
1此時r0中儲存了異常狀態下sp棧地址,這個棧上儲存了r0,lr(異常返回地址),
spsr(異常發生時,cpu的狀態,當然異常返回時需要恢復該狀態)
2之後的函式會把r0中儲存的異常模式的sp上資訊,載入到svc模式下的sp棧上。
異常處理返回時再將svc mode的棧載入到arm暫存器上。
*/
mov r0, sp
/*
lr中是儲存發生異常時arm的cpsr狀態到spsr
1 user模式發生異常則lr=10000&0x0f;lr=pc+lr<<2pc+0時執行 __dabt_usr;
2 svc模式發生異常則lr=10011&0x0f;lr=pc+lr<<2 pc+12時執行 __data_svc
lr=3時執行__dabt_svc
*/
ARM( ldr lr,[pc, lr, lsl #2] )
/* movs中s表示把spsr恢復給cpsr,上面可知spsr儲存的是svc模式,不過此時中斷還是關閉的
異常處理一定要進入svc模式原因:
1)異常處理一定要PL1特權級。2)使能巢狀中斷。
如果一箇中斷模式(例如使用者態發生中斷,arm從usr進入irq模式)中重新允許中斷,
且這個中斷模式中使用了bl指令,bl會把pc放到lr_irq中,這個地址會被當前模式下產生的中斷破壞
這種情況下中斷無法返回。所以為了避免這種情況,中斷處理過程應該切換到svc模式,bl指令可以把
pc(即子程式返回地址)儲存到lr_svc.
*/
movs pc, lr @branch to handler in SVC mode 把spsr copy到cpsr,spsr是svc模式,自此arm切換到了svc模式。
ENDPROC(vector_\name)
3 回過頭來再看缺頁異常處理 :vector_dabt :
3.1 vector_dabt : 它是通過vetcor_stub巨集定義的。
vector_stub dabt, ABT_MODE, 8
.long __dabt_usr @ 0 (USR_26 / USR_32)
.long __dabt_invalid @ 1 (FIQ_26 / FIQ_32)
.long __dabt_invalid @ 2 (IRQ_26 / IRQ_32)
.long __dabt_svc @ 3 (SVC_26 / SVC_32)
3.2 arm在user模式下缺頁異常處理:__dabt_usr__dabt_usr:
usr_entry--進入異常處理流程上面已經分析過。
kuser_cmpxchg_check
mov r2, sp
dabt_helper//v7_early_abort--異常處理。
b ret_from_exception--退出異常處理;
UNWIND(.fnend )
ENDPROC(__dabt_usr)
3.3 arm在user模式下缺頁異常處理:__dabt_usr->usr_entry
注意arm在user模式下無論中斷還是缺頁都會進行user_entry處理:
該函式是在vector_\name處理完成後跳轉過來的,此時r0中儲存了異常模式的sp
此sp儲存了r0/lr/spsr/可以參看上面vector_\name的介紹。
.macro usr_entry
UNWIND(.fnstart )
UNWIND(.cantunwind ) @ don't unwind theuser space
/*這個sp是svc模式的棧地址,為理解方便可以記作:sp_svc,和r0中儲存的異常模式的sp(記做:sp_dabt)不是一個,注意區分 */
sub sp, sp, #S_FRAME_SIZE //sp=sp-S_FRAME_SIZE
/*將r1-r12儲存到sp_svc上:
注意1 ib=incressment berfore;sp=sp+4;[sp]=r1;...sp=sp+12*4;
ia=incressment after
2 sp_svc保持不變,因為不是stmib sp!, {r1 - r12}
此處使用ib目的是為r0預留棧空間;
*/
ARM( stmib sp,{r1 - r12} )
THUMB( stmia sp, {r0 - r12} )
/*
此時r0儲存的是異常時的sp如:sp_dabt
(vector_stub dabt中實現r0=sp;sp=lr;sp+4=spsr)
缺頁異常為例:
r3=[r0]=[sp_dabt]=r0; 異常模式和svc模式r0是通用的。
r4=r0=r0+4=lr_dabt; 異常發生時刻的lr值,即異常返回地址,異常返回時,需要將其賦值給arm的pc暫存器。
r5=spsr_dabt;...r0=r0+3*4 異常發生時刻cpu的狀態,異常返回時,需要將其賦值給arm的cpsr暫存器。
r0是異常mode下sp_dabt,,
在vector_dabt中初始:[r3]=dabt_r0;[r4]=dabt_lr;[r5]=dabt_spsr
此處將異常mode(dabt、irq等)的棧資訊儲存到svc或user模式下的普通暫存器
之後再把普通暫存器入棧
*/
ldmia r0, {r3 - r5}
/* r0儲存了svc模式下sp上用於儲存pc的棧地址 */
add r0, sp, #S_PC @here for interlock avoidance r0=&pt_regs->ARM_pc
mov r6, #-1 @ "" "" "" "" r6=-1
/*開始將異常時的r0,dabt_lr,dabt_spsr,dabt_sp,dabt_lr儲存到當前sp(即pt_regs) */
str r3, [sp] @save the "real" r0 copied [sp] = r3 中斷那一刻的r0儲存到棧頂
@from the exception stack
@普通暫存器入棧
@We are now ready to fill in the remaining blanks on the stack:
@
@ r4 - lr_<exception>, already fixed upfor correct return/restart
@ r5 - spsr_<exception>
@ r6 - orig_r0 (see pt_regs definition inptrace.h)
@
@Also, separately save sp_usr and lr_usr
@將異常時的棧儲存到當前模式下的pt_regs中
/*
此時,r0中儲存的是svc模式下(即當前模式)pt_regs->pc指標棧地址;
注意此處r0沒有加"!"(r0!),所以r0值不變
[r0]=r4;r0+4=r5;
即:pt_regs->ARM_pc=dabt_lr;
pt_regs->ARM_cpsr=r5=dabt_spsr;
pt_regs->ARM_ORIG_r0=r6=-1
至此:svc模式下棧中儲存了pc指標和cpsr
異常返回時將pt_regs->ARM_pc,pt_regs->ARM_cpsr分別載入到arm暫存器
pt_regs->ARM_pc載入到pc暫存器後自然跳轉到異常發生時刻的lr_dabt,從而實現異常返回。
pt_regs->ARM_cpsr同理。
*/
stmia r0, {r4 - r6}
/*
r0=pt_regs->pc
此分支[r0-4]=pt_regs->lr=lr_svc;
[r0-8]=pt_regs->sp=sp_svc ;db=decrementbefore
注意:
1當前棧上(pt_regs)只有r0/pc/cpsr/orig_r0上儲存的是異常發生時刻的r0/lr/spsr/-1
2 sp和lr儲存的是此刻svcmode的sp和lr值
*/
ARM( stmdb r0,{sp, lr}^ )
THUMB( store_user_sp_lrr0, r1, S_SP - S_PC )
/*完成將異常時的dabt_r0,dabt_lr,dabt_spsr,dabt_sp,dabt_lr儲存到當前sp(即pt_regs) */
@
@Enable the alignment trap while in kernel mode
@
alignment_trapr0
@
@Clear FP to mark the first stack frame
@
zero_fp
ct_user_exitsave = 0
.endm
3.4 arm在svc模式下缺頁異常處理:__dabt_svc
__dabt_svc:
svc_entry
mov r2, sp
dabt_helper
THUMB( ldr r5, [sp, #S_PSR] ) @ potentiallyupdated CPSR
svc_exitr5 @return from exception
UNWIND(.fnend )
ENDPROC(__dabt_svc)
注意:__irq_svc同樣需要如下處理:
3.5 arm在svc模式下缺頁異常處理:__dabt_svc->svc_entry
.macro svc_entry, stack_hole=0
UNWIND(.fnstart )
UNWIND(.save {r0 - pc} )
sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4)//sp指向r1
SPFIX( tst sp, #4 )//判斷bit2是否為0
SPFIX( subeq sp,sp, #4 )
stmia sp, {r1 - r12} //svc mode的r1-r12入棧;[sp]=r1;[sp+4]=r2
ldmia r0, {r3 - r5}//r3 =dabt_r0;r4=dabt_lr;r5=dabt_spsr
add r7, sp, #S_SP - 4 @ here for interlock avoidance //r7指向pt_regs第12個 @ r7=pt_regs->sp
mov r6, #-1 @ "" "" "" ""
/*此時r2指向棧頂,因為之前sub sp, sp, #(S_FRAME_SIZE + \stack_hole -4)*/
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)
SPFIX( addeq r2, r2, #4 )
/*此時r3中儲存的是發生異常時刻的r0;[sp, #-4]!表示sp=sp-4:
此時sp位於棧低指向r0*/
str r3, [sp, #-4]! @ save the "real" r0 copied //吧dabt_r0儲存到sp-4;sp=sp-4
@from the exception stack
mov r3, lr //儲存svc mode的lr_svc到r3
@
@We are now ready to fill in the remaining blanks on the stack:
@
@ r2 - sp_svc=pt_regs->ARM_sp
@ r3 - lr_svc=pt_regs->ARM_lr=lr_svc
@ r4 - lr_<exception>=pt_regs->ARM_pc,already fixed up for correct return/restart
@ r5 -spsr_<exception>==pt_regs->ARM_cpsr
@ r6 - orig_r0==pt_regs->ARM_ORIG_r0 (seept_regs definition in ptrace.h)
@ r7=pt_regs->ARM_sp
/*[r7]=pt_regs->ARM_sp=r2(此時r2指向棧頂)*/
stmia r7, {r2 - r6}
.endm
3.6 arm在svc模式下缺頁異常處理:__dabt_svc->svc_exit
.macro svc_exit, rpsr, irq = 0
.if \irq != 0
@IRQs already off
.else
@IRQs off again before pulling preserved data off the stack
disable_irq_notrace
.endif
/*
經過svc_entry處理:sp指向棧底;(pt_regs*)(sp)->ARM_sp卻指向了棧頂。
此時[lr] =pt_regs->ARM_sp是棧頂
*/
ldr lr, [sp, #S_SP] @ top of the stack
/*
從sp->s_sp中取出64位數放到r0和r1中
即 : r0 = [pt_regs->ARM_lr];r1=[pt_regs->ARM_pc]
*/
ldrd r0, r1, [sp, #S_LR] @ calling lr and pc
clrex @clear the exclusive monitor 清除某塊記憶體的獨佔訪問標誌
/*
此時lr是棧頂:
將lr/pc/spsr放到棧上:
lr=lr-4;[lr]=\spsr=pt_regs->ARM_cpsr;
lr=lr-4;[lr]=r1=[pt_regs->ARM_pc];
lr=lr-4;[lr]=r0=[pt_regs->ARM_lr];
至此lr中儲存的地址上儲存了指向了svc_entry中指定的lr;
*/
stmdb lr!, {r0, r1, \rpsr} @ calling lr and rfe context
/*出棧:賦值r0-r12暫存器 */
ldmia sp, {r0 - r12}
/*
此時lr上儲存的是棧地址即&pt_regs->ARM_lr
sp=lr;lr=pt_regs->ARM_lr此時sp儲存的地址上儲存的是pt_regs->ARM_lr的值
*/
mov sp, lr
/*
lr=[sp]真正從棧地址&pt_regs->ARM_lr上取出pt_regs->ARM_lr放入lr暫存器;
sp=sp+4;
至此sp上儲存了pc地址
*/
ldr lr, [sp], #4
rfeia sp!
.endm
【正文二】linux系統arm缺頁異常處理之c語言階段
一.跳轉到缺頁處理的c語言函式入口 : dabt_helper->CPU_DABORT_HANDLER
.macro dabt_helper
@
@ Call the processor-specific abort handler:
@
@ r2 - pt_regs
@ r4 - aborted context pc
@ r5 - aborted context psr
@
@ The abort handler must return the aborted address in r0, and
@ the fault status register in r1. r9 must be preserved.
@
/*
缺頁異常處理在此跳轉到c函式入口;
__dabt_user/__dabt_svc->dabt_helper->CPU_DABORT_HANDLER=v7_early_abort->
do_DataAbort->do_translation_fault->do_page_fault->__do_page_fault->
handle_mm_fault->handle_pte_fault
*/
bl CPU_DABORT_HANDLER
.endm
二. c函式處理流程說明:
1 缺頁異常處理在 dabt_helper中跳轉到CPU_DABORT_HANDLER,最後呼叫到handle_pte_fault函式,為使用者空間地址建立頁表。
3 do_DataAbort是實際的c函式處理流程入口。
因為c語言處理部分相對簡單,在此不做詳細介紹,只給出以上大體流程,感興趣可以參考原始碼看一下。
【總結】
以上分析了linux系統是如何實現的arm缺頁處理。