1. 程式人生 > >Arm架構異常處理流程之缺頁異常

Arm架構異常處理流程之缺頁異常

【摘要】 本文將為您介紹linux核心是如何實現缺頁異常處理的。缺頁異常、中斷和系統呼叫同屬arm異常處理,筆者計劃分三篇文件分別介紹一下,其實在彙編階段三種處理流程有很多相通之處,不過為了閱讀方便,即使相同的部分也會重新在各自文件中介紹一遍。 【背景】

本部分簡單介紹下為何要寫此文?

1開發除錯工具:完成動態監測io地址是否被修改的功能。

2假設你程式中的一塊記憶體地址被篡改,你是否希望動態監測到何時何地被修改?

 本文介紹的內容,就是實現該功能的理論基礎之一。

3基本設計思路:

第一步:將io地址重新對映為只讀。

第二步:在armdabt異常中 dump出棧資訊。Dabt

可以理解為缺頁異常。

第三步:考慮中斷異常中發生dabt異常的情況。

4本文的目的不是為了介紹工具的設計方法,而是介紹該工具所依賴的arm異常處理流程。

【正文一】linux系統arm缺頁異常處理之彙編階段

1 為了介紹方便介紹,先列出兩個知識點。 

1.1 linux系統為實現異常處理引入了棧幀的概念:

#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]
1.2 Arm的幾種模式介紹:

處理器模式縮寫對應的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儲存到異常模式的棧中。

2vector_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)

回過頭來再看缺頁異常處理 :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

注意armuser模式下無論中斷還是缺頁都會進行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函式,為使用者空間地址建立頁表。

do_DataAbort是實際的c函式處理流程入口。

因為c語言處理部分相對簡單,在此不做詳細介紹,只給出以上大體流程,感興趣可以參考原始碼看一下。

【總結】

以上分析了linux系統是如何實現的arm缺頁處理。