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

嵌入式Linux内核错误跟踪技术

时间:2023-03-17 20:15:50 科技观察

随着嵌入式Linux系统的广泛应用,对系统的可靠性提出了更高的要求,尤其是涉及到生命财产等重要领域,要求系统达到安全IntegrityLevel3级及以上[1],故障率(每小时发生危险故障的可能性)小于10-7,相当于系统的平均故障间隔时间(MTBF)至少要达到1141年,因此提高系统可靠性成为一项艰巨的任务。对工业领域某公司14878个控制器系统的应用调查显示,从2004年初到2007年9月底,随着软硬件的不断完善,报错报错率已降至2004年的五分之一或更少,但发现错误的时间增加到原来的三倍多。这种解决问题所需时间的上升趋势固然是软件问题,但缺乏必要的手段来协助解决问题是主要原因。通过对故障的统计跟踪,发现难以解决的软件错误和解决时间长的软件错误集中在操作系统的核心部分,其中很大一部分集中在驱动程序中第2部分]。因此,错误跟踪技术被视为提高系统安全完整性水平的重要措施[1]。大多数现代操作系统都为开发提供了操作系统内核的“故障转储”机制,即当软件系统崩溃时,将内存内容保存到磁盘[3],或者发送到故障服务器[3]通过网络,或直接启动内核调试器[4]等,进行事后分析和改进。近年来,基于Linux操作系统内核的故障转储机制有以下几种:(1)LKCD(LinuxKernelCrashDump)机制[3];(2)KDUMP(LinuxKernelDump)机制[4];(3)KDB机制[5];(4)KGDB机制[6]。综合以上机制,可以发现四种机制有以下三个共同点:(1)适用于计算资源丰富、存储空间充足的应用;(2)系统崩溃后的恢复时间没有严格要求;(3)主要针对比较常见的硬件平台,比如X86平台。在嵌入式应用中,如果想直接使用上述其中一种机制,会遇到以下三个无法解决的困难:(1)存储空间不足。嵌入式系统一般使用Flash作为内存,而Flash的容量是有限的,在嵌入式系统中可能比内存容量小很多。因此,将内存内容全部保存到Flash中是不可行的。(2)录音时间应尽可能短。嵌入式系统一般都有复位响应时间越短越好的要求。一些嵌入式操作系统在2秒内重置并重新启动,但是上述可以在Linux系统中使用的内核故障转储机制非常耗时。30s以内是不可能的。写Flash的操作也是非常耗时的。实验表明,将2MB数据写入Flash需要400ms之多。(3)需要支持特定的硬件平台。嵌入式系统有多种类型的硬件。上面提到的四种机制都对X86平台提供了很好的支持,但是对其他系统的硬件支持还不成熟。由于这些困难,很难将上述四种内核崩溃转储机制中的一种移植到特定的嵌入式应用平台。因此,本文针对上述嵌入式系统的三大特点,引入了一种基于平台的嵌入式Linux内核崩溃信息记录机制LCRT(LinuxCrashRecordandTrace),以期在嵌入式系统中定位软件故障,解决软件故障。Linux系统提供帮助。一、Linux内核崩溃分析分析Linux内核在运行过程中对各种“陷阱”的处理就可以知道。Linux内核可以监控应用程序引起的错误。当应用程序被零除、内存访问越界、缓冲区溢出等错误时,Linux内核的异常处理例程可以处理这些由应用程序引起的异常。当应用程序产生不可恢复的错误时,Linux内核只能终止产生错误的应用程序,其他应用程序仍然可以正常运行。如果Linux内核本身或新开发的Linux内核模块存在bug,如“被零除”、“内存访问越界”、“缓冲区溢出”等错误也会被异常处理例程处理的Linux内核。Linux内核在异常处理程序中判断,如果发现是“严重不可恢复”的内核异常,就会引起“kernelpanic”,即Linux内核崩溃。图1显示了Linux内核对异常情况的处理流程。2.LCRT机制的设计与实现通过分析Linux内核代码可知,Linux内核本身提供了“内核通知机制”[7-8],预先定义了“内核事件通知”chain”,这使得Linux内核扩展开发人员可以使用这些预定义的内核事件通知链,在特定内核事件发生时执行额外的处理。通过对Linux内核源码的研究发现,针对上述“严重不可恢复的内核异常”,预定义了一个通知链和通知点,使得在Linux内核崩溃后,Linux内核可以将预先定义的“内核崩溃通知链”[7]连接到LCRT机制,获取Linux内核崩溃现场的一些信息,记录在非易失性存储器中,从而分析崩溃原因Linux内核崩溃。2.1设计要点LCRT机制的设计和实现基于以下具体机制:(1)编译器选项和内核依赖性Linux内核和相应的驱动程序使用GNU[9]的开源编译器GCC[9]编译。为了结合LCRT机制方便地提取和记录信息,需要特定的GCC编译器选项来编译Linux内核和相关的驱动程序和应用程序。使用的选项是:-mpoke-function-name[9]。使用该选项编译出的二进制程序可以包含C语言函数名的信息,方便追溯函数调用链时记录的信息的可读性。(2)Linux内核notify_chain机制[8]Linux内核提供了“通知链”功能,预定义了一条内核崩溃通知链。当Linux内核的异常处理例程判断系统进入“不可恢复”状态时,会沿着预定义的通知链依次调用注册在相应链中的通知函数。(3)函数调用的栈布局Linux内核大部分是用C语言实现的,C语言也多用于Linux内核开发。Linux内核和使用LKM扩展添加到Linux内核执行环境的代码是有规律可循的,这些代码执行过程中产生的堆栈布局与这些有规律的代码相关联。例如,这些函数会保存函数调用后的返回地址、调用函数时传递的参数、函数执行前调用函数的函数所拥有的栈帧的栈底。2.2LCRT机制的设计思路LCRT机制分为Linux内核模块[8]部分和Linux用户程序部分。内核模块部分的设计采用了Linux内核模块的方式,而不是直接修改Linux内核。这种设计降低了Linux内核与LCRT机制的耦合度,同时满足了Linux内核与LCRT机制独立升级的便利性。用户程序部分完成从非易失性存储器读取、清除LCRT机制保存的信息等相关功能。在LCRT机制的设计中,根据嵌入式系统的特点,设计决策如下:(1)记录对解决和定位问题最有辅助意义的函数调用关系链。(2)为了不占用过多的存储空间,有选择地保存函数调用序列中函数使用的栈内容,而不是保存所有内容。(3)将记录的信息保存到非易失性存储器中,既达到了掉电保存的目的,又缩短了写入时间。LCRT机制的设计包括以下五个方面。(1)设计Linux内核模块,动态加载LCRT机制,尽量少修改Linux内核代码。(2)将LCRT的通知函数挂载到相应的、预定义的Linux内核通知链上。(3)在LCRT机制的通知处理函数中进行堆栈回溯,获取函数调用信息。(4)将回溯的函数调用信息和栈空间内容记录到非易失性存储器中。(5)用于开发可以从非易失性存储器中读取保存信息的用户空间的工具。2.3LCRT机制的实现LCRT机制的实现可以参考2.2节的设计思路,分步实现。限于篇幅,本文只涉及Linux内核模块的原理和实现细节,只给出了LCRT机制的内核模块实现的伪代码。用伪代码描述LCRT机制的加载函数如下:)ptr_nocacheBT_NVRAM_LENGTH);notifier_chain_register(&panic_notifier_list,&my_panic_block);return0;}LCRT机制的通知处理函数完成函数调用关系回溯、获取函数名、函数栈内容等工作。限于篇幅,在这里用下面的伪代码来说明:voidll_bt_information(structpt_regs*pr){变量定义等初始化工作do{reglist=*(unsignedlong*)(*myfp-8);//获取函数开始执行时保存的寄存器信息from函数栈帧顶部  //从函数的代码中获取函数名区//获取之前保存的函数参数信息e函数从函数的栈帧执行函数体代码  //从本函数的栈帧获取调用本函数调用本函数的代码位置函数栈帧的栈底}while(直到函数调用链的链头);   //获取函数调用栈帧的内容//填充信息记录的记录头//将上面循环获取的信息保存到非易失性存储器中write_to_nvram((void*)bt_nvram_ptr,&bt_record_header,sizeof(bt_info_t));}3.验证和评估LCRT机制3.1部署LCRT机制部署LCRT机制,LCRT机制工作前需要做的相关工作有:(1)编译LCRT机制的Linux内核模块部分目标Linux内核;(2)将LCRT机制的内核模块部分加载到Linux内核中。3.2实验结果为了测试LCRT机制的效果,构造一个会导致Linux内核崩溃的设备驱动模块,并将这个内核驱动模块记录为bugguy.ko,列出bugguy.ko中会导致崩溃的代码Linux内核崩溃如下图如下图:irqreturn_tmy_timer_interrupt(intirq,void*dev_id,structpt_regs*regs){Confirmhardwarestatusandclearinterruptstatusif(ujiffies>5000){void*ill_pointer=NULL;  *(unsignedlong*)ill_pointer=0;}else{ujiffies++;  }  returnIRQ_HANDLED;}解释:粗体代码为产生bug的代码。从上面的代码可以看出这个错误是解析空指针导致的。如果在中断处理程序中发生空指针分析,将导致Linux内核崩溃。在部署了LCRT机制的嵌入式linux系统上将这个bugguy.ko加载到Linux内核中,这样会导致Linux内核崩溃的中断处理程序就可以运行,LCRT机制可以将相关信息保存到非易失性内存。系统复位后,可以通过LCRT机制的用户空间工具读取保存的信息。实验结果表明,可以得到如图2所示的函数调用链信息。图2被标记为会导致Linux内核崩溃的错误代码的中断处理函数,也就是真正导致系统崩溃的“罪魁祸首”。所有记录的信息仅占用不到1KB的存储空间,写入非易失性存储器的时间控制在50ms以内。所记录的信息可以在使用少量空间和少量时间的情况下对发现和解决问题有很大帮助。实验结果表明,在LCRT机制的作用下,可以快速定位嵌入式Linux系统中可能导致系统宕机的软件隐患。这为后续故障排除和软件改进提供了关键的辅助信息。对于嵌入式Linux内核,有助于提高Linux内核的稳定性和可靠性。在基于ARM的嵌入式Linux应用中,开发了LCRT机制,当系统内核崩溃时,将导致崩溃的函数调用链和堆栈信息记录在非易失性存储器中。至此,LCRT机制可以记录基于ARM的嵌入式Linux内核崩溃时的函数调用链信息,可以直接获取函数名,函数调用链中单个函数被调用时的参数信息,以及栈帧信息函数调用链中的每个函数。这些记录的信息对于改进和开发基于ARM的嵌入式Linux应用程序具有重要的辅助意义。