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

4233

积分

0

好友

586

主题
发表于 5 小时前 | 查看: 3| 回复: 0

当Linux内核遇到一些非致命的错误时,它不会让系统立即崩溃,而是会产生一个称为“Oops”的诊断信息。这对于内核开发者,特别是驱动开发者来说,是定位问题的关键线索。面对屏幕上滚过的一大段令人困惑的十六进制和符号,该如何入手呢?本文将带你一步步拆解一个真实的Oops日志,并介绍多种定位问题根因的实用方法。

OOPS信息解读

当我们尝试加载一个有问题的内核模块时,系统输出了以下Oops信息。我们将逐段分析其含义。

root@firefly:~/mnt/module# insmod oops_module.ko
[  867.140514] Unable to handle kernel NULL pointer dereference at virtual address 00000000

第一行直接指出了问题的核心:内核无法处理一个对虚拟地址 0x00000000 的空指针解引用异常。如果错误是由代码直接调用 BUG()BUG_ON() 触发的,这里可能还会显示源代码中的行号。

[  867.141279] pgd = ffffffc0f0a65000
[  867.141582] [00000000] *pgd=0000000000000000, *pud=0000000000000000

这两行显示了试图访问的地址(本例中为0)的页表信息(PGD、PUD)。这里全是0,表明这个地址没有对应的有效页表项。

