前言
什么是Oops?从语言学的角度说,Oops应该是一个拟声词。当出了点小事故,或者做了比较尴尬的事之后,你可以说"Oops",翻译成中国话就叫做“哎呦”。“哎呦,对不起,对不起,我真不是故意打碎您的杯子的”。看,Oops就是这个意思。
在Linux内核开发中的Oops是什么呢?其实,它和上面的解释也没什么本质的差别,只不过说话的主角变成了Linux。当某些比较致命的问题出现时,我们的Linux内核也会抱歉的对我们说:“哎呦(Oops),对不起,我把事情搞砸了”。Linux内核在发生kernel panic时会打印出Oops信息,把目前的寄存器状态、堆栈内容、以及完整的Call trace都show给我们看,这样就可以帮助我们定位错误。
1. Oops的产生
挑选一位随机幸运内核,insmod oops.ko产生如下标准打印,产生了一段如下图打印: Oops 信息包含以下几部分内容:
- 一段文本描述信息。
比如类似“Unable to handle kernel NULL pointer dereference at virtual address 00000000”的信息,它说明了发生的是哪类错误 - Oops 信息的序号
比如是第 1 次、第 2 次等。这些信息与下面类似,中括号内的数据表示序号。 Internal error: Oops: 817 [#1] PREEMPT SMP ARM - 内核中加载的模块名称(也可能没有),以下面字样开头
Modules linked in:xxx - 发生错误的 CPU 的序号,对于单处理器的系统,序号为 0
CPU: 1 PID: 1412 Comm: insmod Tainted: P O 4.9.37 #1 下图是关于Tainted(污染)后面字段具体含义(可以注意到,第3部分加载的模块后面有些模块带有(PO)等字样,实际上就是和这里是相同的含义),源码路径: \kernel\panic.c
-
发生错误时 CPU 的各个寄存器值 -
当前进程的名字及进程 ID Process insmod (pid: 1412, stack limit = 0x9eb8e210) 这并不是说发生错误的是这个进程,而是表示发生错误时,当前进程是它。错误可能发生在内核代码、驱动程序,也可能就是这个进程的错误 -
栈信息 -
栈回溯信息,可以从中看出函数调用关系 -
出错指令附近的指令的机器码(出错指令在小括号里),也有可能没有 关于错误码,如下为armv7架构定义的FSR(错误状态寄存器,分为DFSR和IFSR,根据不同处理器使用不同的FSR)的错误代码,实际上源码中,oops的错误码就是通过汇编获取的寄存器值,如下为手册中IFSR获取错误码的方法(DFSR同样) 如下为DFSR结构(IFSR关于FS码是相同的) 对于上面0x817的错误码解释为:写入内存时报错,错误原因是:Translation fault 也就是页表转换出现问题
从上可以大致知道Oops 可以看成是内核级的Segmentation Fault。应用程序如果进行了非法内存访问或执行了非法指令,会得到Segfault信号,一般的行为是coredump,应用程序也可以自己截获 Segfault信号,自行处理。如果内核自己犯了这样的错误,则会打出Oops信息,也就是说Oops一般是由于内存原因导致的。
2.源码分析
2.1 溯源过程C部分
直接通过打印找到产生的对应代码,oops的打印为__die函数(\arch\arm\kernel\traps.c)。 第265行打印就是oops信息,后边三个字符串来自三个编译开关,分别表示允许抢占,支持对称多处理器,采用ARM指令。 第269行忽略,备注也写的陷阱和错误数在ARM上几乎没有意义 第273行,打印加载的模组信息 第274行,打印寄存器信息(CPU号,任务名,污染原因,PC,LR(链接寄存器,保存函数返回的地址),SP(栈指针),IP,FP(栈顶指针),R10-R0等寄存器值,CPU的Flags (Flags后边大写字母表示相应的位为1,小写表示为0)【指NZCV这几个状态寄存器】) 第275行,打印了当前出错的进程名,pid值,和堆栈限制,在ARM平台栈的增长方向是从高地址向低地址,sp指针是当前的栈顶,stack limit打印的是栈的限制,表示最小地址是多少,如果SP比这个值小,那么表示栈溢出了。 第279行之后打印堆栈和函数的调用回溯。 如下为pt_reg的定义 进一步溯源,深入探究oops源码,调用__die的函数为die(\arch\arm\kernel\traps.c): 第344行:调用oops_begin,在这个地方关闭本CPU中断,获取CPUID, 对oops上锁.如果同一个CPU已经在处理die了,那么就是嵌套die,不需要再获取锁了 第347行获取cpu是不是处于用户模式,如果不是用户模式并且report_bug的返回值如果不等于BUG_TRAP_TYPE_NONE打印会变为”Oops - BUG”,如果是这种情况,就比较严重,一般会打印如下 第355行:die的最后是调用oops_end,这里边的操作很多是和oops_begin相对应的,然后调用oops_exit,该函数会打印trace结束标志,调用kmsg_dump(KMSG_DUMP_OOPS)。但是如果oops产生在中断过程中,oops_end函数会直接产生panic或者如果配置宏CONFIG_PANIC_ON_OOPS_VALUE的值为1(panic_on_oops),则也会直接panic 通常情况由于空指针或者错误的虚拟地址导致的oops,函数为:__do_kernel_fault。源码位于\arch\arm\mm\fault.c 第138行:尝试进行异常修复,这里有一套很复杂的内存异常回复处理,不深入,失败后会继续向下执行 第152行:执行完前面的die操作后,直接干掉出问题的进程 继续溯源,可以找到在\arch\arm\mm\fault.c中发现两个函数都有调用__do_kernel_fault。分别是do_bad_area和do_page_fault,这里先不具体分析其源码,继续溯源
调用do_bad_area函数的有如下函数:do_alignment,do_translation_fault,do_sect_fault 调用do_page_fault函数的有:do_translation_fault 而最终汇总成如下该数组 最终由函数do_DataAbort调用
2.2溯源过程汇编部分
以下部分为汇编过程,并且涉及到部分内存申请流程。 当内核申请内存时,虚拟内存映射到实际物理内存,系统自动触发缺页中断,缺页中断机制根据所访问页面的状态来分配物理页面并建立映射关系。触发缺页中断的情况有两种 , 第一,程序访问了非法地址(我们主要分析的);第二,访问的地址是合法的,但是该地址还未分配物理页框。
当程序访问的虚拟页面没有进行过物理页面的映射时,会通过发生缺页中断来分配和映射物理页面。发生缺页中断时,处理器会跳转到异常向量表 Data abort 向量中开始执行缺页中断的汇编阶段,这个阶段与处理器架构紧密联系,例如对于ARMv7-A架构,汇编处理流程为:__vectors_start -> vector_dabt -> __dabt_usr/__dabt_svc -> dabt_helper -> v7_early_abort 如下为中断向量表,源码位于:arch\arm\kernel\entry-armv.S
以svc为例,会调用dabt_helper 最后dabt_helper会bl到CPU_DABORT_HANDLER这个函数中,根据使用的架构不同,该函数使用的可能会不相同 如下使用的v7架构,使用函数为v7_early_abort v7_early_abort源码位于:\arch\arm\mm\abort-ev7.S 这个函数实际上就是实现了从arm中获取FSR(错误状态寄存器)和FAR(错误地址寄存器,也就是要映射的地址),r0=地址,ri=错误码,r2=pt_regs(在对应的__dabt_svc中已经获取)
2.3 do_DataAbort的函数注册
从2.1和2.2分别对C部分和汇编部分进行简单的分析,下面来看一下do_DataAbort是如何识别不同的页面分配的 如下函数为对fsr_info数组的注册函数,因为do_DataAbort实际上就是根据fsr_info这个数组进行函数调用的 全局搜索hook_fault_code可以发现如下:实际上也就是对fsr_info数组补齐了段错误的函数回调 也就是说,接下来只要对着fsr_info数组这个数组进行分析,就能知道oops的全部产生原因了
2.4 总流程图
3.oops产生原因分析
如下表,为汇总的frs_info,包括对齐,页表转换,页,段权限 我们继续对源码进行分析。如下图为do_DataAbort函数中对fsr寄存器读取数据的处理。也就是对fsr寄存器取fs,因为fs分布为fs[3:0]位于bit3:0,fs[4]位于bit10,所以处理后对fsr_info进行直接查表即可 第547行如上分析 第550行执行表中对应函数,只有do_bad会返回1,其余函数皆返回0. 第561行,执行由于do_bad对应的fsr导致的错误,arm_notify_die中判断当前CPU是否处于用户态,如果不是则执行die
3.1 do_translation_fault
static int __kprobes
do_translation_fault(unsigned long addr, unsigned int fsr,
struct pt_regs *regs)
{
#define TASK_SIZE (UL(CONFIG_PAGE_OFFSET) - UL(SZ_16M))
if (addr < TASK_SIZE)
return do_page_fault(addr, fsr, regs);
if (user_mode(regs))
goto bad_area;
return 0;
bad_area:
do_bad_area(addr, fsr, regs);
return 0;
}
如下图为do_bad_area源码 第195判断是否处于用户模式,如果不是就Oops
3.2 do_page_fault
直接看下图流程即可,不进行具体分析,总之在处于非用户模式下缺页且处理出现错误,会执行__do_kernel_fault。题外:do_page_fault完成了真正的物理页面分配工作,另外栈扩展、mmap的支持等也都在这里。对于物理页面的分配,会调用到do_anonymous_page->。。。-> __rmqueue,__rmqueue中实现了物理页面分配的伙伴算法
3.3 do_sect_fault
源码如下,一旦出错,直接do_bad_area
3.4 bad_mode
bad_mode(中断异常)也可以导致die并且最终直接panic,源码如下 引用流程:xx中断异常 -> xx_invalid -> common_invalid -> bad_mode
3.5总结
4. Oops的解决思路
1.先看是否由BUG/BUG_ON引起,如果是BUG引起的,则直接看产生条件,这种具体情况具体分析 2.如果不是,则可以根据Oops现场打印进一步分析。我们继续看在第一章中产生的Oops信息。如下图。首先直接看到了错误原因,空指针引起的,然后看到错误码0x817:即写内存时,缺页,映射失败。接下来直接看PC指针就行了 3.PC is at myoops_test_init +0xc/0x14 确定了出问题的函数位置,然后看一下出错进程是insmod 也就是说就是在insmod oops.ko驱动时后出的问题。接下来需要对该进程增加调试信息,以供我们能够找到出错位置 4.增加 –g编译选项,见下图。如果file带有stripped,说明makefile或者脚本中存在选项,将其暂时屏蔽即可。 5.对于内核增加调试信息,直接搜索debug_info,将其打开即可 6.使用对应工具链的gbd定位问题源码所在位置 查看代码(默认显示10行) l/list 例:l *(函数名+偏移) 然后根据定位到的对应源码上下文继续查找问题即可 7.使用addr2line定位内核中问题源码。如下图为之前出现问题的一串oops打印。可以发现问题出现在dwc2_queue_transaction。我们直接找到内核对应的符号表,找到该函数对应内核的所在位置 确定其偏移为 0x8054ced8+0xf8=0x8054CFD0 使用命令 xxx(工具链)-addr2line -C -f -e vmlinux 8054CFD0,确定到了问题所在行数2805 附addr2line参数说明: (1).-a:在函数名、文件名和行号信息之前,以十六进制形式显示地址。 (2).-b:指定目标文件的格式为bfdname。 (3).-C:将低级别的符号名解码为用户级别的名字。 (4).-e:指定需要转换地址的可执行文件名,默认文件是a.out。 (5).-f:在显示文件名、行号信息的同时显示函数名。 (6).-s:仅显示每个文件名(the base of each file name)去除目录名。 (7).-i:如果需要转换的地址是一个内联函数,则还将打印返回第一个非内联函数的信息。 (8).-j:读取指定section的偏移而不是绝对地址。 (9).-p:使打印更加人性化:每个地址(location)的信息都打印在一行上。 (10).-r:启用或禁用递归量限制。 (11).–help:打印帮助信息。 (12).–version:打印版本号。
进阶:反汇编方案,适合高手 使用命令:arm-seev100-linux-gnueabihf-objdump -d oops.ko > test.s 然后直接生撸汇编,从PC可以看出出错在0xc。此时r3=0,r2=1。Str即将r2中数据给到r3指向的内存即0。而0这个内存地址很明显是非法的
|