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

news2024/11/20 3:26:31

最近在调试设备时,遇到了一个偶发的开机死机问题。通过查看输出日志,发现内核报告了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)

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

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

那我们就顺藤摸瓜,看看这个位置对应的代码是什么。首先,在工程中搜索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 << THREAD_SIZE_ORDER)

这是一个跟页面大小相关的量。在当前系统中,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 : [<bf64b670>]    lr : [<bf64b5f0>]    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] ; 0x1089664:  e3a01003   mov    r1, #39668:  e593300c   ldr    r3, [r3, #12]966c:  e5933308   ldr    r3, [r3, #776] ; 0x3089670:  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库的影响。二者需要统一起来看待和管理。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/110616.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

YOLOV7学习记录之训练过程

在前面学习YOLOV7的过程中&#xff0c;我们已经学习了其网络结构&#xff0c;然而实际上YOLOV7项目的难点并不在于其网络模型而是在于其损失函数的设计&#xff0c;即如何才能训练出来合适的bbox。 神经网络模型都有训练和测试&#xff08;推理&#xff09;过程&#xff0c;在Y…

QT JS交互、调用JS、传值

本文详细的介绍了QT JS交互、调用JS、传值的各种操作&#xff0c;包括QT向JS传递String字符串、包括QT向JS传递Int数字、包括QT向JS传递List数组&#xff0c;同时也接收JS向QT返回的List数组、JS向QT返回的Json、JS向QT返回的数字、JS向QT返回的字符串。 本文作者原创&#xff…

Vue基础8之Vue组件化编程、非单文件组件与单文件组件

Vue基础8Vue组件化编程对组件的理解一些概念的理解非单文件组件基本使用几个注意点组件的嵌套VueComponent一个重要的内置关系先导篇&#xff1a;原型对象正文&#xff08;可以理解为类的继承&#xff09;单文件组件Vue组件化编程 对组件的理解 传统方式&#xff1a; 使用组…

计算机网络-交换方式

目录电路交换&#xff08;Circuit Switching&#xff09;分组交换&#xff08;Packet Switching&#xff09;报文交换&#xff08;Message Switching&#xff09;电路交换、报文交换、分组交换的对比电路交换&#xff08;Circuit Switching&#xff09; 在电话问世后不久&#…

扫雷游戏的设计(百分百还原电脑操作)

目录 &#x1f332;了解扫雷游戏的作用原理并梳理思路 &#x1f332;扫雷游戏前期部分完善 &#x1f337;文件的创建 &#x1f337;创建菜单&#xff0c;完善主函数 &#x1f333;代码呈现&#xff1a; &#x1f332;扫雷游戏主题内容 &#x1f334;第一步初始化棋盘 &#x1…

Gradle中如何修改Springboot引入的依赖版本

扫描漏洞升级 不知道各位是否遇到过以下问题&#xff1a; 当下层项目将spring引入的某个依赖版本升级之后&#xff0c;上层项目只要指定了Springboot版本&#xff0c;那么还是会将这个版本改回去&#xff1f; 比如&#xff1a;现在有两个Springboot项目A、B&#xff0c;B项目…

Git安装和配置

GitGitee 官网安装配置教程&#xff1a;https://gitee.com/help/articles/4104本文是以官网教程为基础而展开的实践笔记。初学者可以以本文为引入&#xff0c;但建议最终以官方文档为最终深入学习的参考。一、 下载和安装Git 1、官网下载&#xff1a;https://git-scm.com 如果对…

HTML5基础

HTML5 文章目录HTML5概述开发工具浏览器开发软件DemoHTML5语法HTML5标签HTML5标签属性HTML5文档注释HTML5文档结构头部内容主体内容DemoHTML5常见标签常见块级标签标题标签水平线标签段落标签换行标签引用标签预格式标签无序列表标签有序列表标签定义列表标签分区标签常见行级标…

【Java寒假打卡】Java基础-继承

【Java寒假打卡】Java基础-继承一、继承的好处和弊端二、继承的成员变量访问特点三、重写方法四、方法重写的注意事项五、权限修饰符六、构造方法一、继承的好处和弊端 继承的好处 提高了代码的复用性 提高了代码的维护性 让类和类之间产生了关系 是多态的前提 继承的弊端 …

Flink-使用filter和SideOutPut进行分流操作

