linux arm notify die,ARM Linux 异常处理 ---data abort

简要分析arm 数据异常的处理过程

异常向量与程序跳转

data abort是ARM体系定义的异常之一。异常发生时,ARM会自动跳转到异常向量表中,通过向量表中的跳转命令跳转到相应的异常处理中去。

ARM的异常处理向量表在entry-armv.S文件中:

.globl__vectors_start

__vectors_start:

swiSYS_ERROR0

bvector_und + stubs_offset

ldrpc, .LCvswi + stubs_offset

bvector_pabt + stubs_offset

bvector_dabt + stubs_offset

bvector_addrexcptn + stubs_offset

bvector_irq + stubs_offset

bvector_fiq + stubs_offset

对于data abort,对应的跳转地址是vector_dabt +

stubs_offset。这个地址的指令定义也在entry-armv.S:

vector_stubdabt,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)

.long__dabt_invalid@4

.long__dabt_invalid@5

.long__dabt_invalid@6

.long__dabt_invalid@7

.long__dabt_invalid@8

.long__dabt_invalid@9

.long__dabt_invalid@a

.long__dabt_invalid@b

.long__dabt_invalid@c

.long__dabt_invalid@d

.long__dabt_invalid@e

.long__dabt_invalid@f

vector_stub是一个宏定义:

.macrovector_stub, name, mode,

correction=0

.align5

vector_\name:

.if \correction

sublr, lr, #\correction

.endif

@

@ Save r0, lr_(parent PC) and spsr_

@ (parent CPSR)

@

stmiasp, {r0,

lr}@ save r0, lr

mrslr,

spsr@保存跳转之前的CPSR到lr寄存器

