找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2685

积分

0

好友

357

主题
发表于 前天 04:00 | 查看: 26| 回复: 0

在之前的系列文章中,我们探讨了 printk 的原理与使用以及动态输出 dynamic debug 等内核调试手段。本篇我们将通过一个具体的实验,深入分享如何分析和定位 Linux 内核中最常见的运行时错误之一 —— Oops。

本篇环境

  • 硬件平台:飞凌 OK3588 开发板
  • 编译环境:Ubuntu 20.04 LTS
  • 编译工具链:aarch64-linux-gnu-

Oops 简介

对于嵌入式 Linux 底层开发人员来说,Oops 应该是一个比较常见的问题。它指的是 Linux 内核在发生不正确行为(例如访问非法地址)时产生的一份错误报告。这份报告包含了异常发生时的详细上下文信息:

  • 出错原因
  • CPU 寄存器状态
  • 出错的指令地址和数据地址
  • 函数调用栈回溯信息
  • 甚至栈内存内容

内核会根据异常的严重程度来决定下一步操作:杀死导致异常的进程,或者直接挂起系统(panic)。通常,发生在进程上下文(非中断上下文)中的 Oops 会导致该进程以一个信号退出;但如果 Oops 发生在中断上下文,就会直接引发系统 panic。如果内核配置了 panic_on_oops 参数,那么任何 Oops 都会导致系统宕机。

那么,当我们拿到一份 Oops 错误报告时,应该如何进行分析呢?接下来我们通过一个实例来详细讲解。

Oops 错误实例

首先,我们来看一个制造空指针解引用 Oops 的简单内核模块示例:

#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

将模块编译后加载到开发板上,会立即触发 Oops,关键日志如下:

[  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.336788] Code: 910003fd 91000000 95fee5c3 d2800000 (b900001f)
[  173.336793] ---[ end trace 0000000000000002 ]---

面对这样一大段看起来复杂的日志,我们该如何入手分析呢?

Oops 错误分析方法

前期日志分析

