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 : [
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分析未仔细展开。