strlr, [sp,

#8]@ save spsr

@

@ Prepare forSVC32 mode.IRQs remain disabled.

@

mrsr0, cpsr

eorr0, r0, #(\mode ^SVC_MODE)

msrspsr_cxsf,

r0@准备进入svc模式

@

@ the branch table must immediately follow this code

@

andlr, lr,

#0x0f@得到跳转前所处的模式(usr、svr等)

movr0, sp

ldrlr, [pc, lr, lsl

#2]@根据模式跳转到相应的data abort指令,并进入svc模式

movspc,

lr@ branch to handler inSVCmode

ENDPROC(vector_\name)

.endm

由代码中红色标注部分可看出,对于同一个异常,根据进入异常之前所处的模式,会跳转到不同的指令分支,这些指令分支紧跟在vector_stub宏定义的后面。如果进入data abort之前处于usr模式,那么跳转到__dabt_usr;如果处于svc模式,那么跳转到__dabt_svc;否则跳转到__dabt_invalid。

实际上,进入异常向量前Linux只能处于usr或者svc两种模式之一。这时因为irq等异常在跳转表中都要经过vector_stub宏,而不管之前是哪种状态,这个宏都会将CPU状态改为svc模式。

usr模式即Linux中的用户态模式,svc即内核模式。

下面看一下在不同模式下进入data abort时的处理过程。

svc模式进入data abort

svc模式进入data abort,也就是Linux的内核模式进入data aboart时,会跳转到__dabt_svc。

__dabt_svc:

svc_entry@保护寄存器现场

mrsr9, cpsr

tstr3,

#PSR_I_BIT@检查是否要开中断

biceqr9, r9, #PSR_I_BIT

blCPU_DABORT_HANDLER@处理异常之前的准备工作

msrcpsr_c, r9

movr2, sp

bldo_DataAbort@主要操作都在这里,本文暂不研究

disable_irq

ldrr0, [sp, #S_PSR]

msrspsr_cxsf, r0

ldmiasp, {r0 - pc}^@ load r0 - pc, cpsr

ENDPROC(__dabt_svc)

CPU_DABORT_HANDLER的定义在glue.h:

#define CPU_DABORT_HANDLER

v6_early_abort

对于s3c6410,v6_early_abort的定义在abort-ev6.S中,里面涉及到很多ARM的细节操作,但对我们来说,只需要了解下面这两句即可:

mrcp15, 0, r1, c5, c0, 0@ get FSR

mrcp15, 0, r0, c6, c0,

0@ get FAR

这两句用于读取协处理器CP15的C5、C6寄存器。当data abort异常发生时,C5寄存器中保存的值指明了是哪种原因导致的异常,具体原因可在介绍arm的资料中找到。C6寄存器中保存的是导致data abort的存储地址。

usr模式进入data abort

usr模式进入data abort,也就是Linux的用户模式进入data bort时,会跳转到__dabt_usr。

__dabt_usr:

usr_entry@保护寄存器现场

kuser_cmpxchg_check

blCPU_DABORT_HANDLER@与svc模式时处理过程一样

enable_irq@开中断

movr2, sp

adrlr,

ret_from_exception@重设返回地址

bdo_DataAbort@与svc模式时处理过程一样

ENDPROC(__dabt_usr)

由代码可知,用户模式和内核模式的data abort处理过程类似,区别在于:

l用户模式下data abort处理一定是开中断的;内核模式下则由具体情况决定。

l用户模式下异常处理返回地址被设为ret_from_exception (entry-armv.S文件);内核模式下则返回到出现异常的那条语句。

下面看一下ret_from_exception:

ENTRY(ret_from_exception)

get_thread_info tsk

movwhy, #0

bret_to_user

ENDPROC(__pabt_usr)

ret_to_user会判断是否需要进行进程调度,并最终返回到用户空间。用户空间data abort时可能产生进程调度的原因就在这里。

未定义状态的data abort

除了usr和svc模式之外,其它模式下发生data abort时,都会调用__dabt_invalid函数。这里所说的其它模式在linux正常运行过程中是不应该存在的,所以如果进入__dabt_invalid函数,那就代表Linux内核应该崩溃了。

__dabt_invalid:

inv_entry BAD_DATA

bcommon_invalid

ENDPROC(__dabt_invalid)

inv_entry宏做的主要工作是保存寄存器现场(压栈)。

common_invalid做一些必要的设置,最终调用C函数bad_mode

(traps.c)。

asmlinkage void bad_mode(struct

pt_regs *regs, int reason)

{

console_verbose();

printk(KERN_CRIT "Bad mode

in %s handler detected\n", handler[reason]);

die("Oops - bad mode",

regs, 0);

local_irq_disable();

panic("bad mode");

}

由代码可知,bad_mode主要是输出一些必要的信息,然后调用panic函数,进入死循环。

上文提到data abort的正常处理过程中,最终会调用do_DataAbort函数,下面分析一下该函数的处理过程。

do_DataAbort

asmlinkage void __exception

do_DataAbort(

unsigned long addr,//导致异常的内存地址

unsigned int fsr,//异常发生时CP15中的寄存器值,见前文

struct pt_regs

*regs)//异常发生时的寄存器值列表

{

const struct fsr_info *inf =

fsr_info + (fsr & 15) + ((fsr & (1 << 10)) >> 6);

if (!inf->fn(addr, fsr, regs))

return;

info.si_signo = inf->sig;

info.si_errno = 0;

info.si_code= inf->code;

info.si_addr= (void __user *)addr;

arm_notify_die("", regs, &info, fsr, 0);

}

处理data abort时,首先根据fsr的值得到产生abort的原因,然后根据此原因从一个全局数组fsr_info中得到处理此种abort的struct fsr_info结构,然后调用结构中的fn函数处理。如果fn函数为空,或者函数返回不为0,则调用arm_notify_die函数。

arm_notify_die

首先看一下比较简单的情形,即fsr_info中fn未定义,此时调用arm_notify_die处理:

void arm_notify_die(const char

*str, struct pt_regs *regs,

struct siginfo *info, unsigned

long err, unsigned long trap)

{

if (user_mode(regs)) {

//。。。

force_sig_info(info->si_signo, info, current);

} else {

die(str, regs, err);

}

}

该函数首先使用user_mode判断abort时是属于用户模式还是内核模式,判断方法是看cpsr寄存器中的模式位。按照arm的定义,模式位为0代表用户模式。

l如果是用户模式,那么强制发送一个信号给导致abort的任务(注意这里的任务可能是一个线程)。具体哪个信号被发送由struct

fsr_info结构体中定义的值决定,一般来说,是一个能使进程停止的信号,比如SIGSEGV等等(SIGSEGV之类的信号即使被发给一个线程,也会停止整个进程,具体可看get_signal_to_deliver函数)。

l如果是内核模式,那么调用die函数,这是kernel处理OOPS的标准函数。

fsr_info

fsr_info数组定义在fault.c中,对于每一种可能导致data abort的原因,都有一个fsr_info结构与之对应。

static struct fsr_info fsr_info[] = {

{

do_bad,             SIGSEGV,

0,        "vector

exception"          },

//。。。

{do_translation_fault,   SIGSEGV, SEGV_MAPERR, "section

translation fault"},

{

do_bad,             SIGBUS,0,         "external abort on

linefetch"},

{do_page_fault,    SIGSEGV, SEGV_MAPERR,"page translation

fault"    },

{

do_bad,             SIGBUS,0,         "external abort on

non-linefetch" },

{

do_bad,             SIGSEGV, SEGV_ACCERR,"section domain

fault"            },

{

do_bad,             SIGBUS,0,         "external abort on

non-linefetch" },

{

do_bad,             SIGSEGV, SEGV_ACCERR,"page domain

fault"         },

{

do_bad,             SIGBUS,0,         "external abort on translation"        },

{do_sect_fault,    SIGSEGV, SEGV_ACCERR,"section permission

fault"       },

{

do_bad,             SIGBUS,0,         "external abort on

translation"        },

{do_page_fault,    SIGSEGV, SEGV_ACCERR,"page permission

fault"           },

{

do_bad,             SIGBUS, 0,        "unknown

16"                },

//。。。

{

do_bad,             SIGBUS, 0,        "unknown

30"                },

{

do_bad,             SIGBUS, 0,        "unknown

31"                }

};

fsr_info对大多数abort都调用do_bad函数处理,do_bad函数简单返回1,这样就可以继续执行上面提到的arm_notify_die。

fsr_info对以下四种特殊abort将作单独处理:

l"section translation

fault"      do_translation_fault段转换错误,即找不到二级页表

l"page

translation fault"         do_page_fault页表错误,即线性地址无效,没有对应的物理地址

l"section permission

fault"     do_sect_fault段权限错误,即二级页表权限错误

l"page

permission fault"         do_page_fault页权限错误

段权限错误do_sect_fault

do_sect_fault函数直接调用do_bad_area作处理,并返回0,所以不会再经过arm_notify_die。do_bad_area中,判断是否属于用户模式。如果是用户模式,调用__do_user_fault函数;否则调用__do_kernel_fault函数。

void

do_bad_area(unsigned long addr, unsigned int fsr, struct pt_regs *regs)

if

(user_mode(regs))

__do_user_fault(tsk,

addr, fsr, SIGSEGV, SEGV_MAPERR, regs);

else

__do_kernel_fault(mm,

addr, fsr, regs);

__do_user_fault中,会发送信号给当前线程。

__do_kernel_fault则比较复杂:

l调用fixup_exception进行修复操作,fixup的具体细节可在内核文档exception.txt中找到,它可用于处理get_user之类函数传入的地址参数无效的情况。

l如果不能修复,调用die函数处理oops。

l如果没有进程上下文,内核会在上一步的oops中panic。所以到这里肯定有一个进程与之关联,于是调用do_exit(SIGKILL)函数退出进程,SIGKILL会被设置在task_struct的exit_code域。

段表错误do_translation_fault

do_translation_fault函数中,会首先判断引起abort的地址是否处于用户空间。

l如果是用户空间地址,调用do_page_fault,转入和页表错误、页权限错误同样的处理流程。

l如果是内核空间地址,会判断该地址对应的二级页表指针是否在init_mm中。如果在init_mm里面,那么复制该二级页表指针到当前进程的一级页表;否则,调用do_bad_area处理(可能会调用到fixup)。

对段表错误的处理逻辑的个人理解如下(不保证完全准确):Linux产生段表错误,除了fixup之外还有两种原因:一个是用户空间映射的线性地址出现异常,另一个是内核中调用vmalloc分配的线性地址出现异常。对用户空间地址的异常处理很容易理解。对于内核地址,从vmalloc的实现代码中可以看到,它分配的线性空间的映射关系都会保存到全局变量init_mm中,所以,任何vmalloc生成的线性空间的二级页表都应该在init_mm中找到。(init_mm是内核的mm_struct,管理整个内核的内存映射)。

从这里也可以看出,对vmalloc的地址访问可能会产生两次异常:第一次是段表错误,生成二级页表;第二次是页表错误,分配真正的物理页面到线性空间。

页表错误do_page_fault

页权限错误do_page_fault

do_page_fault完成了真正的物理页面分配工作,另外栈扩展、mmap的支持等也都在这里。对于物理页面的分配,会调用到do_anonymous_page->。。。-> __rmqueue,__rmqueue中实现了物理页面分配的伙伴算法。

如果当前没有足够物理页面供内存分配,即分配失败:

l内核模式下的abort会调用__do_kernel_fault,这与段权限错误中的处理一样。

l用户模式下,会调用do_group_exit退出该任务所属的进程。

用户程序申请内存空间时,如果库函数本身的内存池不能满足分配,会调用brk系统调用向系统申请扩大堆空间。但此时扩大的只是线性空间,直到真正使用到那块线性空间时,系统才会通过data abort分配物理页面。所以,malloc返回不为NULL只能说明得到了线性空间的资源,真正物理内存分配失败时,进程还是会以资源不足为由,直接退出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值