首先,我们逐段解读关键信息。

  1. 模块加载提示

    [  173.330566] oops_test: loading out-of-tree module taints kernel.

    这表示加载了一个“树外”(未在内核源码树内编译)的模块,这会使内核状态被“污染”。通常是因为内核启用了签名验证机制,而我们编译的模块没有签名。

  2. 错误类型

    Unable to handle kernel NULL pointer dereference at virtual address 0000000000000000

    直接指出了错误本质:内核空指针解引用。虚拟地址 0x0000000000000000 明确告诉我们,代码试图访问空指针指向的内存。

  3. 错误码与内核特性

    [  173.331047] Internal error: Oops: 96000045 [#1] PREEMPT_RT SMP

    96000045 是 ARM64 架构特定的错误码(ESR)。[#1] 表示此 Oops 信息是第一次显示。PREEMPT_RT SMP 表明当前内核配置了实时抢占并支持多核。

    Oops 错误码的通用定义(部分架构)可以参考内核文档,通常各位含义如下:

    • bit 0 == 0 表示页面未找到,1 表示保护错误。
    • bit 1 == 0 表示读操作,1 表示写操作。
    • bit 2 == 0 表示内核模式,1 表示用户模式。
    • bit 3 == 0 表示数据访问,1 表示指令获取。
  4. 加载的模块

    [  173.331052] Modules linked in: oops_test(O+)

    Modules linked in 列出了已加载的模块,这里只有 oops_test(O+) 中的 O 表示这是一个 Out-of-tree 模块。

  5. CPU、进程与污染状态

    [  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: 0:错误发生在 CPU 0 上。
    • PID: 1458 Comm: insmod:触发错误的进程是 insmod,进程ID为1458。
    • Tainted: G O:内核污染标志。G 表示所有模块都有GPL兼容许可证,O 表示加载了外部(Out-of-tree)模块。
    • 5.10.66-rt53 #21:内核版本信息。
    • Hardware name:硬件平台名称。
  6. 关键寄存器

    [  173.331072] pc : my_oops_init+0x28/0x1000 [oops_test]
    [  173.331082] lr : my_oops_init+0x24/0x1000 [oops_test]
    [  173.331088] sp : ffffffc01391bb20
    • pc (程序计数器):指向出错地址,位于 oops_test 模块的 my_oops_init 函数偏移 0x28 字节处。0x1000 是该函数的估计大小。
    • lr (链接寄存器):指向返回地址,位于同一函数偏移 0x24 字节处。
    • sp (栈指针):当前栈顶地址。
  7. 调用栈回溯

    [  173.331166] Call trace:
    [  173.331169]  my_oops_init+0x28/0x1000 [oops_test]
    [  173.331176]  do_one_initcall+0xb4/0x210
    ...

    这是最宝贵的调试信息之一,清晰地展示了从错误发生点开始的函数调用链,帮助我们理解代码的执行路径。

  8. 机器码

    [  173.336788] Code: 910003fd 91000000 95fee5c3 d2800000 (b900001f)

    这是出错位置附近的原始机器指令,对于没有源代码的情况尤为重要。

初步判断错误位置

解读完 Oops 日志,我们需要判断错误发生在内核函数还是驱动模块中。System.map 文件记录了内核所有符号的地址。我们查看内核编译生成的 System.map,找到内核函数的地址范围(例如 ffffffc010000000 ~ ffffffc0120d0000)。而我们的 PC 值 0xffffffc009034f28 显然不在这个范围内,因此可以断定是某个驱动模块(即我们的 oops_test)引起的错误。

注意:如果将模块直接编译进内核(built-in),那么错误就会被视为内核函数错误,PC 地址也会落在 System.map 的范围内。

既然确定是模块错误,并且 Oops 信息也给出了模块名和函数名,接下来的分析就更有针对性了。下面介绍三种定位出错代码行的实用工具。

objdump 工具分析

从 Oops 信息我们已知错误出在 oops_test 模块的 my_oops_init 函数。我们可以使用交叉编译工具链中的 objdump 工具反汇编该模块,进一步确认。

$ 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

在反汇编输出中,我们关注偏移量 0x28 对应的指令(与 Oops 中 pc : my_oops_init+0x28 对应):

  • 24: d2800000 mov x0, #0x0:将 0 赋值给 x0 寄存器。
  • 28: b900001f str wzr, [x0]:将零寄存器 wzr(其值恒为0)的值存储到 x0 寄存器所指向的内存地址。

这里 x0 的值为 0,所以 str wzr, [x0] 就是在向地址 0 写入 0,这正是导致“空指针解引用”的指令。

addr2line 工具分析

addr2line 是 GNU Binutils 中的工具,它能将地址转换为对应的文件名和行号,前提是二进制文件携带调试信息(-g 选项)。

我们可以用它来分析我们的模块。首先确保模块编译时添加了 -g 选项(例如在 Makefile 中添加 KBUILD_CFLAGS += -g)。然后执行:

$ aarch64-linux-gnu-addr2line -e oops_test.o -p -f 0x28
create_oops at /home/forlinx/test/oops/oops_test.c:7

或者直接分析带调试信息的 .ko 文件:

$ aarch64-linux-gnu-addr2line -e oops_test.ko -p -f 0x28
create_oops at /home/forlinx/test/oops/oops_test.c:7

输出清晰地告诉我们,地址 0x28 对应的是 create_oops 函数,位于源文件 /home/forlinx/test/oops/oops_test.c 的第 7 行。查看源代码,第7行正是 *(int *)0 = 0;

提示:如果 Oops 发生在内核自身(vmlinux),只需将 oops_test.ko 替换为 vmlinux 文件即可用相同方法分析。

decodecode 工具分析

前面两种方法都需要源代码。decodecode 工具则适用于没有源代码或符号表的情景。它位于内核源码的 scripts/ 目录下,可以直接解析 Oops 日志中的机器码。

使用方法

  1. 设置环境变量,指向正确的架构和工具链。
    $ 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-
  2. 将 Oops 日志中从 Code: 开始的那一行及上下文保存到一个文本文件(如 oops.txt)。
  3. 运行 decodecode 脚本。
    $ ./scripts/decodecode < oops.txt

decodecode工具解析Oops机器码结果

执行后,工具会输出反汇编结果,并明确标出导致陷阱的指令(trapping instruction)。从输出中可以看到出错的指令是 str wzr, [x0],这与我们之前用 objdump 分析的结果一致。

总结

本篇通过一个实际的空指针 Oops 案例,详细介绍了三种分析定位错误的方法:

  1. 日志分析法:仔细阅读 Oops 报告,获取错误类型、发生位置(模块、函数、偏移)、调用栈等关键信息。
  2. objdump 反汇编:直接反汇编目标文件,查看出错地址对应的汇编指令,理解底层操作。
  3. addr2line 定位源码行:在拥有调试信息的情况下,快速将地址映射到源代码文件和行号,效率最高。
  4. decodecode 解析机器码:在没有源码时,利用内核自带工具解析 Oops 中的机器码,获取汇编指令。

这三种方法各有适用场景,熟练掌握后能极大地提升内核问题调试效率。希望本文能为大家的内核开发与调试工作带来帮助。如果你有更多关于内核调试的技巧或心得,欢迎在云栈社区交流讨论。

一张表示文章结束的梗图




上一篇:Linux 内核崩溃怎么分析?从获取vmcore到crash调试全解析
下一篇:深度解析 Linux 中断机制:从硬件 GICv3 到内核实现与工作队列
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-5 18:45 , Processed in 0.550056 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表