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

4991

积分

0

好友

662

主题
发表于 4 天前 | 查看: 23| 回复: 0

最近在调试一台嵌入式设备时,遇到了一个偶发的开机死机问题。通过查看系统启动日志,发现内核报告了一个oops错误,具体信息如下(中间省略了部分重复或次要的堆栈信息,以......代替):

Unable to handle kernel NULL pointer dereference at virtual address 0000000c
pgd = cdd90000
[0000000c] *pgd=8df4d831, *pte=00000000, *ppte=00000000
Internal error: Oops: 17 [#1] SMP ARM
CPU: 0 PID: 206 Comm: mount Tainted: P           O   3.18.20 #4
task: ced40e40 ti: cdf7c000 task.ti: cdf7c000
PC is at exfat_fill_super+0xc8/0x4cc [exfat]
LR is at exfat_fill_super+0x48/0x4cc [exfat]
pc : [<bf64b670>]    lr : [<bf64b5f0>]    psr: a0080013
sp : cdf7de48  ip : ffffffff  fp : c0744a30
r10: 00000001  r9 : bf652dac  r8 : 00008000
r7 : cdf80000  r6 : cf302000  r5 : cdf85000  r4 : cdf41000
r3 : 00000000  r2 : cdf85104  r1 : 00000003  r0 : 000001b5
Flags: NzCv  IRQs on  FIQs on  Mode SVC_32  ISA ARM  Segment user
Control: 10c5387d  Table: 8dd9006a  DAC: 00000015

SP: 0xcdf7ddc8:
ddc8  cfa70880 fffffffc 0000000b cf17f800 cf4ea000 cf17f600 00000000 cfdee780
dde8  bf64b670 a0080013 ffffffff cdf7de34 00008000 c0012e18 000001b5 00000003
......
Process mount (pid: 206, stack limit = 0xcdf7c238)
Stack: (0xcdf7de48 to 0xcdf7e000)
de40:                   00000001 cdf41000 cdf7deb0 cf17f60c 00000001 00008000
de60: cdf41000 cdf7c038 c0744a30 c0264164 bf652db4 cdf7de84 3b9aca00 00000004
de80: cf4ea6c0 00000083 cf4ea734 cf302000 cf4ea6c0 00000083 00008000 cdf41000
......
dfc0: 01197040 01197040 be9fff49 00000015 be9fff31 00008000 00000000 00000000
dfe0: b6e3d2e0 be9ffaf8 0007ebec b6e3d2f0 60080010 be9fff49 00000000 00000000
[<bf64b670>] (exfat_fill_super [exfat]) from [<c00d12ec>] (mount_bdev+0x168/0x190)
[<c00d12ec>] (mount_bdev) from [<bf64b0ac>] (exfat_fs_mount+0x18/0x20 [exfat])
[<bf64b0ac>] (exfat_fs_mount [exfat]) from [<c00d1fd4>] (mount_fs+0x14/0xcc)
[<c00d1fd4>] (mount_fs) from [<c00ea480>] (vfs_kern_mount+0x4c/0x104)
[<c00ea480>] (vfs_kern_mount) from [<c00ed214>] (do_mount+0x194/0xb54)
[<c00ed214>] (do_mount) from [<c00edf1c>] (SyS_mount+0x74/0xa0)
[<c00edf1c>] (SyS_mount) from [<c000e4e0>] (ret_fast_syscall+0x0/0x38)
Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)

从日志第一行就能明确看出,内核试图访问一个空指针(NULL pointer dereference),地址是 0x0000000c。结合调用栈信息,问题发生在挂载(mount)exfat格式文件系统的过程中,具体函数是 exfat_fill_super

由于最近硬件环境有变动(更换了读卡器和更高速的存储卡),最初怀疑是硬件兼容性问题。但硬件变更如何精确导致代码级的空指针访问,逻辑上并不清晰。好在oops信息给出了非常关键的线索:程序计数器(PC)停在了 exfat_fill_super+0xc8/0x4cc。这就为我们指明了调查方向。

第一步:定位问题代码行

首先,在工程中搜索 exfat_fill_super 函数,确认它来自一个第三方开源的exfat文件系统驱动模块,该模块被编译为内核模块(.ko文件)在启动时加载。

接下来,我们需要知道 +0xc8 这个偏移具体对应哪一行C代码。最直接的方法是使用GDB进行反汇编定位。通过以下命令,我们找到了问题点:

(gdb) l *exfat_fill_super+0xc8
0x9670 is at ./exfat-nofuse-master/exfat_super.c:2301.

GDB告诉我们,问题发生在源文件 exfat_super.c 的第2301行。查看该行代码,内容如下:

2301        opts->fs_fmask = opts->fs_dmask = current->fs->umask;

这是一行连续的赋值语句。根据C语言规则,赋值从右向左进行,所以实际执行顺序是:

  1. 计算 current->fs->umask 的值。
  2. 将值赋给 opts->fs_dmask
  3. 再将同一个值赋给 opts->fs_fmask

因此,可能的空指针嫌疑点有三个:optscurrent->fs、以及计算出结果后赋值的目标 opts->fs_dmaskopts->fs_fmask。结合oops报出的地址 0x0000000c(一个很小的偏移),optscurrent 这类根指针为NULL的可能性更大。

第二步:层层递进,分析指针来源

我们逐一分析这些指针。

