Kernel Exception基本流程
框架:
一张网络上的图通俗易懂。
空间布局:
32 bit 为例,先说概括一下用户空间吧;
对于32位的RAM来说,最大的访问空间是4G,其中用户空间占0-3G,每个进程独享的;内核空间占3G-4G,所有进程共享。
接下里是那1G大小的内核空间:
- vmlinux代码/数据段:任何程序都有TEXT(可执行代码),RW(数据段),ZI段(未初始化数据段),用户空间也有,对应的是.text,.data,.bss
- module区域:kernel可以支持ko(模块),因此需要一段空间用于存储代码和数据段。
- vmalloc区域:kernel除了可以申请连续物理地址的内存外,还可以申请不连续的内存(虚拟地址是连续的),可以避免内存碎片化而申请不到内存。
- io map区域:留给io寄存器映射的区域,有些版本没有io map区域而是直接用vmalloc区域了。
- memmap:kernel是通过page结构体描述内存的,每一个页框都有对应的page结构体,而memmap就是page结构体数组。
随着Linux版本的更替,布局有稍许的变化,此处不深究。
对于32bit的来说,有高低内存之分:
hight memory:
ZONE_DMA 内存开始的16MB (直接存储器存取)
ZONE_NORMAL 16MB~896MB (正常的存取)
ZONE_HIGHMEM 896MB ~ 结束(1G) (高内存)
借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。
low memory:
由于kernel没办法将所有内存都映射进来,毕竟kernel自己只占1G,如果RAM超过1G,就无法全部映射。怎么办呢?只能先映射一部分了,这部分叫low memory。其他的就按需映射,VMALLOC区域就是用于按需映射的。
那如果是64bit的ARM:
可以讲整个RAM映射进来,就没有所谓的 high memory了,vmalloc区域功能除了外设寄存器也直接映射到vmalloc了,因此也就没有32bit布局里的 io map 区域了。
>= kernel-4.6/N0.MP8 kernel-4.4(patch back)
Start End Size Use
-----------------------------------------------------------------------
0000000000000000 0000007fffffffff 512GB user
ffffff8000000000 ffffff8008000000 128MB modules
ffffff8008080000 ................ ..... vmlinux
ffffff8008000000 ffffffbdbfff0000 246GB vmalloc
ffffffbdbfff0000 ffffffbdc0000000 64KB [guard page]
ffffffbdc0000000 ffffffbfc0000000 8GB vmemmap
ffffffbfc0000000 ffffffbffe7fd000 ~1G [guard]
ffffffbffe7fd000 ffffffbffec00000 4108KB fixed mappings
ffffffbffec00000 ffffffbffee00000 2MB [guard]
ffffffbffee00000 ffffffbfffe00000 16MB PCI I/O
ffffffbfffe00000 ffffffc000000000 2MB [guard]
ffffffc000000000 ffffffffffffffff 256GB kernel logical memory map
KE的类别:
- oops(类似于assert,有机会恢复)
- 内核行为表现为通知感兴趣模块,打印各种信息,如寄存器值,堆栈信息…
- 当出现oops时,我们就可以根据寄存器等信息调试并解决问题。
- /proc/sys/kernel/panic_on_oops为1时导致panic。
- panic(死机、重启)
- 表示Linux kernel遇到了一个不知道该怎么继续的情况。内核行为表现为通知感兴趣模块,死机或者重启。
- 在kernel代码里,有些代码加了错误检查,发现错误可能直接调用了panic(),并输出信息提供调试。
调试方法:
- 在线调试, Online debug, 指的是在程序的运行过程中监视程序的行为,分析是否符合预期。通常会借助一些工具,如GDB和Trace32等。有时候也会借助一些硬件设备的协助,如仿真器/JTAG,但是准备环境非常困难,而且用起来也很麻烦,除非一些runtime问题需要外很少使用。
- 离线调试, Offline debug, 指的是在程序的运行中收集需要的信息,在Bug发生后根据收集到的信息来分析的一种手段。通常也分为两种方式,一种是Logging,一种是Memory Dump。
前期异常处理:
在内核调用可以用来方便标记bug,提供断言并输出信息。最常用的两个是BUG()和BUG_ON()。当被调用的时候,它们会引发oops,导致栈的回溯和错误信息的打印。这就是软件主动回报异常的功能。
die()函数流程:
简单来说,触发oops
之后会走到debug_locks_off()
方法里打印一些log,如果这个异常是代码里调用BUG()/BUG_ON()
引起的,那么会通过report_bug()
方法进行额外的log说明。
再往下就是__die()方法
,因为绝大部分关键信息都是由它输出的,因此它的流程如下:
__die()输出的内容大致有:
-
基本错误信息,看一份kernel log有没有oops,直接搜索关键字Internal error就可以了。
-
CPU寄存器信息
-
寄存器附近的内存
-
调用栈
-
PC附近指令
panic()函数流程:
流程走到panic()就离异常重启不远了,panic()有标志性的log输出,大致如下:
关键字 Kernel panic
通知链:
panic()会调用栈上的回调函数通知感兴趣的模块,比如aee注册了回调函数,用于保存kernel log/mini dump等关键信息,并将其保存到emmc的expdb分区,等等重启后将其回读并保存成KE db。
nested panic流程:
有时die()/panic()流程不一定能正常走完,可能走到某一步又发生了异常,则就形成了嵌套,这种情况,我们一般不会关注后面的异常,而是关注最开始的那个异常。
为了避免异常嵌套,在发生第2次异常时,我们就拦截下来,我们在3个地方用于拦截nested panic:
- do_PrefetchAbort()
- do_DataAbort()
- do_undefinstr()
拦截后不走die()/panic()流程,因为这些流程可能会再次发生异常,走我们写的函数aee_stop_nested_panic()函数,在这里面尽量少用kernel模块,很有可能也会发生异常,仅仅将寄存器等重要信息输出到ram console就等死(死循环等等看门狗复位!) 在这个db文件里:SYS_LAST_KMSG。