前言
前几篇我们讲了下Linux驱动基础和Linux内核调试的文章,没看的同学可以看下,本篇以做实验为目的来给大家分享下如何分析Oops错误。
本篇环境
硬件平台:飞凌OK3588开发板
编译环境:Ubuntu 20.04 LTS
编译工具链:aarch64-linux-gnu-
Oops的简介
Oops对于做嵌入式Linux底层开发人员来说应该是比较多见的问题,它是Linux内核发生不正确的行为(比如访问到非法地址)并产生一份错误报告里面包含当时将产生异常时出错原因,CPU的状态,出错的指令地址、数据地址及其他寄存器,函数调用的顺序甚至是栈里面的内容都打印出来,然后根据异常的严重程度来决定下一步的操作:杀死导致异常的进程或者挂起系统,发生在非中断上下文中是以一个信号退出进程而已,但是如果发生在中断上下文之间就是panic系统宕机,如果设置了panic_on_oops,任何oops都是panic,如果设置了panic_on_oops,任何oops都是panic。接下来我们讲讲如果拿到Oops错误报告,该如何应对分析它。
如何手动触发oops
Linux内核提供了一些手动触发oops的API接口
-
BUG_ON():打印bug相关信息,并进入oops流程 -
BUG():
Oops错误的实例
下面看一个Oops的空指针的demo:
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/init.h>
static void create_oops(void)
{
*(int *)0 = 0;
}
static int __init my_oops_init(void)
{
printk("oops module init\n");
create_oops();
return 0;
}
static void __exit my_oops_exit(void)
{
printk("oops module exit\n");
}
module_init(my_oops_init);
module_exit(my_oops_exit);
MODULE_LICENSE("GPL");
Makefile
KERNELDIR ?= /home/forlinx/OK3588_Linux_fs/kernel
obj-m += oops_test.o
all: modules
modules:
$(MAKE) ARCH=arm64 CROSS_COMPILE=aarch64-none-linux-gnu- -C $(KERNELDIR) M=$(shell pwd) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules_install
clean:
$(MAKE) -C $(KERNELDIR) M=$(shell pwd) modules clean
接下来我们放到板子进行实验
0;root@ok3588# insmod oops_test.ko
[ 173.330566] oops_test: loading out-of-tree module taints kernel.
[ 173.331004] oops module init
[ 173.331011] Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000
[ 173.331016] Mem abort info:
[ 173.331018] ESR = 0x96000045
[ 173.331021] EC = 0x25: DABT (current EL), IL = 32 bits
[ 173.331024] SET = 0, FnV = 0
[ 173.331026] EA = 0, S1PTW = 0
[ 173.331028] Data abort info:
[ 173.331030] ISV = 0, ISS = 0x00000045
[ 173.331032] CM = 0, WnR = 1
[ 173.331034] user pgtable: 4k pages, 39-bit VAs, pgdp=0000000108782000
[ 173.331038] [0000000000000000] pgd=0000000000000000, p4d=0000000000000000, pud=0000000000000000
[ 173.331047] Internal error: Oops: 96000045 [#1] PREEMPT_RT SMP
[ 173.331052] Modules linked in: oops_test(O+)
[ 173.331060] CPU: 0 PID: 1458 Comm: insmod Tainted: G O 5.10.66-rt53 #21
[ 173.331065] Hardware name: Forlinx OK3588 Board (DT)
[ 173.331067] pstate: 60400009 (nZCv daif +PAN -UAO -TCO BTYPE=--)
[ 173.331072] pc : my_oops_init+0x28/0x1000 [oops_test]
[ 173.331082] lr : my_oops_init+0x24/0x1000 [oops_test]
[ 173.331088] sp : ffffffc01391bb20
[ 173.331091] x29: ffffffc01391bb20 x28: ffffff811e6db3b8
[ 173.331096] x27: 0000000000000003 x26: 0000000000000000
[ 173.331102] x25: 0000000000000019 x24: 0000000000000000
[ 173.331106] x23: 0000000000000000 x22: ffffffc011fa28c0
[ 173.331111] x21: ffffffc011fa4380 x20: ffffffc009035000
[ 173.331116] x19: ffffffc011fa2900 x18: 0000000000000000
[ 173.331121] x17: 0000000000000000 x16: 0000000000000000
[ 173.331126] x15: 180f0a0700000000 x14: 00656c75646f6d5f
[ 173.331132] x13: 0000000000000000 x12: 0000000000000018
[ 173.331136] x11: 0101010101010101 x10: ffffffff7f7f7f7f
[ 173.331141] x9 : ffffffc0100a07d0 x8 : 74696e6920656c75
[ 173.331146] x7 : 646f6d2073706f6f x6 : ffffffc012055ae9
[ 173.331151] x5 : ffffffc012055ae8 x4 : ffffff81feeb1b70
[ 173.331156] x3 : 0000000000000000 x2 : 0000000000000000
[ 173.331161] x1 : ffffff8119b2eac0 x0 : 0000000000000000
[ 173.331166] Call trace:
[ 173.331169] my_oops_init+0x28/0x1000 [oops_test]
[ 173.331176] do_one_initcall+0xb4/0x210
[ 173.331183] do_init_module+0x68/0x210
[ 173.331191] load_module+0x1cb4/0x2258
[ 173.331196] __do_sys_finit_module+0xe0/0x100
[ 173.331202] __arm64_sys_finit_module+0x28/0x34
[ 173.331207] el0_svc_common.constprop.0+0x154/0x204
[ 173.331213] do_el0_svc+0x8c/0x98
[ 173.331218] el0_svc+0x20/0x30
[ 173.331224] el0_sync_handler+0xd8/0x184
[ 173.331229] el0_sync+0x1a0/0x1c0
[ 173.331234]
[ 173.331234] PC: 0xffffffc009034f28:
....
[ 173.336788] Code: 910003fd 91000000 95fee5c3 d2800000 (b900001f)
[ 173.336793] ---[ end trace 0000000000000002 ]---
Oops错误的分析
前期日志的分析
分析下Oops上述一段日志错误的信息
[ 173.330566] oops_test: loading out-of-tree module taints kernel.
意思是加载树外的模块会污染内核,因为内核在编译的时候选择支持内核签名机制,我在Makefile上没有开启签名机制
Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000
意思是指出了一个内核错误,即空指针解引用错误。地址为 0x0000000000000000
,说明在代码中存在对空指针的操作
[ 173.331047] Internal error: Oops: 96000045 [#1] PREEMPT_RT SMP
96000045
表示错误码,我也不是很清楚具体含义,知道的可以下面评论。后面[]内的数值是与页面有关的oops信息被显示的次数,这里是#1代表被显示1次。之后显示内核的重要特性SMP和PREEMPT被显示的配置情况。这条信息所在的内核启用了PREEMPT_RT SMP支持,所以是多核支持实时内核抢占
Oops的错误码根据错误的原因会有不同的定义,如果发现自己遇到的Oops和下面无法对应的话,最好去内核代码里查找:
-
error_code: -
bit 0 == 0 means no page found, 1 means protection fault -
bit 1 == 0 means read, 1 means write -
bit 2 == 0 means kernel, 1 means user-mode -
bit 3 == 0 means data, 1 means instruction
[ 173.331052] Modules linked in: oops_test(O+)
Modules linked in为加载了的模块列表,oops_test
为加载的模块名。
[ 173.331052] Modules linked in: oops_test(O+)
[ 173.331060] CPU: 0 PID: 1458 Comm: insmod Tainted: G O 5.10.66-rt53 #21
[ 173.331065] Hardware name: Forlinx OK3588 Board (DT)
CPU后的数字是错误所在逻辑CPU的编号,这里是CPU0,表示CPU0发生的错误,PID:1458表示正在运行的进程ID1458,内核污染原因(G),内核版本( 5.10.66-rt53)。
内核污染原因包括私有驱动加载(P),模块强制加载(F),模块强制卸载(R),机器检查异常发生(M),检测到错误页(B)等。如果涉及到了某项原因,就会显示为Tainted: G PF R
这样。如果不存在问题,就会显示为Not Tainted
。Tainted
的敏感字符可以从内核中Documentation/oops-tracing.txt找到
1:'G'如果所有装载的模块都有GPL或相容的许可证,'P'如果装载了任何的专有模块。没有模块MODULE_LICENSE或者带有insmod认为是与GPL不相容的的MODULE_LICENSE的模块被认定是专有的。
2:'F'如果有任何通过“insmod -f”被强制装载的模块,' '如果所有模块都被正常装载。
3:'S'如果oops发生在SMP内核中,运行于没有证明安全运行多处理器的硬件。 当前这种情况仅限于几种不支持SMP的速龙处理器。
4:'R'如果模块通过“insmod -f”被强制装载,' '如果所有模块都被正常装载。
5:'M'如果任何处理器报告了机器检查异常,' '如果没有发生机器检查异常。
6:'B'如果页释放函数发现了一个错误的页引用或者一些非预期的页标志。
7:'U'如果用户或者用户应用程序特别请求设置污染标志,否则' '。
8:'D'如果内核刚刚死掉,比如有OOPS或者BUG。
Hardware name表示硬件平台的名称。这里我们是飞凌的OK3588开发板
[ 173.331072] pc : my_oops_init+0x28/0x1000 [oops_test]
[ 173.331082] lr : my_oops_init+0x24/0x1000 [oops_test]
[ 173.331088] sp : ffffffc01391bb20
指明了出错信息位于my_oops_init+0x28/0x1000 [oops_test]
表示pc指针指向错误发生的地址是my_oops_init
函数的第0x28个字节,0x1000表示my_oops_init
函数的占用大小。
lr指针指向my_oops_init+0x24/0x1000 [oops_test]
表示pc指针指向错误发生的地址是oops_test
中的my_oops_init
函数偏移第36个字节,0x1000表示my_oops_init
函数的占用大小。
sp指向的是ffffffc01391bb20
[ 173.331166] Call trace:
[ 173.331169] my_oops_init+0x28/0x1000 [oops_test]
[ 173.331176] do_one_initcall+0xb4/0x210
[ 173.331183] do_init_module+0x68/0x210
[ 173.331191] load_module+0x1cb4/0x2258
[ 173.331196] __do_sys_finit_module+0xe0/0x100
[ 173.331202] __arm64_sys_finit_module+0x28/0x34
[ 173.331207] el0_svc_common.constprop.0+0x154/0x204
[ 173.331213] do_el0_svc+0x8c/0x98
[ 173.331218] el0_svc+0x20/0x30
[ 173.331224] el0_sync_handler+0xd8/0x184
[ 173.331229] el0_sync+0x1a0/0x1c0
函数调用栈回溯信息,可以从中看出函数调用关系
[ 173.331234] PC: 0xffffffc009034f28:
PC指向0xffffffc009034f28
解读完Oops的日志,下面就要根据oops日志确定出错位置是内核函数还是驱动
System.map文件记录了所有符号的运行地址,这里的符号可以理解成函数名和变量。
System.map一般在内核编译完成后,根目录下生成。
0000000000000000 A _kernel_flags_le_hi32
0000000000000000 A _kernel_size_le_hi32
000000000000000a A _kernel_flags_le_lo32
0000000000000200 A PECOFF_FILE_ALIGNMENT
000000000052eb88 A __rela_size
0000000000990200 A __pecoff_data_rawsize
0000000000a30000 A __pecoff_data_size
0000000001630000 A __efistub_primary_entry_offset
000000000178a390 A __rela_offset
0000000002030200 A __efistub_kernel_size
00000000020d0000 A _kernel_size_le_lo32
ffffffc010000000 t __efistub__text
ffffffc010000000 t _head
ffffffc010000000 T _text
ffffffc010000040 t pe_header
ffffffc010000044 t coff_header
ffffffc010000058 t optional_header
ffffffc010000070 t extra_header_fields
ffffffc0100000f8 t section_table
ffffffc010010000 T __irqentry_text_start
ffffffc010010000 T _stext
ffffffc010010000 t efi_header_end
ffffffc010010000 t gic_handle_irq
ffffffc0100100d0 t gic_handle_irq
ffffffc0100103ec T __irqentry_text_end
ffffffc0100103f0 T __do_softirq
ffffffc0100103f0 T __softirqentry_text_start
ffffffc0100106f4 T __softirqentry_text_end
ffffffc0100106f8 T __entry_text_start
ffffffc010010800 T vectors
ffffffc010010fd8 t __bad_stack
ffffffc010011078 t el0_sync_invalid
ffffffc010011224 t el0_irq_invalid
ffffffc0100113d0 t el0_fiq_invalid
ffffffc01001157c t el0_error_invalid
ffffffc010011728 t el0_fiq_invalid_compat
ffffffc0100118d8 t el1_sync_invalid
ffffffc01001196c t el1_irq_invalid
ffffffc010011a00 t el1_fiq_invalid
ffffffc010011a94 t el1_error_invalid
ffffffc010011b40 t el1_sync
....
ffffffc0120c28b2 b __key.0
ffffffc0120c28b2 b __key.1
ffffffc0120c28b2 b __key.2
ffffffc0120c28b2 b __key.4
ffffffc0120c28b2 b __key.6
ffffffc0120c28b2 b __key.7
ffffffc0120c28b8 b rfkill_no.5
ffffffc0120c28c0 b g_rfkill
ffffffc0120c28c8 b country_cloc
ffffffc0120c28d4 b wifi_bt_vbat_state
ffffffc0120c28d8 b wifi_power_state
ffffffc0120c28dc B wifi_custom_mac_addr
ffffffc0120c28e2 b wifi_chip_type_string
ffffffc0120c2924 b power_set_time
ffffffc0120c2928 b __key.5
ffffffc0120c2928 b g_rfkill
ffffffc0120c2930 b bt_power_state
ffffffc0120c2938 b sleep_dir
ffffffc0120c2940 b empty.0
ffffffc0120c2980 b net_header
ffffffc0120c2988 B dns_resolver_debug
ffffffc0120c2990 B dns_resolver_cache
ffffffc0120c2998 b l3mdev_handlers
ffffffc0120c29a8 B __bss_stop
ffffffc0120c3000 B init_pg_dir
ffffffc0120c5000 B init_pg_end
ffffffc0120d0000 B _end
System.map中内核函数的范围是:ffffffc010000000 ~ ffffffc0120d0000
。而PC出错的位置是0xffffffc009034f28
。所以可以断定不是内核函数出错引起的,而是某个驱动模块引起的。
注意:如果把oops_module.ko直接编译进内核中,就是内核引起的错误了。PC出错时的地址也会刚好在System.map中。
接下来讲下调试驱动模块的三个工具: objdump
, addr2line
, decodecode
.
objdump工具分析
从上面我们Oops信息中告诉我们,错误是出在了oops_test模块的my_oops_init中
[ 173.331072] pc : my_oops_init+0x28/0x1000 [oops_test]
[ 173.331082] lr : my_oops_init+0x24/0x1000 [oops_test]
[ 173.331088] sp : ffffffc01391bb20
那如果oops没有打印出出错驱动的名字呢,该如何定位
其实我们可以使用cat /proc/kallsyms > kallsyms.txt
命令,在kallsyms.txt
中找出PC值接近的符合。
接下来,我们就要准备反汇编oops_test.o了,根据反汇编可以进一步确认出错的行数。
$ aarch64-linux-gnu-objdump -Sd oops_test.o
oops_test.o: file format elf64-littleaarch64
Disassembly of section .init.text:
0000000000000000 <init_module>:
0: d503245f bti c
4: d503201f nop
8: d503201f nop
c: d503233f paciasp
10: a9bf7bfd stp x29, x30, [sp, #-16]!
14: 90000000 adrp x0, 0 <init_module>
18: 910003fd mov x29, sp
1c: 91000000 add x0, x0, #0x0
20: 94000000 bl 0 <printk>
24: d2800000 mov x0, #0x0 // #0
28: b900001f str wzr, [x0]
2c: a8c17bfd ldp x29, x30, [sp], #16
30: d50323bf autiasp
34: d65f03c0 ret
Disassembly of section .exit.text:
0000000000000000 <cleanup_module>:
0: d503233f paciasp
4: a9bf7bfd stp x29, x30, [sp, #-16]!
8: 90000000 adrp x0, 0 <cleanup_module>
c: 910003fd mov x29, sp
10: 91000000 add x0, x0, #0x0
14: 94000000 bl 0 <printk>
18: a8c17bfd ldp x29, x30, [sp], #16
1c: d50323bf autiasp
20: d65f03c0 ret
或者在Makefile中添加如下语句,并重新编译内核模块
KBUILD_CFLAGS += -g
在用objdump
工具进行反汇编下出错模块oops_test.ko
$ aarch64-linux-gnu-objdump -Sd oops_test.ko
oops_test.o: file format elf64-littleaarch64
Disassembly of section .init.text:
0000000000000000 <init_module>:
0: d503245f bti c
4: d503201f nop
8: d503201f nop
c: d503233f paciasp
10: a9bf7bfd stp x29, x30, [sp, #-16]!
14: 90000000 adrp x0, 0 <init_module>
18: 910003fd mov x29, sp
1c: 91000000 add x0, x0, #0x0
20: 94000000 bl 0 <printk>
24: d2800000 mov x0, #0x0 // #0
28: b900001f str wzr, [x0]
2c: a8c17bfd ldp x29, x30, [sp], #16
30: d50323bf autiasp
34: d65f03c0 ret
Disassembly of section .exit.text:
0000000000000000 <cleanup_module>:
0: d503233f paciasp
4: a9bf7bfd stp x29, x30, [sp, #-16]!
8: 90000000 adrp x0, 0 <cleanup_module>
c: 910003fd mov x29, sp
10: 91000000 add x0, x0, #0x0
14: 94000000 bl 0 <printk>
18: a8c17bfd ldp x29, x30, [sp], #16
1c: d50323bf autiasp
20: d65f03c0 ret
通过反汇编工具objdump
可以看到oops_test模块里的init_module的汇编情况,第24~28字节的指令用于把0赋值给x0寄存器,然后往x0寄存器写入0,wzr是一种特殊寄存器,值为0,所以这里发生写空指针错误。
addr2line工具分析
Linux下addr2line
命令用于将程序指令地址转换为所对应的函数名、以及函数所在的源文件名和行号。当含有调试信息(-g)的执行程序出现crash时(core dumped),可使用addr2line
命令快速定位出错的位置。
如果无法确定文件名或函数名,addr2line
将在它们的位置打印两个问号;如果无法确定行号,addr2line
将打印0或一个问号。
参数说明:
-a:在函数名、文件名和行号信息之前,以十六进制形式显示地址。
-b:指定目标文件的格式为bfdname。
-C:将低级别的符号名解码为用户级别的名字。
-e:指定需要转换地址的可执行文件名,可以是.so库文件,可以是.o文件。
-f:在显示文件名、行号信息的同时显示函数名。
-s:仅显示每个文件名(the base of each file name)去除目录名。
-i:如果需要转换的地址是一个内联函数,则还将打印返回第一个非内联函数的信息。
-j:读取指定section的偏移而不是绝对地址。
-p:使打印更加人性化:每个地址(location)的信息都打印在一行上。
-r:启用或禁用递归量限制。
--help:打印帮助信息。
--version:打印版本号。
我们将addr2line
工具检测下出错模块oops_test.o
$ aarch64-linux-gnu-addr2line -e oops_test.o -p -f 0x28
create_oops at /home/forlinx/test/oops/oops_test.c:7
或者在Makefile中添加如下语句,并重新编译内核模块
KBUILD_CFLAGS += -g
在用addr2line
工具进行检测下出错模块oops_test.ko
$ aarch64-linux-gnu-addr2line -e oops_test.ko -p -f 0x28
create_oops at /home/forlinx/test/oops/oops_test.c:7
通过addr2line
工具可以分析到oops_test模块的出错位置位于create_oops函数里具体在第7行位置
如果oops发生在内核中,将oops_test.o换成对应的vmlinux即可。
decodecode工具分析
前两种方法适用于有源代码的情况下,这种方法适用于没有源代码或者符合表的场景下,将oops异常的log作为输入就可以解析出错误位置的汇编代码,工具位于linux/scripts/,里面有一个decodecode
工具可以用来转换机器码
准备工作
先导出环境变量,开发板的架构以及编译时用的交叉工具链。工具链用的是绝对路径
$ export ARCH=arm64
$ export CROSS_COMPILE=/home/forlinx/OK3588_Linux_fs/prebuilts/gcc/linux-x86/aarch64/gcc-arm-10.3-2021.07-x86_64-aarch64-none-linux-gnu/bin/aarch64-none-linux-gnu-
然后把出错的oops日志保存到一个.txt文件中。
然后执行内核目录下的scripts/decodecode工具脚本,具体命令如下: 执行脚本后,就可以得到出错的汇编代码。trapping instruction
指出了出错的地址。根据oops_test.ko的反汇编可以知道出错的位置10: b900001f str wzr, [x0]
。
总结
本篇我们学会了三种分析Oops错误的方法,当然还有gdb
,faddr2line
的方法这里就不分享了,学废了的话一键三连支持下!欢迎关注公众号[Linux随笔录],不定期分享Linux小知识
参考:
Documentation/oops-tracing.txt
https://www.cnblogs.com/dongxb/p/16846094.html