1. 分析 current
current 是一个宏,用于获取当前进程的 task_struct 结构体指针。在ARM平台上,它通过当前栈指针(SP)计算得来。oops日志中给出了 sp : cdf7de48ti: cdf7c000(thread_info地址),计算是合理的。日志也明确打印了 task: ced40e40,证明 current 宏获取到的任务结构体指针非空。所以 current 本身不是NULL。

2. 分析 current->fs
fstask_struct 结构体中一个指向 fs_struct 的指针,负责管理进程的文件系统信息(如根目录、当前工作目录等)。问题很可能出在这里:current 指针有效,但 current->fs 这个成员本身是 NULL

为什么一个进程的 fs 结构体会是空呢?这通常发生在内核线程或某些早期初始化阶段的进程,它们可能没有设置完整的文件系统上下文。而 mount 进程理论上应该有,但这提示我们可能遇到了状态不一致的情况。

第三步:通过汇编和偏移确认罪魁祸首

仅仅怀疑还不够,我们需要确凿证据。oops日志底部的 Code: 部分给出了发生异常时CPU执行的机器码:

Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)

括号 () 内的指令 e1d330bc 就是触发异常的那一条。反汇编目标ko文件,找到 exfat_fill_super 函数开头偏移 0xc8(即地址0x9670)附近的代码,可以清晰地看到:

9660:  e5851108   str    r1, [r5, #264] ; 0x108
9664:  e3a01003   mov    r1, #3
9668:  e593300c   ldr    r3, [r3, #12]
966c:  e5933308   ldr    r3, [r3, #776] ; 0x308
9670:  e1d330bc   ldrh   r3, [r3, #12]

关键来了:

  • 966c 行:ldr r3, [r3, #776]。这条指令从 r3+776 的地址加载数据到 r3。根据C代码逻辑,这对应着 current->fs 的操作,即从 task_struct 中取出 fs 成员。这里的 776fstask_struct 结构体中的偏移量。
  • 9670 行:ldrh r3, [r3, #12]。这条指令从 r3+12 的地址加载半字数据到 r3。这对应着 fs->umask 的操作。此时如果 r3(即 fs 指针)为 NULL,那么访问 NULL+12 就会触发空指针异常,地址正好是 0x0000000c,与oops日志完全吻合。

至此,我们确认了:current->fs 指针为 NULL,导致访问 fs->umask 时崩溃。

第四步:探寻根本原因:偏移量为何“飘移”?

问题似乎找到了,但更深层的问题来了:mount 进程的 fs 指针为何会是 NULL?这不太符合常理。一个更诡异的线索出现了:上面汇编中 fs 的偏移量是 776 (0x308)。

为了调试,我在代码中打印了几个关键结构体成员的偏移量,得到:fs_offset=904。这与汇编里的 776 对不上!重新编译模块后查看新生成的汇编,发现 fs 的偏移量确实变成了 904 (0x388)。

同一个内核版本,同一个结构体,成员偏移量怎么会变?

答案指向一个经典问题:内核模块与内核版本不匹配。具体来说:

  1. exfat.ko 内核模块是基于某个版本的内核源代码编译的,当时 task_struct 结构体中 fs 成员的偏移量是 776
  2. 后来,内核的配置文件(.config)发生了变更,例如启用了新的内核配置选项(如 CONFIG_PERF_EVENTS)。这些选项可能在 task_struct 中增加了新的成员,而新增成员的位置在 fs 成员之前,导致 fs 的实际偏移量向后移动,变成了 904
  3. 系统升级时,只更新了内核镜像(zImage),却没有重新编译 exfat.ko 模块。导致系统运行时,旧的模块(认为 fs776 偏移处)去访问新内核(fs 实际在 904 偏移处)的数据。
  4. 从错误的偏移 776 读出来的数据是一个随机值(很可能就是 0),被当成 fs 指针,从而在后续访问时引发空指针异常。

第五步:验证与解决

为了验证这个猜想,我检查了内核的配置历史,发现确实后来为使能性能监控而打开了 CONFIG_PERF_EVENTS 选项。关闭此选项,保持与编译exfat模块时完全相同的配置,重新编译内核和模块,再次反汇编,fs 的偏移量果然变回了 776

解决方案 非常明确:确保所有内核模块与当前运行的内核是严格匹配的。在更新内核或内核配置后,必须重新编译所有依赖的内核模块。在嵌入式开发中,这意味着一套完整的固件(包含内核和所有模块)应该作为一个整体进行版本管理。

经验总结

  1. oops是朋友:内核oops信息包含大量调试宝藏,如PC位置、堆栈、寄存器值和指令码,是定位问题的起点。
  2. 偏移量是关键线索:在内核模块相关故障中,结构体成员偏移量的异常往往是版本不匹配的“烟雾弹”。
  3. 模块与内核需同步:无论是桌面Linux还是嵌入式系统,在更新内核后,必须考虑对已加载内核模块的影响。它们是一个整体,版本必须同步。
  4. 调试需要耐心与推理:从表象(死机)到日志(oops),再到代码(C和汇编),最后结合系统状态(内核配置)进行推理,是解决复杂内核问题的标准路径。这种抽丝剥茧的分析过程,本身也是对系统理解的深化。

这次排查经历,始于一个模糊的硬件怀疑,终结于一个清晰的软件配置问题,再次体现了系统软件调试中保持严谨逻辑的重要性。




上一篇:MySQL性能优化实战:30个技巧搞定索引、慢查询与分库分表
下一篇:Linux 内核崩溃怎么分析?从获取vmcore到crash调试全解析
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-4-7 18:37 , Processed in 1.562640 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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