本部分简单介绍下为何要写此文?
1开发调试工具:完成动态监测io地址是否被修改的功能。
2假设你程序中的一块内存地址被篡改,你是否希望动态监测到何时何地被修改?
本文介绍的内容,就是实现该功能的理论基础之一。
3基本设计思路:
第一步:将io地址重新映射为只读。
第二步:在arm的dabt异常中 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保存到异常模式的栈中。
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函数,为用户空间地址创建页表。
2 创建页表的过程可以参考博文: linux内核源码是如何实现分页机制的
3 do_DataAbort是实际的c函数处理流程入口。
因为c语言处理部分相对简单,在此不做详细介绍,只给出以上大体流程,感兴趣可以参考源代码看一下。
【总结】
以上分析了linux系统是如何实现的arm缺页处理。