当前位置: 首页 > 科技观察

一个kernelOops问题的分析与解决

时间:2023-03-12 03:54:43 科技观察

最近在调试设备的时候,遇到了一个偶发性的bootcrash问题。通过查看输出日志,发现内核报了oops错误,如下图(中间省略了部分日志,替换为...):UnabletohandlekernelNULLpointerdereferenceatvirtualaddress0000000cpgd=cdd90000[0000000c]*pgd=8df4d831,*pte=00000000,*ppte=00000000内部错误:哎呀:17[#1]SMPARMCPU:0PID:206Comm:mountTainted:PO3.18.20#4task:ced40e40ti:cdf7c000PCtask.ti0:cdf7c0在exfat_fill_super+0xc8/0x4cc[exfat]LR在exfat_fill_super+0x48/0x4cc[exfat]pc:[]lr:[]psr:a0080013sp:cdf7de48ip:fffffffffp:c0744a30r10:00000001r9:bf652dacr8:00008000r7:cdf80000r6:cf302000r5:cdf85000r4:cdf41000r3:00000000r2:cdf85104r1:00000003r0:000001b5Flags:NzCvIRQsonFIQsonModeSVC_32ISAARMSegmentuserControl:10c5387dTable:8dd9006aDAC:00000015SP:0xcdf7ddc8:ddc8cfa70880fffffffc0000000bcf17f800cf4ea000cf17f60000000000cfdee780dde8bf64b670a0080013ffffffffcdf7de3400008000c0012e18000001b500000003......Processmount(pid:206,stacklimit=0xcdf7c238)Stack:(0xcdf7de48to0xcdf7e000)de40:00000001cdf41000cdf7deb0cf17f60c0000000100008000de60:cdf41000cdf7c038c0744a30c0264164bf652db4cdf7de843b9aca0000000004de80:cf4ea6c000000083cf4ea734cf302000cf4ea6c00000008300008000cdf41000......dfc0:0119704001197040be9fff4900000015be9fff31000080000000000000000000dfe0:b6e3d2e0be9ffaf80007ebecb6e3d2f060080010be9fff490000000000000000[](exfat_fill_super[exfat])from[](mount_bdev+0x168/0x190)[](mount_bdev)来自[](exfat_fs_mount+0x18/0x20[exfat])[](exfat_fs_mount[exfat])来自[](mount_fs+0x14/0xcc)[](mount_fs)来自[](vfs_kern_mount+0x4c/0x104)[](vfs_kern_mount)来自[](do_mount+0x194/0xb54)[](do_mount)from[](SyS_mount+0x74/0xa0)[](SyS_mount)from[](ret_fast_syscall+0x0/0x38)Code:e5851108e3a01003e593300ce5933308(e1information)来自上面的logd3,初步可以看出,挂载exfat格式文件系统的内存卡时,内核出现了空指针访问问题,最终导致内核崩溃输出oops,因为这个问题以前没有遇到过,而硬件最近换了读卡器存储卡也更新了,从以前的100MB/s到120MB/s,所以初步怀疑问题可能是换了读卡器或者(和)存储卡导致的。但是,硬件和卡变化如何影响和导致上述oops错误的细节尚不清楚。还好堆栈信息比较清楚。当发生异常时,PC指针指向这个位置:exfat_fill_super+0xc8/0x4cc(PC在exfat_fill_super+0xc8/0x4cc[exfat])。那我们就顺藤摸瓜看看这个位置对应的代码是什么吧。首先在工程中搜索exfat_fill_super函数,了解其位置和关联模块。经过一番操作,发现这个函数在第三方开源库exfat中。该库提供对exfat文件系统挂载的支持,编译成ko库文件,在系统启动时被加载到系统中。其次,我们看一下问题日志中PC指针指向的代码行。因为日志只显示了exfat_fill_super函数的0xc8偏移量,所以为了准确找到这个位置,我们需要使用gdb,如下图:(gdb)lexfat_fill_supersb->s_d_op=&exfat_dentry_ops;}#endifstaticintexfat_fill_super(structsuper_block*sb,void*data,intsilent){structinode*root_inode=NULL;结构exfat_sb_info*sbi;intlongdebugerr;;(gdb)l*exfat_fill_super+0xc80x9670位于./exfat-nofuse-master/exfat_super.c:2301。内部选项;字符*iocharset;opts->fs_uid=current_uid();opts->fs_gid=current_gid();opts->fs_fmask=opts->fs_dmask=current->fs->umask;opts->allow_utime=(unsignedshort)-1;opts->codepage=exfat_default_codepage;opts->iocharset=exfat_default_iocharset;选择->区分大小写=0;可以看到,gdb告诉我们0xc8偏移量在2301行(它还告诉我们对应的程序集在0x9670,下面是将被使用):2301opts->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是一个宏,用来获取当前线程的任务结构(这里隐藏了另一个指针)。#defineget_current()(current_thread_info()->task)#definecurrentget_current()当前arm平台,通过栈寄存器获取线程信息。staticinlinestructthread_info*current_thread_info(void){registerunsignedlongspasm("sp");return(structthread_info*)(sp&~(THREAD_SIZE-1));}从上面的代码进一步知道,线程信息栈寄存器是通过位操作得到的。这里THREAD_SIZE的定义如下:#defineTHREAD_SIZE_ORDER1#defineTHREAD_SIZE(PAGE_SIZE<task返回初始日志,部分信息如下:task:ced40e40ti:cdf7c000task.ti:cdf7c000PCisatexfat_fill_super+0xc8/0x4cc[exfat]LR在exfat_fill_super+0x48/0x4cc[exfat]pc:[]lr:[]psr:a0080013sp:cdf7de48ip:fffffffffp:c0744a30其中sp在cdf7defo4,所以thread_in的位置应该是cdf7c000。从上面的日志我们也可以看出ti是cdf7c000,所以这个位置不会是空指针的位置。这里的任务是thread_info结构体的一个子域,如下:structthread_info{unsignedlongflags;/*低级标志*/intpreempt_count;/*0=>可抢占,<0=>错误*/mm_segment_taddr_limit;/*地址限制*/structtask_struct*task;/*主要任务结构*/structexec_domain*exec_domain;/*执行域*/那么,task有没有可能是空指针呢?上面的oosplog也给出了,task:ced40e40,所以task也不为空。这样current指的就是这里的task,一个不为空的地址。那么让我们再看看current->fs。这里的fs是task_struct结构体的一个子字段structfs_struct*fs;(省略了一些字段)。structtask_struct{易失性长状态;/*-1不可运行,0可运行,>0已停止*/void*stack;atomic_t用法;无符号整数标志;/*每个进程标志,定义如下*/unsignedintptrace;....../*此任务的CPU特定状态*/structthread_structthread;/*文件系统信息*/structfs_struct*fs;/*打开文件信息*/structfiles_struct*files;/*命名空间*/structnsproxy*nsproxy;…#ifdefCONFIG_PERF_EVENTSstructperf_event_context*perf_event_ctxp[perf_nr_task_contexts];结构互斥体perf_event_mutex;结构列表头perf_event_list;#endif#ifdefCONFIG_DEBUG_PREEMPT无符号长抢占列表;#endif#ifdefCONFIG_DEBUG_PREEMPTunsignedlongpreempt#endif_disable}来自上面的定义;看,是跟文件系统相关的结构体。分析到这里,考虑到问题的函数是exfat_fill_super,名字好像是一个超快的填充文件系统的操作。另外测试部门反馈,问题出现后,内存卡格式化后会恢复,所以怀疑是不是因为更换读卡器和内存卡导致读取超级块出错信息,文件系统相关访问出现空指针,报oops。为了验证这个想法,我将上面连续赋值的那行代码(也就是上述问题所在的2301行代码)拆分成多条语句,然后在每个指针使用点添加一条log,以便在出现问题时,输出问题出在哪个指针上。另外,为了尽可能的保护环境,出现问题后软重启设备,重新配置uboot参数让内核通过nfs挂载根文件系统,这样之前的ko库文件就可以了更换测试。奇怪的是,每次更换后,问题都消失了。这个现象好像打破了之前的猜测,感觉问题又出在软件方面了。在这种棘手的打印方案不起作用之后,我决定直接分析汇编代码,看看问题发生时空指针落在了哪里。反汇编目标文件,结合gdb(前面提到)报告的位置和oops报告的指令内容。代码:e5851108e3a01003e593300ce5933308(e1d330bc)??问题确定出在以下程序集的9670行:9660:e5851108strr1,[r5,#264];0x1089664:e3a01003movr1,#39603,c,l3r312]966c:e5933308ldrr3,[r3,#776];0x3089670:e1d330bcldrhr3,[r3,#12]9674:e1c2c0bcstrhip,[r2,#12]9678:e1c200bestrhr0,[r2,#14]]967c:e1c230bastrhr3,[r2,#10]9680:e1c230b8strhr3,[r2,#8]这是一条加载指令,即将r3寄存器所指示的内存地址在位置12之后偏移两个字节,加载到r3寄存器中。这里r3表示的内存地址是多少?根据oops给出的信息,是00000000,加上12,就是地址0000000C,所以oops报错。UnabletohandlekernelNULLpointerdereferenceatvirtualaddress0000000c结合问题点前后的C代码和汇编代码,直观上,这里的12应该是某个结构体中某个子字段的偏移量。找到这个偏移量对应的字段。然后你可以确定哪个赋值有一个空指针。回到C代码,在有问题的代码行前后使用了几个结构。为了快速确定偏移量,我选择参考内核container_of宏,定义一个宏来查找偏移量。#definemy_offsetof(TYPE,MEMBER)((size_t)&((TYPE*)0)->MEMBER)通过这个宏,快速找到结构中每个元素的偏移量。当然也可以通过看代码来判断,但是没有这种方法快。正是通过这个操作,引出了问题的最终原因。让我们继续。添加log获取offset后,获取到的相关offset信息如下:task_offset=12,fs_offset=904,umask_offset=12,fs_fmask=8,fs_dmask=10这里12,904,12,8,10好像与装配有隐含的对应关系。但是这里的904和776没有任何关系。我决定在添加日志后查看目标文件的反汇编代码,如下:97b8:e3a0b000movfp,#097bc:e3a0207bmovr2,#123;0x7b97c0:e3000000movwr0,#097c4:e300a000movwsl,#097c8:e5933388ldrr3,[r3,#904];0x38897cc:e3400000movtr0,#097d0:e340a000movtsl,#097d4:e1d330bcldrhr3,[r3,#12]97d8:e1c930bastrhr3,[r9,#10]97dc:e1c930b8strhr3,[r9,#8]97e0:e5cb2000strbr2,[fp]97e4:e595300cldrr3,[r5,#12]由于此时修改了代码,只能粗略判断之前问题所在的编译范围。从上面可以看出,这次汇编中的值对应的是打印出来的偏移量。根据这个偏移量,结合汇编,基本可以确定之前出现问题的汇编对应的是C代码中的fs->umask语句。因为fs是空的,如果再去获取umask,就会报空指针异常。那么问题来了,为什么fs会变空呢?有经验的读者此时可能已经猜到问题的原因了。我们可以看到,前面的代码反汇编后,fs的偏移量是776,加入日志重新编译后,反汇编变成了904。虽然加入日志导致代码修改,但不影响这个偏移量,所以此处的fs偏移量可能是问题所在。对于偏移量的变化,我考虑了三个因素,分别进行了验证:1.ko库文件由于坏的flash块或其他原因导致二进制文件中的一些位翻转。经过实际验证,排除了这个原因。2.ko库是针对不同平台编译的,是放置错误造成的。经过实际验证,也排除了这个原因。3.是添加当前日志后编译的ko库,其依赖的内核配置与上次编译ko库依赖的内核配置相比更新了,即内核配置发生了变化(内核版本本身是一致的)。最常见的情况是对内核的menuconfig操作。查看fs所在的task_struct结构体,发现里面有很多ifdef,但是一个都没有配置,但是有一个perf相关的CONFIG_PERF_EVENTS,是后面因为调试性能需要新配置的。但是这个配置选项是在fs结构体的后面(见前面的task_struct结构体),按理说不会影响fs在整个结构体中的偏移量。考虑到task_struct结构包含很多子结构,不排除上面的perf配置影响了fs前面的一些子结构,导致fs本身的offset改变。说了这么多,到时候你就知道是真是假了。把上面的选项关掉,重新编译内核,再编译exfat,查看汇编,发现offset又回到776了,没错,就是这个问题。最后的原因是内核更新了,但是ko还没有更新,导致两者不匹配(旧的ko库从offset776开始找到fs,但是在新内核中,fs的offset变成了904),从而产生潜在的问题。问题的原因终于找到了,但是问题的过程其实更值得关注:因为ko库也是运行在内核空间的,所以需要和内核版本匹配,进行一致的版本管理。再者,不仅在嵌入式领域,在桌面端也是如此。如果系统加载了ko库,当内核更新时,需要考虑对ko库的影响。两者需要一起看待和管理。个人简介:lccz(龙城池子),资深嵌入式开发人员,对Linux内核相关技术感兴趣。