ARM平台Linux内核空指针异常处理流程

ARM平台Linux内核空指针异常处理流程

平台:Linux 3.10.40 + ARM V7

一 从异常向量入口到__do_kernel_fault

访问空指针在ARM平台上属于data abort异常,对应异常向量中的vector_dabt。

文件:arch/arm/kernel/entry-armv.S

__vectors_start:

         W(b)          vector_rst

         W(b)          vector_und

         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

在vector_dabt中,因为是内核发生异常,且内核运行在supervisor中,所以跳转到__dabt_svc中执行。

/*

* Data abort dispatcher

* Enter in ABT mode, spsr = USR CPSR, lr = USR PC

*/

         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)

         .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

__dabt_svc实现如下:

         __dabt_svc:

         svc_entry

         mov r2, sp

         dabt_helper

THUMB(         ldr    r5, [sp, #S_PSR]        )        @ potentially updated CPSR

         svc_exit r5                                    @ return from exception

UNWIND(.fnend              )

ENDPROC(__dabt_svc)

dabt_helper宏展开如下,在未定义MULTI_DABORT的情况下调用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.

         @

#ifdef MULTI_DABORT

         ldr    ip, .LCprocfns

         mov lr, pc

         ldr    pc, [ip, #PROCESSOR_DABT_FUNC]

#else

         bl      CPU_DABORT_HANDLER

#endif

         .endm

根据平台不同./arch/arm/include/asm/glue-df.h中CPU_DABORT_HANDLER的定义也不同,可以参考./arch/arm/mm/Makefile,假如配置了CONFIG_CPU_ABRT_EV7那么是v7_early_abort,实现在abort-ev7.c中,会调用do_DataAbort

后续的调用序列为:

arch/arm/mm/fault.c中do_DataAbort -> do_translation_fault -> do_page_fault -> __do_kernel_fault

二 __do_kernel_fault

/*

* Oops.  The kernel tried to access some page that wasn't present.

*/

static void

__do_kernel_fault(struct mm_struct *mm, unsigned long addr, unsigned int fsr,

                     struct pt_regs *regs)

{

         /*

         * Are we prepared to handle this kernel fault?

         */

         if (fixup_exception(regs))

                   return;

         /*

         * No handler, we'll have to terminate things with extreme prejudice.

         */

         bust_spinlocks(1);

         printk(KERN_ALERT

                   "Unable to handle kernel %s at virtual address %08lx\n",

                   (addr < PAGE_SIZE) ? "NULL pointer dereference" :

                   "paging request", addr);

         show_pte(mm, addr);

         die("Oops", regs, fsr);

         bust_spinlocks(0);

         do_exit(SIGKILL);

}

__do_kernel_fault打印出我们在Kmsg中第一句空指针异常日志,如:

Unable to handle kernel NULL pointer dereference at virtual address 00000004

从代码实现可以看出只要地址小于PAGE_SIZE都会打印出是空指针,而不仅仅是0地址才打印空指针。

show_pte打印page table相关信息,后边调用die函数。die实现在arch/arm/kernel/traps.c文件中

三 die

代码位于arch/arm/kernel/traps.c。

/*

* This function is protected against re-entrancy.

*/

void die(const char *str, struct pt_regs *regs, int err)

{

         enum bug_trap_type bug_type = BUG_TRAP_TYPE_NONE;

         unsigned long flags = oops_begin();

         int sig = SIGSEGV;

         if (!user_mode(regs))

                   bug_type = report_bug(regs->ARM_pc, regs);

         if (bug_type != BUG_TRAP_TYPE_NONE)

                   str = "Oops - BUG";

         if (__die(str, err, regs))

                   sig = 0;

#ifdef CONFIG_HISI_RDR

         if (NULL != p_exc_hook) /*excute exc hook func*/

                   p_exc_hook((int)current, (int)arm_exc_type, (int)regs);

#endif

         oops_end(flags, regs, sig);

}

die中首先调用oops_begin,在这个地方关闭本CPU中断,获取CPUID, 对oops上锁.如果同一个CPU已经在处理die了,那么就是嵌套die,不需要再获取锁了。

console_verbose的作用是提高console的Loglevel,使console中打印出所有的日志。

static unsigned long oops_begin(void)

{

         int cpu;

         unsigned long flags;

         oops_enter();

         /* racy, but better than risking deadlock. */

         raw_local_irq_save(flags);

         cpu = smp_processor_id();

         if (!arch_spin_trylock(&die_lock)) {

                   if (cpu == die_owner)

                            /* nested oops. should stop eventually */;

                   else

                            arch_spin_lock(&die_lock);

         }

         die_nest_count++;

         die_owner = cpu;

         console_verbose();

         bust_spinlocks(1);

         return flags;

}

接着是判断是不是user模式,如果不是user模式调用./lib/bug.c中的report_bug函数,

  3. Implement the trap

     - In the illegal instruction trap handler (typically), verify

       that the fault was in kernel mode, and call report_bug()

     - report_bug() will return whether it was a false alarm, a warning,

       or an actual bug.

     - You must implement the is_valid_bugaddr(bugaddr) callback which

       returns true if the eip is a real kernel address, and it points

       to the expected BUG trap instruction.

以上是bug.c中对report_bug的说明,report_bug的返回值如果不等于BUG_TRAP_TYPE_NONE,那么str会被赋值为"Oops - BUG"。

四 __die

首先是打印错误字符串,如:Internal error: Oops: 805 [#1] PREEMPT SMP ARM。

错误字符串就是Oops,err是0x805,1表示仅die一次,后边三个字符串来自三个编译开关,分别表示允许抢占,支持对称多处理器,采用ARM指令。

从整个函数调用分析err的来源,在最开始可以跟踪到是在v7_early_abort中,实现代码如下:

mrc     p15, 0, r1, c5, c0, 0           @ get FSR

参考ARM的说明文档,这个是fault status registers,在ARMv7上定义了两种Auxiliary Fault Status Registers,分别是ADFSR,和AIFSR,D代表Data,I代表Instruction。这个寄存器的具体含义是根据处理器实现不同而不同的,如果是A53处理器,这个是DFSR,该寄存器的值为805表示写出错,类型为translation faults, section.

notify_die的作用是调用所有die_chain上注册的回调函数,在kernel/notifier.c中实现。

print_modules函数打印模块信息,日志从"Modules linked in:"开始。

static int __die(const char *str, int err, struct pt_regs *regs)

{

         struct task_struct *tsk = current;

         static int die_counter;

         int ret;

         printk(KERN_EMERG "Internal error: %s: %x [#%d]" S_PREEMPT S_SMP

                S_ISA "\n", str, err, ++die_counter);

         /* trap and error numbers are mostly meaningless on ARM */

         ret = notify_die(DIE_OOPS, str, regs, err, tsk->thread.trap_no, SIGSEGV);

         if (ret == NOTIFY_STOP)

                   return 1;

         print_modules();

         __show_regs(regs);

         printk(KERN_EMERG "Process %.*s (pid: %d, stack limit = 0x%p)\n",

                   TASK_COMM_LEN, tsk->comm, task_pid_nr(tsk), end_of_stack(tsk));

         if (!user_mode(regs) || in_interrupt()) {

                   dump_mem(KERN_EMERG, "Stack: ", regs->ARM_sp,

                            THREAD_SIZE + (unsigned long)task_stack_page(tsk));

                   dump_backtrace(regs, tsk);

                   dump_instr(KERN_EMERG, regs);

         }

         return 0;

}

__show_regs内容较多,在下一节单独讨论。

以下这句打印比较重要,在ARM平台栈的增长方向是从高地址向低地址,sp指针是当前的栈顶,stack limit打印的是栈的限制,表示最小地址是多少,如果SP比这个值小,那么表示栈溢出了。

Process swapper/2 (pid: 0, stack limit = 0xe028e238)

dump_mem打印栈顶信息。dump_backtrace打印函数调用序列。dump_instr是打印指令的意思,输出PC指针附近的指令,其实这个意义不大,因为在__show_regs中打印的更多。

Code: e5843014 e2813020 e5910020 e5220008 (e5802004)

五 __show_regs

__show_regs会依次调用show_regs_print_info,dump_stack_print_info。

CPU: 2 PID: 0 Comm: swapper/2 Tainted: G        W    3.10.40-g6101b41 #1

“Tainted: G        W   ”的输出来自print_tainted函数。

如果panic代码是在处理work那么work相关的信息将通过print_worker_info函数打印出来,包括该workqueue的名称,通过set_worker_desc设置的该work的描述信息。

接着打印的是当前任务和线程的结构体指针:

task: e0264fc0 ti: e028e000 task.ti: e028e000

__show_regs后半部分打印信息如下,根据寄存器状态打印,需要注意的是Flags后边大写字母表示相应的位为1,小写表示为0

PC is at run_timer_softirq+0xdc/0x240

LR is at _raw_spin_lock_irq+0x24/0x28

pc : []    lr : []    psr: 200001d3

sp : e028fe28  ip : e028fe10  fp : e028fe5c

r10: 3f4f9f7c  r9 : c0b06084  r8 : 0000000a

r7 : 00000101  r6 : e028e000  r5 : 0000009a  r4 : e02a2000

r3 : e02a24f0  r2 : e028fe28  r1 : e02a24d0  r0 : 00000000

Flags: nzCv  IRQs off  FIQs off  Mode SVC_32  ISA ARM  Segment kernel

以下调试信息是关于CP15的Control后边打印的是system control registers,Table后边打印的是translte table base register, DAC后边的事Domain access control register。

Control: 10c5387d  Table: 25a6406a  DAC: 00000015

__show_regs的最后是打印各个寄存器指向地址前后各128字节的内容。并不是所有的寄存器都打印,如果寄存器地址小于PAGE_SIZE或者大于-256,那么将跳过。

六 oops_end

die的最后是调用oops_end,这里边的操作很多是和oops_begin相对应的,需要注意的是打开中断的地方。调用oops_exit,该函数会打印trace结束标志,调用kmsg_dump(KMSG_DUMP_OOPS),这就是为什么ramoops特性可以扑捉oops的原因。

接着判断是否在处理FIQ,如果没有在处理fiq,那么判断是不是在处理中断,如果是在处理中断那么调用panic。panic_on_oops控制在oops时是否panic,如果为1那么调用panic,最后如果signr不为0,调用do_exit,正常情况下都应该是SIGSEGV,最后的处理在do_exit中。需要注意的是panic函数是不会返回的,所以如果前边调用了panic,那么后边的do_exit函数将不会被调用。

static void oops_end(unsigned long flags, struct pt_regs *regs, int signr)

{

         if (regs && kexec_should_crash(current))

                   crash_kexec(regs);

         bust_spinlocks(0);

         die_owner = -1;

         add_taint(TAINT_DIE, LOCKDEP_NOW_UNRELIABLE);

         die_nest_count--;

         if (!die_nest_count)

                   /* Nest count reaches zero, release the lock. */

                   arch_spin_unlock(&die_lock);

         raw_local_irq_restore(flags);

         oops_exit();

         if (fiq_in_progress == 0) {

                   if (in_interrupt())

                            panic("Fatal exception in interrupt");

                   if (panic_on_oops)

                            panic("Fatal exception");

                   if (signr)

                            do_exit(signr);

         }

}

七 panic

panic函数实现注释上已经说明,该函数将挂起系统,不会返回。

调用local_irq_disable停止本核中断,防止再次发生中断同时又发生panic,发生死锁。紧接着获取锁,如果无法得到锁,那么进入死等,这个不是好事情。

后边几个关键点依次是调用printk打印异常信息,调用smp_send_stop()停其他核。调用kmsg_dump(KMSG_DUMP_PANIC),这就是为什么ramoops中经常一个oops后边紧接着就是一个panic的缘故,调用atomic_notifier_call_chain,处理所有的钩子函数。后边是延时一段时间,调用emergency_restart重启系统。不同的平台有不同的实现,最后可能会进入一个死循环。

遗留问题:

1. MULTI_DABORT在什么情况下定义,作用是什么

2. bug.c中report_bug原理及应用;

3. print_tainted函数的原理及作用;

4. c_backtrace实现在/arch/arm/lib/backtrace.S,未仔细分析;

5. panic分析未仔细展开。

你可能感兴趣的:(Linux)