电子产业一站式赋能平台

PCB联盟网

搜索
查看: 29|回复: 0
收起左侧

Linux内核调试 | 分析Oops错误

[复制链接]

434

主题

434

帖子

5935

积分

四级会员

Rank: 4

积分
5935
发表于 2025-6-2 10:43:00 | 显示全部楼层 |阅读模式
前言
  • 本篇环境
  • Oops的简介
  • Oops错误的实例
  • Oops错误的分析
  • 前期日志的分析
  • objdump工具分析
  • addr2line工具分析
  • decodecode工具分析
  • 总结
    前言本篇以做实验为目的来给大家分享下如何分析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错误的实例看一个Oops的空指针的demo:
    #include
    #include
    #include
    static void create_oops(void)
    {
    *(int *)0 = 0;
    }
    static int __init my_oops_init(void)
    {
    printk("oops module init
    ");
    create_oops();
    return 0;
    }
    static void __exit my_oops_exit(void)
    {
    printk("oops module exit
    ");
    }
    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 :
       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
      18: 910003fd  mov x29, sp
      1c: 91000000  add x0, x0, #0x0
      20: 94000000  bl 0
      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 :
       0: d503233f  paciasp
       4: a9bf7bfd  stp x29, x30, [sp, #-16]!
       8: 90000000  adrp x0, 0
       c: 910003fd  mov x29, sp
      10: 91000000  add x0, x0, #0x0
      14: 94000000  bl 0
      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工具脚本,具体命令如下:

    im32bmj5toi64082376510.png

    im32bmj5toi64082376510.png

    执行脚本后,就可以得到出错的汇编代码。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

    end

    一口Linux

    关注,回复【1024】海量Linux资料赠送
    精彩文章合集
    文章推荐
    ?【专辑】ARM?【专辑】粉丝问答?【专辑】所有原创?【专辑】linux入门?【专辑】计算机网络?【专辑】Linux驱动?【干货】嵌入式驱动工程师学习路线?【干货】Linux嵌入式所有知识点-思维导图
  • 回复

    使用道具 举报

    发表回复

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    关闭

    站长推荐上一条 /1 下一条


    联系客服 关注微信 下载APP 返回顶部 返回列表