Linux内核调试 | 分析Oops错误

前言

前几篇我们讲了下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 TaintedTainted的敏感字符可以从内核中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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值