[monitor] 4. Linux内核Oops机制

1、内核异常

Linux内核在陷入一些不可恢复的异常时,马上就要崩溃了,内核陷入了恐慌,这种内核的异常除了复位也没有什么更好的手段。在复位之前内核会打出出现异常的寄存器、堆栈等故障场景信息,以供故障定位之用,这些信息就叫做Oops信息。

1.1、BUG_ON()

在内核的关键点中,常常预设一些判断条件来捕获内核的异常,如果这些条件成立表明内核出现了bug或者warn。内核会调用panic复位或者仅仅打印一条warning消息。

1

1.2、die()

BUG_ON()捕获的是一些预置异常,而die用来处理系统中的一些动态异常,比如非法地址访问、非法指令、除0异常等等,这些异常会触发cpu的异常处理程序,最终都会调用die()函数来处理。
例如一个完整的die信息打印如下:

2

die()函数的具体处理:

3
4
5

1.2.1、show_registers()

6
7
8
9
10
11
12
13
14
15

1.3、panic()

BUG_ON()和die()的处理中,如果决定复位系统就会调用panic()。panic()函数做一些复位前的处理然后复位系统。

16
17
18

2、相关机制

在异常处理中,有一些机制是需要注意的。

2.1、反汇编定位

在Oops信息打印出故障点的位置和堆栈信息以后,我们需要根据故障点找到源码中对应的位置。我们需要使用同一份源码编译出带调试信息的可执行代码,并根据可执行代码反汇编出一份带调试信息的汇编代码。

  • 第一步:编译带调试信息的可执行代码;

如果是编译用户文件或者驱动模块,可以简单的在Makefile的CFLAGS中加入“-g”选项:

19

如果是内核,需要配置make menuconfig使CONFIG_DEBUG_INFO选项被配置:

20
21

  • 第二步:反汇编带调试信息的可执行代码,反汇编命令“objdump -DSslt ”;

22
23

  • 第三步:根据oops中的故障点,在反汇编出的.s文件中找到对应的源码;

2.2、钩子函数

在die()和panic()中都会调用钩子函数,用户可以把自己的函数注册到钩子链表中去,在对应的时机会被系统调用。
die()函数调用的钩子函数:

24
25

panic()函数调用的钩子函数:

26

2.3、堆栈回溯

在系统异常时,需要根据异常的堆栈信息,反推出发生异常时的堆栈调用关系。在上面用到的堆栈trace函数print_context_stack()中,我们看到了回溯堆栈的两种方法。

27

我们以arm cpu的实现方式为例,说明使用堆栈帧方式的堆栈回溯方法:

ARM 系列处理器有31个32位的通用寄存器。其中l6个(R0-R15)在所有模式下均可见,其余l5个用于加速异常处理。
有3个通用寄存器具有特殊用途,它们同异常处理的现场分析紧密相关:

  • (1)链接寄存器R14(Link Register),用于保存返回地址。当出现异常时,异常返回地址将存放在相应模式的R14_mod中。
  • (2)堆栈指针寄存器R13(StackPointer Register),用于指示栈顶位置,在ARM 处理器的每种模式均有。
  • (3)程序计数器R15 (Program CounterRegister),指向当前指令之后两条指令的位置。另外一些编译器(例如GCC)把R1 1用作堆栈帧寄存器(Stack FrameRegister),它是进行堆栈回溯的重要依据。

通常,编译器产生的代码中对堆栈的结构和使用存在一个约定,其中一个重要的概念是堆栈帧(Stack Frame)或称为活动记录(Active Record)。一个堆栈帧就是运行栈中的一个存储块,它按照一定的规则描述了当前函数的调用信息,一个栈框架对应一次函数调用。Tornado forARM 的gcc编译器产生的运行堆栈结构如图。

28

由图3可见,SP为异常现场SP(Stack Pointer)寄存器值,FP为异常现场FP(Frame Pointer)寄存器值。FP[n]表示第n层函数调用时压栈的FP寄存器值。IP[n]表示第n层函数调用时压栈的SP寄存器值。LRIn]表示第n层函数调用时压栈的LR寄存器(Link Register)值,其中保存了此次调用的返回地址。PCln]表示第n层函数调用的入口地址。每个堆栈帧用一种颜色标识,FP向低地址方向偏移3个位置就能得到上一个堆栈帧的位置, 由此将堆栈帧链接起来。

对于几乎所有的函数,ARM 的机器码中,函数开头都有如下形式的函数序言:

mov r12r13
stmdb r13 1,{rl 1,rl2,r14,pc)
sub rl1.rl2.#4

在函数的结尾都有如下形式的函数尾声:

ldmfd r13 1,{rl 1r13,pc)

代码段中特征十分明显的函数序言恰好位于函数入口处。因此匹配到函数序言,就找到了函数入口地址,再从系统符号表中查找此地址对应的符号,就得到了函数名。

2.4、内核符号表

在系统出故障打印堆栈信息的时候,不但需要分析出堆栈的调用关系,还需要根据地址解析出符号信息。
2.4的内核符号表保存在System.map文件中,在2.6中内核符号表已经被编译到内核中。在编译内核时,会根据内核符号生成.tmp_kallsyms2.s/.tmp_kallsyms3.s文件,再把这个符号表文件联编到vmlinux中。
生成kallsyms的方法是在内核源文件根目录的makefile中定义的:

29

scripts/kallsyms脚本根据“nm –n file”的结果,生成tmp_kallsyms%.S文件。tmp_kallsyms%.S中定义的就是kallsyms的相关数据:

30
31
32

符号表的基本元素就是符号名和符号地址,不过kallsyms的规模比较庞大,为了加速和压缩的一些操作,所以才弄出这6个结构来表述kallsyms符号表。最终kallsyms会被链接到vmlinux的.rodata section 中。
在符号表查询函数kallsyms_lookup()中,会根据地址找到最近的符号表,并给出偏移。

33
34

posted @ 2017-10-13 13:59  pwl999  阅读(214)  评论(0)    收藏  举报