文章目录1.什么是分流&#xff1f;2. 过滤器(filter)3. 使用侧输出流&#xff08;SideOutput&#xff09;&#x1f48e;&#x1f48e;&#x1f48e;&#x1f48e;&#x1f48e; 更多资源链接&#xff0c;欢迎访问作者gitee仓库&#xff1a;https://gitee.com/fanggaolei/learni…

四、网络层(七)网络层设备

目录 7.1 路由器的组成和功能 7.2 路由表与路由转发 7.1 路由器的组成和功能 路由器是一种具有多个输入/输出端口的专用计算机&#xff0c;其任务是连接不同的网络&#xff08;可以是异构的&#xff09;并完成路由转发。在多个逻辑网络&#xff08;即多个广播域&#xff…

Vulnhub靶机:HACKADEMIC_ RTB1

目录介绍信息收集主机发现主机信息探测网站探测Sql注入挂马提权介绍 系列&#xff1a;Hackademic&#xff08;此系列共2台&#xff09; 发布日期&#xff1a;2011年9月6日 难度&#xff1a;初级 运行环境&#xff1a;VMware Workstation 目标&#xff1a;取得 root 权限 flag…

5W2H分析法

什么是5W2H 5W2H分析法又叫七何分析法&#xff0c;是二战中美国陆军兵器修理部首创。简单、方便&#xff0c;易于理解、使用&#xff0c;富有启发意义&#xff0c;广泛用于企业管理和技术活动&#xff0c;对于决策和执行性的活动措施也非常有帮助&#xff0c;也有助于弥补考虑…

【UE4 第一人称射击游戏】07-添加“AK47”武器

素材资料地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1epyD62jpOZg-o4NjWEjiyg 密码&#xff1a;jlhr 效果&#xff1a; 步骤&#xff1a; 1.打开“WalkRun_BS”&#xff0c;将内插时间改为1 2.创建一个文件夹&#xff0c;命名为“Weapons” 进入“Weapons”…

可视化数据图表-FineReportJS实现清空控件内容

1. 概述 1.1 问题描述 在使用查询控件时&#xff0c;有时我们希望能够快捷重置控件的内容&#xff0c;或者重置所有控件的内容。效果如下图所示&#xff1a; 重置某个控件的内容&#xff1a;1.2 实现思路 在使用查询控件时&#xff0c;有时我们希望能够快捷重置控件的内容&a…

H3C 二层链路聚合

简介&#xff1a; 它通过将多条以太网物理链路捆绑在一起成为一条逻辑链路&#xff0c;从而实现增加链路带宽的目的。 成员端口&#xff1a; 选中&#xff08;Selected&#xff09;状态&#xff1a;此状态下的成员端口可以参与用户数据的转发&#xff0c;处于此状态的成员端口…

绝!OpenAI 年底上新,单卡 1 分钟生成 3D 点云,text-to 3D 告别高算力消耗时代

内容一览&#xff1a;继 DALL-E、ChatGPT 之后&#xff0c;OpenAI 再发力&#xff0c;于近日发布 PointE&#xff0c;可以依据文本提示直接生成 3D 点云。 关键词&#xff1a;OpenAI 3D 点云 PointE OpenAI 年底冲业绩&#xff0c;半个多月前发布的 ChatGPT 广大网友还没…

政务行业势能厂商 |美创科技入选《嘶吼2022中国网络安全产业势能榜》

近日&#xff0c;网络安全垂直媒体嘶吼网络安全产业研究院正式发布《嘶吼2022中国网络安全产业势能榜》评选结果。凭借在政务数据安全领域的服务深耕以及广泛的市场认可&#xff0c;美创科技入选势能榜“政务篇”&#xff0c;获评政务行业“专精型”安全厂商。 嘶吼安全产业研究…

Apache 之执行 CGI 脚本(Python 实现)

目录前言1 查看并挑选 Python 版本2 用 Python 实现一个简单的 CGI 脚本3 查看 CGI 环境变量总结前言 本文记录了一个搭建 CGI 环境的示例。前文推荐&#xff1a;《Apache 2.4.54 x64 安装及配置》。 【系统环境】 Win10-64bit Apache 2.4.54 x64 Python 3.11.1 1 查看并挑选…

PyInstaller的常用打包命令

学习了pyqt后&#xff0c;设计了界面&#xff0c;并且需要打包为exe程序。 每次打包时&#xff0c;都要查好久资料&#xff0c;故此记录一下常用的命令。 PyInstaller 是一个 Python 应用程序打包工具&#xff0c;它可以将 Python 程序打包为单个独立可执行文件。 要使用 P…