阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

一个内核 Oops 问题的分析及解决

298次阅读
没有评论

共计 7812 个字符,预计需要花费 20 分钟才能阅读完成。

说了这么多,那么到底是不是呢,验证一下就知道了。关闭上述选项,重新编译内核,之后再编译 exfat,查看汇编,发现偏移回到了 776。Yes,问题就是这里了。

最近在调试设备时,遇到了一个偶发的开机死机问题。通过查看输出日志,发现内核报告了 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 : []    lr : []    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
[] (exfat_fill_super [exfat]) from [] (mount_bdev+0x168/0x190)
[] (mount_bdev) from [] (exfat_fs_mount+0x18/0x20 [exfat])
[] (exfat_fs_mount [exfat]) from [] (mount_fs+0x14/0xcc)
[] (mount_fs) from [] (vfs_kern_mount+0x4c/0x104)
[] (vfs_kern_mount) from [] (do_mount+0x194/0xb54)
[] (do_mount) from [] (SyS_mount+0x74/0xa0)
[] (SyS_mount) from [] (ret_fast_syscall+0x0/0x38)Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)

从上述日志信息中,初步可以看出,在挂载 exfat 格式文件系统的存储卡时,内核出现了空指针访问问题,最终导致内核奔溃并输出 oops。因为之前没有遇到过这个问题,且最近硬件更换了读卡器,存储卡也更新换代了,从之前的 100MB/ s 换到了 120MB/s,所以,最初怀疑问题可能是因为更换读卡器或 (和) 存储卡导致的。但是,硬件和卡的变更到底是如何影响并导致上述 oops 错误的,这其中的细节并不清楚。好在堆栈信息比较明确,异常时,PC 指针指向了这个位置:exfat_fill_super+0xc8/0x4cc (PC is at exfat_fill_super+0xc8/0x4cc [exfat])。那我们就顺藤摸瓜,看看这个位置对应的代码是什么。

首先,在工程中搜索 exfat_fill_super 这个函数,了解其位置和关联模块。一番操作下来,发现这个函数在第三方开源库 exfat 中。这个库提供了 exfat 文件系统挂载的支持,并被编译为 ko 库文件,在系统启动时 insmod 到系统中。

其次,我们看看问题日志中,PC 指针指向的代码具体是哪一行? 因为日志中只提示在 exfat_fill_super 这个函数的 0xc8 偏移处,为了准确找到这个位置,我们需要借助 gdb,如下所示:

(gdb) l exfat_fill_super
                    sb->s_d_op = &exfat_dentry_ops;
    }
   #endif
    static int exfat_fill_super(struct super_block *sb, void *data, int silent)
    {
            struct inode *root_inode = NULL;
            struct exfat_sb_info *sbi;
            int debug, ret;
           long error;
(gdb) l *exfat_fill_super+0xc8
 0x9670 is at ./exfat-nofuse-master/exfat_super.c:2301.
            int option;
            char *iocharset;

            opts->fs_uid = current_uid();
            opts->fs_gid = current_gid();
            opts->fs_fmask = opts->fs_dmask = current->fs->umask;
            opts->allow_utime = (unsigned short) -1;
            opts->codepage = exfat_default_codepage;
            opts->iocharset = exfat_default_iocharset;
            opts->casesensitive = 0;

可以看到,gdb 告诉我们,0xc8 偏移在 2301 这一行(也告诉我们对应的汇编在 0x9670 处,后面会用到):

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

但是,比较烦人的是,这行代码是连续赋值,并且都使用到了指针,所以并不能一下就确定问题到底在那一个赋值上产生。不过,不着急,我们先看看这行代码做了什么。按照 C 语言的规则,连续赋值是从右到左执行,所以先执行的应该是:

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

执行这行代码时,需要先确定 current->fs,再确定 fs->umask,最后,将结果给 opts->fs_dmask。所以,就这一处赋值而言,就有三个可能的疑点。

先看第一个 current->fs。这里 current 是一个宏,用于获取当前线程的任务结构体(这里又隐藏一个指针)。

#define get_current() (current_thread_info()->task)
#define current get_current()

对于当前 arm 平台,线程信息是通过堆栈寄存器获取的。

static inline struct thread_info *current_thread_info(void)
{register unsigned long sp asm ("sp");
   return (struct thread_info *)(sp & ~(THREAD_SIZE - 1));
}

从上面代码,进一步的得知,线程信息是堆栈寄存器通过位运算获得的。这里的 THREAD_SIZE 定义如下:

#define THREAD_SIZE_ORDER   1
#define THREAD_SIZE       (PAGE_SIZE 

这是一个跟页面大小相关的量。在当前系统中,PAGE_SIZE 为 4KB 大小,所以 THREAD_SIZE 为 8KB 大小,也即 0x2000,一共 14 位。减去 1,就是 1FFFF,取反就是 0b’0000(第一个 0 占 1bit,其余为 4bit),然后参与“与”运算。这一连串的运算,总结为一句话,就是将给定的栈指针地址的低 13 位与 0 进行与运算,即将栈指针低 13 位清零。

这就是说内核线程结构体是在当前栈 8KB 对齐的低地址处。这是内核在设计时故意安排的,可以提高查找效率。我们来看这个指针获取是否存在空指针访问的问题:

current_thread_info()->task

回到最开始的日志中,部分信息如下:

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 : []    lr : []    psr: a0080013
sp : cdf7de48  ip : ffffffff  fp : c0744a30

其中,sp 在 cdf7de48,所以 thread_info 的位置应该是 cdf7c000,从上面的日志中也可以看到 ti 是 cdf7c000,所以这个位置不会是空指针的位置。

这里的 task 是 thread_info 结构体的一个子域,如下:

struct thread_info {
   unsigned long     flags;    /* low level flags */
   int          preempt_count; /* 0 => preemptable, <0 => bug */
   mm_segment_t      addr_limit;    /* address limit */
   struct task_struct *task;    /* main task structure */
   struct exec_domain *exec_domain;  /* execution domain */

那么,task 有没有可能是一个空指针呢? 上面 oosp 的日志也给出了,task: ced40e40,所以,task 也不为空。

这样,current 就指代了这里的 task,一个不为空的地址。所以我们再看 current->fs。

这里的 fs 是 task_struct 结构体的一个子域 struct fs_struct *fs;(部分字段省略)。

struct task_struct {
   volatile long state;   /* -1 unrunnable, 0 runnable, >0 stopped */
   void *stack;
   atomic_t usage;
   unsigned int flags;    /* per process flags, defined below */
   unsigned int ptrace;
   ......
/* CPU-specific state of this task */
   struct thread_struct thread;
/* filesystem information */
   struct fs_struct *fs;
/* open file information */
   struct files_struct *files;
/* namespaces */
   struct nsproxy *nsproxy;
   ......
#ifdef CONFIG_PERF_EVENTS
   struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
   struct mutex perf_event_mutex;
   struct list_head perf_event_list;
#endif
#ifdef CONFIG_DEBUG_PREEMPT
   unsigned long preempt_disable_ip;
#endif
   ......
};

从上面的定义,可以看到,它是跟文件系统相关的一个结构体。分析到这里时,考虑到问题所在函数为 exfat_fill_super,看名字似乎是填充文件系统超级快的操作,加之测试部门反馈,问题出现后,格式化存储卡就会恢复,所以我怀疑,会不会是因为更换读卡器和存储卡,导致读取超级块信息有误,才使得文件系统相关访问出现空指针,并报告 oops。

为了验证这一想法,我将上述连续赋值的这行代码 (即前述问题所在的 2301 行代码) 进行拆分,分为多条语句,然后在每一个指针使用点添加日志,以便在问题出现时,输出问题到底在哪个指针上。另外,为了尽可能保留环境,在问题出现后,采取软重启设备,并通过重新配置 uboot 参数,让内核通过 nfs 挂载根文件系统,这样就可以替换之前的 ko 库文件来测试了。

奇怪的是,每次替换后,问题就不出现了。这一现象似乎打破了之前的猜测,感觉问题又偏向软件一侧了。在这种取巧的打印方案没有取得效果后,我决定直接分析汇编代码,看看问题出现时,空指针到底落在了哪里。反汇编目标文件,结合 gdb 报告的位置 (前面已提到) 和 oops 中报告的指令内容。

Code: e5851108 e3a01003 e593300c e5933308 (e1d330bc)

确定问题就在下面汇编中 9670 这一行:

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]

9674:  e1c2c0bc   strh   ip, [r2, #12]
9678:  e1c200be   strh   r0, [r2, #14]
967c:  e1c230ba   strh   r3, [r2, #10]
9680:  e1c230b8   strh   r3, [r2, #8]

这是一条加载指令,即将 r3 寄存器指示的内存地址,偏移 12 位置后的两个字节,加载到 r3 寄存器中。这里 r3 指示的内存地址是什么呢? 根据 oops 中给出的信息,是 00000000,加上 12,就是地址 0000000C,所以 oops 报告。

Unable to handle kernel NULL pointer dereference at virtual address 0000000c

结合 C 代码及问题点前后的汇编代码,直观感觉,这里的 12 应该是一个结构体中某一个子域的偏移,找到这个偏移对应的域,那么就可以确定是在哪一个赋值上出现了空指针。

回到 C 代码,问题代码行前后有好几个结构体使用,为了快速确定偏移,我选择参考内核 container_of 宏,定义一个找偏移的宏。

#define my_offsetof(TYPE, MEMBER)  ((size_t)&((TYPE *)0)->MEMBER)

通过这个宏,快速找到每一个元素在结构体中的偏移。当然,也可以通过看代码来确定,但是没有这种方法来得快。就是通过这个操作,引出了问题的最终原因。我们继续。

添加获取偏移的日志后,得到的相关偏移信息如下:

task_offset=12, fs_offset=904, umask_offset=12, fs_fmask=8, fs_dmask=10

这里的 12、904、12、、8、10 似乎跟汇编有隐隐的对应关系。但是这里的 904 跟 776 没有什么关系。我决定再看看添加日志后目标文件的反汇编代码,如下:

97b8:  e3a0b000   mov    fp, #0
97bc:  e3a0207b   mov    r2, #123   ; 0x7b
97c0:  e3000000   movw   r0, #0
97c4:  e300a000   movw   sl, #0
97c8:  e5933388   ldr    r3, [r3, #904] ; 0x388
97cc:  e3400000   movt   r0, #0
97d0:  e340a000   movt   sl, #0
97d4:  e1d330bc   ldrh   r3, [r3, #12]
97d8:  e1c930ba   strh   r3, [r9, #10]
97dc:  e1c930b8   strh   r3, [r9, #8]
97e0:  e5cb2000   strb   r2, [fp]
97e4:  e595300c   ldr    r3, [r5, #12]

因为此时代码被修改,所以只能大概判断之前问题所在的汇编范围。从上面可以看出,这一次汇编里的数值跟打印出来的偏移对应上了。根据这次的偏移,结合汇编,基本可以确定,之前出问题的汇编对应的就是 C 代码中的 fs->umask 这个语句。

因为 fs 为空,所以再去获取 umask,就会报空指针异常。那问题来了,为啥 fs 会变空呢? 有经验的读者,此时可能已经猜出问题的原因了。

我们看到,之前代码反汇编后,fs 的偏移是 776,添加日志重新编译后,反汇编成了 904。虽然添加日志,导致代码被修改,但是并不影响这个偏移,所以,这里的 fs 偏移可能就是问题所在。对于偏移变化,我考虑了三个因素,分别进行了验证:

1. 是 ko 库文件因为 flash 坏块或其他原因,导致二进制文件部分 bit 翻转。实际验证后,排除了这个原因。

2. 是 ko 库针对不同平台编译的,放置错误导致。实际验证后,这个原因也排除了。

3. 是当前添加日志后所编译 ko 库,其依赖的内核配置跟之前编译 ko 库依赖的内核配置相比有更新,也就是内核配置发生了变化(内核版本本身是一致的)。这种情况最常见的就是对内核进行了 menuconfig 操作。检查 fs 所在的 task_struct 结构体,发现其中有很多 ifdef,不过都不曾配置过,倒是有一个 perf 相关的 CONFIG_PERF_EVENTS,由于调测性能所需,是后来新配置的。但是这个配置选项在 fs 结构体后面(见前面 task_struct 结构体),按理说是不影响 fs 在整个结构体中偏移的。考虑到 task_struct 结构体里面包含了很多子结构体,不排除上述 perf 配置影响了 fs 前面的某些子结构体而导致 fs 自己的偏移发生变化。

说了这么多,那么到底是不是呢,验证一下就知道了。关闭上述选项,重新编译内核,之后再编译 exfat,查看汇编,发现偏移回到了 776。Yes,问题就是这里了。最终原因就是内核更新了,但是 ko 没有更新,导致二者不匹配(旧的 ko 库从 776 偏移找 fs,但是在新内核中,fs 的偏移已经成了 904),产生了潜在的问题。

问题原因最终是找到了,但是问题产生的过程,其实更值得引起注意:ko 库因为也是在内核空间运行,所以需要跟 kernel 版本匹配起来,做版本一致管理。进一步的,不仅仅是嵌入式领域,桌面端也同样的,如果系统中加载了 ko 库,当更新 kernel 时,就需要考虑对 ko 库的影响。二者需要统一起来看待和管理。

阿里云 2 核 2G 服务器 3M 带宽 61 元 1 年,有高配

腾讯云新客低至 82 元 / 年,老客户 99 元 / 年

代金券:在阿里云专用满减优惠券

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2024-07-25发表,共计7812字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19351
评论数
4
阅读量
7986063
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
亚马逊云崩完,微软云崩!当全球第二大云“摔了一跤”:Azure 宕机背后的配置风险与警示

亚马逊云崩完,微软云崩!当全球第二大云“摔了一跤”:Azure 宕机背后的配置风险与警示

亚马逊云崩完,微软云崩!当全球第二大云“摔了一跤”:Azure 宕机背后的配置风险与警示 首先来回顾一下 10...
手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板 前言 大家好,我是星哥。星哥发现很多新手刚接触服务器时,都会被“选购...
升级自动部署更新SSL证书系统、申请godaddy的APIKEY

升级自动部署更新SSL证书系统、申请godaddy的APIKEY

升级自动部署更新 SSL 证书系统、申请 godaddy 的 APIKEY 公司之前花钱购买的 ssl 证书快...
告别Notion焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁”

告别Notion焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁”

  告别 Notion 焦虑!这款全平台开源加密笔记神器,让你的隐私真正“上锁” 引言 在数字笔记工...
自己手撸一个AI智能体—跟创业大佬对话

自己手撸一个AI智能体—跟创业大佬对话

自己手撸一个 AI 智能体 — 跟创业大佬对话 前言 智能体(Agent)已经成为创业者和技术人绕...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
150元打造低成本NAS小钢炮,捡一块3865U工控板

150元打造低成本NAS小钢炮,捡一块3865U工控板

150 元打造低成本 NAS 小钢炮,捡一块 3865U 工控板 一块二手的熊猫 B3 工控板 3865U,搭...
安装并使用谷歌AI编程工具Antigravity(亲测有效)

安装并使用谷歌AI编程工具Antigravity(亲测有效)

  安装并使用谷歌 AI 编程工具 Antigravity(亲测有效) 引言 Antigravity...
星哥带你玩飞牛NAS硬件02:某鱼6张左右就可拿下5盘位的飞牛圣体NAS

星哥带你玩飞牛NAS硬件02:某鱼6张左右就可拿下5盘位的飞牛圣体NAS

星哥带你玩飞牛 NAS 硬件 02:某鱼 6 张左右就可拿下 5 盘位的飞牛圣体 NAS 前言 大家好,我是星...
每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站 - 手机博物馆 -CHAZ 3D Experience 一句话介绍:一个用 3D 方式重温...
星哥带你玩飞牛NAS-11:咪咕视频订阅部署全攻略

星哥带你玩飞牛NAS-11:咪咕视频订阅部署全攻略

星哥带你玩飞牛 NAS-11:咪咕视频订阅部署全攻略 前言 在家庭影音系统里,NAS 不仅是存储中心,更是内容...