[  867.142164] Internal error: Oops: 96000045 [#1] SMP

96000045 表示错误码。方括号 [] 内的数字 #1 表示这是该Oops信息第一次被显示。后面的 SMP 表明当前内核启用了SMP(对称多处理)支持。

注:96000045 这种错误码并不常见,通常见到的是 001002 等形式。Oops的错误代码根据错误原因有不同的定义,如果发现与常见代码不符,最好去内核源代码中查找对应架构的定义。

[  867.142592] Modules linked in: oops_module(O+)

Modules linked in 列出了系统当前已加载的模块列表,(O+) 表明 oops_module 是一个外部模块(Out-of-tree module)。

[  867.143006] CPU: 4 PID: 1163 Comm: insmod Tainted: G           O    4.4.194+ #7
[  867.143649] Hardware name: Firefly-RK3399 Board (Linux Opensource) (DT)
[  867.144236] task: ffffffc0cdc44380 task.stack: ffffffc00a4fc000
  • CPU: 4:错误发生在4号逻辑CPU上。
  • PID: 1163:触发Oops的进程ID是1163。
  • Comm: insmod:进程名称是 insmod,正是我们执行加载模块的命令。
  • Tainted: G O:内核污染标志。
    • G:所有已加载模块都拥有GPL或兼容许可证。
    • O:系统已加载外部(非主线)模块。
  • 4.4.194+:内核版本号。
  • Hardware name:硬件平台名称。
  • tasktask.stack:分别表示当前进程控制块(task struct)和进程内核栈的起始地址。

内核污染原因(Tainted)的详细说明可以在内核源码 kernel/panic.c 中找到,常见标志如下:

Tainted 描述
G 所有模块均有 GPL 或兼容许可证。
P 加载了私有(Proprietary)模块。
F 模块被 insmod -f 强制加载。
S SMP 内核运行在未经认证为 SMP 安全的硬件上。
R 模块被 rmmod -f 强制卸载。
M 处理器报告了机器检查异常(Machine Check Exception)。
B 页释放函数发现了坏的页引用或意外的页标志。
U 用户或用户应用明确请求设置污染标志。
[  867.144761] PC is at init_oopsdemo+0x24/0x38 [oops_module]
[  867.145247] LR is at init_oopsdemo+0x18/0x38 [oops_module]
[  867.145732] pc : [<ffffff8000ef0024>] lr : [<ffffff8000ef0018>] pstate: 40000145
[  867.146386] sp : ffffffc00a4ffc40

这是最关键的信息之一:

  • PC is at init_oopsdemo+0x24/0.38:程序计数器(PC)指向了模块 oops_module 中的函数 init_oopsdemo+0x24 偏移处。0x38 是该函数的总大小(56字节)。这意味着错误发生在这个函数的第36字节(0x24)的位置。
  • 下面几行是PC、LR(链接寄存器)、SP(栈指针)寄存器在异常发生时的具体值。

接下来的大段内容是异常发生时所有通用寄存器的值(X0-X30)以及围绕PC、LR、SP地址的内存转储。这些信息在深入分析指令流和内存状态时非常有用,但对于初步定位问题,我们更关注上面的函数调用信息。

[  867.148820] Process insmod (pid: 1163, stack limit = 0xffffffc00a4fc000)
[  867.148822] Stack: (0xffffffc00a4ffc40 to 0xffffffc00a500000)

stack limit 显示了内核栈的大小(由内核配置选项决定)。Stack 后面是从栈指针开始的部分栈内存内容。

[  867.148907] Call trace:
[  867.148911] Exception stack(0xffffffc00a4ffa70 to 0xffffffc00a4ffba0)
...
[  867.148944] [<ffffff8000ef0024>] init_oopsdemo+0x24/0x38 [oops_module]
[  867.148953] [<ffffff80080830f8>] do_one_initcall+0x78/0x194
[  867.148958] [<ffffff800818d2d0>] do_init_module+0x64/0x1c0
[  867.148962] [<ffffff800813ab5c>] load_module+0x199c/0x1ed0
[  867.148964] [<ffffff800813b2b4>] SyS_finit_module+0xb0/0xbc
[  867.148968] [<ffffff8008082f70>] el0_svc_naked+0x24/0x28

栈回溯(Call trace)信息,这是另一个关键部分。它清晰地展示了从系统调用开始到崩溃点的函数调用链:
el0_svc_naked -> SyS_finit_module -> load_module -> do_init_module -> do_one_initcall -> init_oopsdemo
这证实了错误发生在模块初始化函数 init_oopsdemo 中。

[  867.148972] Code: 95ca7426 d2800000 528102e1 72a32ec1 (b9000001)
[  867.148975] ---[ end trace 1983a52768236533 ]---

Code 部分是错误发生时PC指向地址附近的一小段机器码(指令),括号 () 内的是触发异常的那条指令。最后一行表示Oops信息结束。

如何根据OOPS找出Bug

第一步:确定出错位置在内核函数还是驱动模块

首先,我们需要判断Oops发生在内核核心代码还是某个驱动模块中。System.map 文件记录了内核编译后所有符号(函数、变量)的地址。它通常位于内核编译输出目录的根目录。

查看 System.map 文件头尾,可以确定内核函数的地址范围。例如:

ffffff8008080000 T _head
...
ffffff8009880000 B _end

内核函数的地址范围大致是 ffffff8008080000ffffff8009880000。而我们的Oops中PC值是 ffffff8000ef0024,明显不在这个范围内。因此,可以判定不是内核核心函数出错,而是某个驱动模块的问题。

提示:如果把 oops_module.ko 直接编译进内核(而不是作为模块),那么错误就会被认为是内核引起的,PC地址也会落在 System.map 的范围内。

第二步:反汇编驱动文件

Oops信息已经告诉我们错误出在 oops_module 模块的 init_oopsdemo+0x24。如果Oops没有打印模块名,我们可以通过 cat /proc/kallsyms > kallsyms.txt 命令获取系统所有符号地址,然后在其中查找与PC值(ffffff8000ef0024)最接近的符号,从而确定出错的模块。

接下来,反汇编有问题的模块文件:

aarch64-linux-gnu-objdump -D oops_module.ko > oops_module.dis

在反汇编文件 oops_module.dis 中,我们找到 init_oopsdemo 函数:

0000000000000000 <init_module>:
   0:   a9bf7bfd        stp     x29, x30, [sp, #-16]!
   4:   910003fd        mov     x29, sp
   8:   aa1e03e0        mov     x0, x30
   c:   94000000        bl      0 <_mcount>
  10:   58000100        ldr     x0, 30 <init_module+0x30>
  14:   94000000        bl      0 <printk>
  18:   d2800000        mov     x0, #0x0                    // #0
  1c:   528102e1        mov     w1, #0x817                  // #2071
  20:   72a32ec1        movk    w1, #0x1976, lsl #16
  24:   b9000001        str     w1, [x0]        # 将w1寄存器的值存入x0指向的内存
  28:   a8c17bfd        ldp     x29, x30, [sp], #16
  2c:   d65f03c0        ret

根据 PC is at init_oopsdemo+0x24,我们定位到 0x24 偏移处的指令:str w1, [x0]。这是一条存储指令,意图将寄存器 w1 的值写入 x0 寄存器所指向的内存地址。

再看上一条指令 0x18: d2800000 mov x0, #0x0,它将 x0 寄存器设置为 0。于是真相大白:代码试图向内存地址 0 写入数据,导致了空指针解引用。这完美解释了Oops第一行的“NULL pointer dereference at virtual address 00000000”。

其他辅助定位方法

1. 使用GDB

如果你的内核模块编译时包含了调试信息(-g 选项),可以直接用GDB定位。

aarch64-linux-gnu-gdb -q ./oops_module.ko
(gdb) list *init_oopsdemo+0x24
0x4c is in init_oopsdemo (/home/zhongyi/code/module/oops_module/oops_module.c:10).

GDB会直接告诉你,init_oopsdemo+0x24 对应源文件 oops_module.c 的第10行。

2. 使用addr2line

addr2line 工具可以直接将地址转换为文件名和行号,同样需要调试信息。

aarch64-linux-gnu-addr2line -e ./oops_module.ko -p -f 0x24
init_oopsdemo at /home/zhongyi/code/module/oops_module/oops_module.c:10

3. 使用内核脚本 decodecode

Linux内核源码提供了一个 scripts/decodecode 脚本,即使没有源代码或符号表,也能将Oops中的机器码反汇编。你需要将Oops日志中 Code: 开始的部分保存到文件(例如 oops_log.txt),然后运行:

ARCH=arm64 CROSS_COMPILE=your_toolchain_prefix ./scripts/decodecode < oops_log.txt

执行后,脚本会输出类似的结果,并指出触发异常的指令(trapping instruction):

Code starting with the faulting instruction
===========================================
   0:   b9000001        .word   0xb9000001

结合模块的反汇编,就能定位到出错的汇编指令。

4. 使用内核脚本 faddr2line

这是内核开发者常用的一个脚本,位于 scripts/faddr2line。它可以根据“函数名+偏移量”直接输出源码位置。

scripts/faddr2line /home/zhongyi/code/module/oops_module/oops_module.ko init_oopsdemo+0x24
init_oopsdemo+0x24/0x30:
init_oopsdemo at /home/zhongyi/code/module/oops_module/oops_module.c:18

注意:如果要分析内核本身的Oops,需要将第一个参数替换为包含调试信息的 vmlinux 文件。

问题根源

最后,让我们看看导致这个Oops的源代码 oops_module.c

#include <linux/init.h>
#include <linux/module.h>

MODULE_LICENSE("BSD/GPL");
MODULE_AUTHOR("ZHONGYI");

static  int init_oopsdemo(void)
{
    printk("oops module init! \n");
    *((int*)0x00) = 0x19760817; // 第10行:向地址0写入数据
    return 0;
}

module_init(init_oopsdemo);

static  void cleanup_oopsdemo(void)
{
    printk("oops module exit! \n");
}

module_exit(cleanup_oopsdemo);

问题一目了然:在模块初始化函数中,第10行代码 *((int*)0x00) = 0x19760817; 试图向空地址写入一个整数值,这直接导致了内核的空指针解引用Oops。

通过以上步骤,我们完成了一次完整的内核Oops分析。从解读晦涩的日志,到使用各种工具定位源代码中的Bug行,系统调试 的过程虽然复杂,但非常有章可循。掌握这些方法,将能极大地帮助你在进行 内核 模块或驱动开发时,高效地诊断和解决问题。




上一篇:博通CEO直言CPO非必须:400G SerDes时代铜缆互连仍是AI数据中心最优选
下一篇:高架行车记录仪曝光:理想车主故意别车致比亚迪海鸥与小米SU7失控碰撞
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-3-10 11:19 , Processed in 0.569754 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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