马培++翟高寿
摘要:操作系统是整个计算机系统的核心,而设备驱动程序则占据操作系统内核的相当份额并对系统安全产生举足轻重的作用。作为主机与外围设备之间通信控制的桥梁,设备驱动无疑会受到硬件设备故障的直接影响。在Linux系统中,驱动程序问题是造成系统安全的主要根源之一。特别地,由于设备驱动代码的编写往往没有考虑到硬件设备的瞬时故障,所以当硬件设备发生瞬时差错的时候,就有可能导致相应驱动程序甚至整个系统出现不可预料的后果。本文主要讨论了面向硬件瞬时故障的驱动程序敏感代码的自动分析方法,并设计和实现了对应的代码分析工具原型。运用该工具原型对Linux内核设备驱动源码的实验分析结果表明,该方法和原型可以发现设备驱动中潜在的受到硬件设备瞬时故障影响的敏感代码。据此,采用适当方法对敏感代码进行修复完善,将可以实现设备驱动程序的安全加固。
关键词:设备驱动程序;硬件瞬时故障;敏感代码;Linux;自动分析
中图分类号:TP311
文献标识码:A
DOI:10.3969/j.issn.1003-6970.2015.12.003
本文著录格式:马培,翟高寿.面向硬件瞬时故障的Linux设备驱动敏感代码的自动分析与检测[J].软件,2015,36(12):09-15
0 引言
可靠性对于操作系统来说一直是最重要的问题,由于计算机已经深深嵌入到我们的生活中,并且故障一旦发生我们很难有机会能弥补,因而我们需要计算机更高的可靠性来保证系统运行的正确性。
应用程序是通过设备驱动程序唤起设备,而设备和驱动程序的交互是通过硬件指定的一个特定的协议,如果硬件设备遵从这些特定的协议,那么驱动程序就会完全的信任它并接受来自它的任何输入。不幸的是设备常常不能按照这样的规章行事,某些故障是由于磨损或者电磁干扰引起的,导致往驱动输入的数据发生改变,由于驱动在Linux内核中,就可能导致不可预料的系统故障。我们将驱动程序中由于硬件的瞬时故障而可能导致驱动出现问题的代码称为敏感代码。微软windows服务器的研究证明:(l)设备故障是引起系统崩溃的主要原因;(2)短暂性的硬件设备故障是正常的现象;(3)驱动程序容忍硬件设备故障能提高系统的可靠性。没有解决这些问题,操作系统的可靠性就会由于设备及驱动的可靠性而被限制。
针对设备瞬时故障如何自动分析和检测驱动程序的敏感代码,我们主要是通过验证来自设备的所有输入,并且报告所有存在的故障以使得管理员能够主动管理硬件错误。本文提出通过代码分析工具分析设备驱动程序代码,找到驱动中使用来自设备输入的位置,即敏感代码在驱动程序中的位置。如果驱动程序使用了没有通过正确检查的设备输入数据并且该数据不正确,我们就通过修改驱动程序以插入验证代码,实现驱动容忍硬件瞬时故障的目的。我们实现了当瞬时故障发生并导致驱动出现问题的时候,使用系统日志的方式报告相关的硬件故障。为了修复相应的硬件故障所造成的驱动甚至内核错误,我们引入了一个通用的恢复服务,这个服务可以重置设备。我们依赖于使用隐匿驱动程序来提供这种修复的服务,从而能够针对设备故障实现驱动的恢复机制。
本文提出面向硬件瞬时故障的驱动程序敏感代码的自动分析方法,实现了自动分析工具原型。通过执行自动分析工具表明,该方法可以准确检测到驱动程序中的敏感代码,并实现驱动程序的加固,提高驱动及系统的可靠性。
1 硬件瞬时故障及敏感代码
1.1 硬件瞬时故障
现代的CMOS设备倾向于发生内部错误,并且在当晶体管萎缩的情况下这种错误会更加严重。以前的研究表明某些设备会发生短暂的比特位反转错误,即单个比特位从1变为0或从0变成1;永久性的门级故障,即在一段较长的时间内某一比特保持固定值;桥接故障,即相邻比特对发生电力干扰从而在比特间产生逻辑与门、逻辑或门。环境条件的改变如电磁干扰或者辐射都能导致产生短暂性故障。设备的磨损和老化也可能导致门级故障或者桥接性的错误。
设备驱动程序访问来自设备的输入的时候会侦查故障,如PCI驱动,通过内存或I/O端口执行I/O操作,设备驱动可能会在I/O中读取到设备中的错误的值;如USB驱动,使用请求/响应协议,设备故障可能导致响应包中包含错误的数据。很多硬件故障表现为在设备寄存器中存放了被篡改的值,如设备控制器内部的单比特反转可能传播到某些内部寄存器,而设备驱动程序读取到这个内部寄存器中被篡改的值的时候就可能会发生故障。门级故障也可能存在上述同样的问题。当硬件设备不能在规定的时间内响应,在设备固件中的错误表现就可能会是不正确的输出数据或超时故障。
1.2 敏感代码
敏感代码即驱动程序中由于硬件的瞬时故障导致驱动出现问题的代码。各类硬件纷繁复杂质量层次不齐,驱动可能由于物理设备出现偶发性的瞬时故障而影响驱动及系统的安全可靠性。驱动对外设的访问主要是通过I/O端口,I/O端口即外设寄存器,是接口电路中能被CPU直接访问的寄存器地址。几乎每一种外设都是通过读写设备上的寄存器来进行工作的。Linux下访问I/O端口的方式主要有两种I/O映射方式和内存映射方式。I/O映射方式是直接使用I/O端口操作函数,即使用inb(),outb()等函数进行端口访问,内存访问方式是将I/O端口映射为内存进行访问,即使用I/O内存的函数进行端口访问。当发生硬件瞬时故障的时候可能会篡改I/O端口操作函数、I/O内存函数的返回值,导致驱动从I/O端口读取到的数据是不正确的值。当该数据被应用到驱动代码中不恰当的位置如作为循环条件或数组下标时,就可能导致驱动出现无限轮询或数组越界错误,从而影响驱动运行的正确性,降低驱动及系统的可靠性。因此,我们将驱动代码中使用I/O端口操作函数,并将这些I/O端口函数直接或间接应用在循环条件或数组下标中的代码称之为敏感代码。
2 设备驱动敏感代码自动分析方法的设计
大部分操作系统供应商向设备驱动程序编写者提出建议:如何固化驱动程序能够容忍硬件故障,(1)验证:我们认为从设备中获取的所有的输入都是可疑的,都需要进行验证才能确保数据在正确的范围内;(2)超时:当设备一直没有响应的时候,对于所有与设备的交互都应当有响应的措施来应对超时从而防止永久性的等待;(3)报告:所有可疑的行为或者操作都需要报告给操作系统服务器,从而方便对硬件故障的集中侦查和管理;(4)恢复:设备驱动程序应当能从任何的设备故障中恢复,如果需要可以通过重置设备来达到恢复的目的。
我们的目标主要就是能自动完成上述建议。总体设计方案如图1所示,首先,我们试图使得驱动程序能够容忍设备故障并能够从设备故障中恢复,从而防止了设备故障导致系统崩溃。我们主要针对的是短暂性的故障,即当重置设备后这样的故障不会再重复发生。其次,我们让设备驱动程序报告设备故障,使得系统管理员能够知道存在的短暂性故障并及时的处理故障设备。自动分析方法设计的过程主要是(l)分析驱动与设备交互的原理从而确定I/O端口及I/O内存操作函数,(2)分析驱动程序中直接或间接使用I/O端口操作函数的位置即找到所有受I/O影响的变量,(3)标注敏感代码即被修改的变量被使用在循环条件或数组下表的位置。由于短暂性的硬件故障是偶发性的,我们需要用到简单的故障注入工具进行模拟硬件故障,从而达到验证实验的目的。
2.1 110端口及I/O内存操作函数
Linux操作系统内核主要有主内核、内存管理、网络、文件系统及其设备驱动等目录。其中驱动设备通常不会完全集成到内核代码中,且对应于设备的驱动程序也通常是由第三方编写实现,因此容易在与内核模块通信、系统调用等方面产生错误,从而波及到系统的其他内核模块导致系统崩溃。实践证明,在导致系统故障的代码中,有50%以上的错误是由设备驱动程序引起的。
在Linux系统中,主机与外界交换信息是通过输入输出设备进行的,输入输出设备与系统的交互式是使用驱动这个中间媒介,驱动通过访问I/O端口实现与硬件设备的通信。由于CPU对外设端口物理地址的编址有两种方式:I/O映射方式和内存映射方式,所以驱动访问I/O端口也有两种途径:其一为I/O映射方式即是直接使用I/O端口操作函数;其二是内存映射方式即将I/O端口映射为内存进行访问。这两种方式都要使用相应的I/O端口及I/O内存操作函数对数据进行读写。I/O端口操作函数主要包括inb(),inb_p(),insb(),outb(),outb_p()等。I/O内存操作函数主要有ioread8(),ioport_map(),readb(),readl()等。当硬件发生瞬时故障,可能影响这些函数的返回值,使得驱动读取到错误的值而使驱动出现运行错误,影响驱动甚至系统的可靠性。
2.2 自动分析方法
设备驱动程序依赖于硬件的正确性以确保自身的正确性,我们通过对驱动程序代码进行静态分析找到驱动程序中使用源于设备的数据的位置,并且验证数据的有效性,防止在执行代码之前读取到非有效的数据而导致不可预料的系统错误。静态分析过程的输入主要是整个Linux源码中的驱动代码,即/Linux/drivers和/Linux/sound下的所有文件,输出是驱动程序源码中直接或间接使用I/O和内存操作函数的位置和敏感代码,即可能潜在的故障源列表。
自动分析方法主要是对系统驱动代码进行扫描,首先针对驱动的每一个文件进行词法分析,分析出所有变量、常量、函数、界符等。然后对驱动文件进行语法分析,找出所有的使用I/O端口操作函数和I/O内存函数的位置,并标注被I/O操作函数直接或间接修改的变量。最后找出被修改的变量被应用在循环条件或数组下标的位置,即找到驱动中与硬件瞬时故障相关的敏感代码。
针对自动分析结果所找到的敏感代码,我们的处理机制是在敏感代码处插入超时、越界检查和引入自动修复机制。当发生设备瞬时故障并引起驱动程序发生无限轮询或数组越界错误时,则会唤起通用的恢复服务,即隐匿驱动程序,对设备进行重置,从而达到恢复的目的。
3 原型实现及验证
该原型实现的开发平台是Ubuntu12.04,在Sourceinsight和Gedit下实现代码的编写,开发语言为C语言,使用Makefile和gcc4.8.1实现编译功能,使用gdb代码调试工具对自动分析工具进行调试运行。
3.1 原型实现
本文提出的加固驱动程序主要是排除硬件依赖性故障,固化驱动程序的工作流程如图2可分为三个部分,分别为分析驱动源码找到驱动程序使用源于设备的数据的位置、验证驱动程序获取硬件数据的有效性、插入代码对潜在的故障进行修复。找到驱动程序中使用1/0操作函数的位置主要是对驱动程序代码进行词法分析和语法分析用以识别各种类型的语句,检测出与硬件通信相关的I/0操作函数的使用位置并进行标注。验证相关硬件数据的有效性是对从硬件获取的数据进行有效性检查,如:添加计时器,检查数据范围等。插入代码进行修复是当硬件依赖故障偶然性的发生时我们插入代码,引入通用的自动修复机制,对硬件进行重置从而达到容忍硬件故障的目的。为实现对Linux内核源码中的所有文件应用自动分析工具,我们通过编写shell脚本,实现对所有驱动文件进行遍历,在遍历过程中执行我们的分析过程。
敏感代码检测分析流程如图3所示。主要有四个部分组成:驱动程序代码的预处理、词法分析、语法分析、静态分析输出敏感代码。
3.1.1 设备驱动源程序的预处理
预处理驱动程序是分析代码的第一步,它主要处理C语言中的各种预处理语句和注释。处理的预处理语句主要包括#define、#error、#include、#if、#else、#ifdef、#line等。同时在预处理阶段删除“//”和以“/*”开头以“*/”结尾的注释,以方便后续代码分析。
预处理后生成与分析驱动文件所对应的*.tmp文件。处理后的文件中没有注释行,并且完成了宏定义的替换,被include包含的文件也被插入到相应的位置。
3.1.2 词法分析
词法分析主要是对经过预处理的程序代码进行分析,得出相关单词符号如关键字(int、struct)、变量、常量(十进制、十六进制等)、运算符(+、-、*、/等)、界符(,、;、(等)等,将这些单词符号放入到相应的符号表中。该词法分析主要检测的是变量,以方便后续静态分析找到被I/0操作函数影响的变量。
经过词法分析将相关的单词符号填入到符号表中,用于记录源程序中各种单词符号的属性和特征。本文变量和函数的符号表的定义如下:
(l)变量标识符的符号表否为I/O操作函数*/
}FuncTable:
3.1.3 语法分析
语法分析主要是在完成词法分析的基础上对代码进行语法结构的分析。该分析过程用于获取与变量和函数相关的信息存入到符号表中,为后续的静态分析做准备。
(l)提取变量相关信息,将驱动代码中出现的每一个变量的行号都记录下来,存放在变量符号表的定义变量和使用变量的字段中。
(2)提取函数相关信息,标识每个函数所定义和使用的变量,并记录下函数体内所引用的函数名等。
3.1.4 敏感代码的检测
通过上述的分析过程,我们能准确的标识出驱动源码中所有的标识符及其引用流程,我们静态分析的过程包括两步,第一步是标识出驱动程序中直接或间接使用I/0端口函数和I/0内存函数的位置,并找到被这些函数修改的变量标注在变量符号表的tainted字段中。第二步是找出被修改的变量或I/0操作函数应用在循环条件和数组下标的行号,将其标注为敏感代码输出到Taintedfor.txt中。
当被修改的变量被应用在驱动程序的循环条件中,而由于偶发性的硬件故障而导致该变量从硬件读取来的数据不是预期的值,那就可能导致驱动进去死循环永远都不能跳出来。由于驱动和系统内核有着千丝万缕的联系,驱动的无限轮询就可能导致不可预料的系统故障。当被修改的变量被应用在全局或局部数组变量的下标中,由于数组下标有范围界限,若因为偶发性的硬件故障而导致从硬件获取的数据值不在该数组的合法范围内,驱动就会因为存在越界错误而导致系统问题。
3.1.5 敏感代码的修复与报告
针对上述分析出的潜在的故障因素,我们需要对其进行修复。对于无限轮询故障,主要是在驱动中插入代码打破无限轮询的条件。基于多次对驱动代码的延时测试和其他设备所使用的延时,我们使用最大的延时。我们在循环内添加计时器,并设置最大延时时间。当在循环体内循环的时间超过我们设置的最大延时时间时,我们就认为该循环陷入了无限轮询,此时我们引入通用的恢复服务隐匿驱动程序对硬件进行重置,重新获取硬件相关的信息。对于数组越界问题,我们主要是根据驱动代码上下文,在被修改变量用在数组下标之前的位置处对该变量进行条件判断,若变量值在数组范围外,则引入通用的恢复服务,对故障进行恢复。
在对无限轮询和数组越界的驱动错误进行修复后,当确实出现硬件故障的时候我们使用printk函数实现系统日志通知系统管理员发生了硬件故障,以便于系统管理员对故障进行管理和维护。
3.2 实验验证
3.2.1 敏感代码自动检测结果分析
我们静态分析的对象主要是整个Linux内核驱动,内核版本为Linux2.6.18。通过分析drivers和sound目录下的6000多个源程序文件,这些文件包括的驱动类主要有网卡驱动、声卡驱动、视频驱动、SCSI驱动和其它类驱动,从静态分析的输出文件中发现了大量的潜在的无限轮询和数组越界错误。分析结果表明驱动的硬件依赖性故障是普遍存在的,只有尽量避免这些潜在的错误才能提高操作系统的可靠性。
使用随机抽样方法检查驱动代码分析结果表明:误报率低于9%。对于无限轮询错误,我们检查了100个例子,只是发现了10个误报,误报原因主要是该驱动自身已经实现了检查和修复方法,但这种误报并不会对驱动造成损害,唯一可能的损害就是增加了少量的不必要的开销。
3.2.2 敏感代码修复与故障报告
通过使用自动分析工具对驱动源码进行分析,我们能得到所有驱动类中潜在的硬件依赖性故障。由于对驱动进行测试依赖于硬件,我们不可能拥有内核驱动所对应的所有的硬件,所以我们主要对已经存在的硬件进行验证,如电脑本机的网卡和声卡。由于短暂性的硬件故障是偶发性的,我们无法预期什么时候会发生,所以我们引入了故障注入工具,模拟硬件故障的发生。通过故障注入工具模拟硬件故障,我们测试了固化前的驱动和固化后的本机网卡驱动。在测试过程中,我们发现对于固化后的驱动代码示例如下图4所示,当发生了硬件瞬时故障的时候能够唤起通用的恢复机制对硬件进行重置,驱动程序再次读取相关信息可以正常运行,而对于固化前的驱动,由于发生了短暂性的硬件故障,导致了驱动不能正常运行下去。正是由于我们插入的修复代码和引入驱动的恢复机制,当发生硬件故障时候,我们的驱动程序能够容忍硬件瞬时故障,从而实现在软件中容忍硬件故障的目的。
对于故障的报告分析,我们主要是报告设备超时(驱动代码发生无限轮洵)和报告驱动代码数组越界。由于Linux没有故障管理服务,我们使用printk来实现系统日志。系统管理员可以通过在终端输入命令dmesg或者进入Linux系统日志目录进行查看。对于设备超时的故障报告主要是当由于硬件瞬时故障而导致驱动程序发生无限轮询错误的时候通过printk函数以infinite loop为标识写系统日志。系统管理员通过infinite loop为标识查看是否发生该类故障。对于数组越界的错误报告主要是当驱动代码由于硬件故障而发生数组越界错误的时候通过printk函数以Array out of range为标识写系统日志。系统管理员通过Arrav out of range查看是否发生数组越界故障。通过dmesg查看系统日志发现,我们能正确的报告发生的相关驱动故障,从而方便管理员对故障进行管理和维护。
4 测试评估
4.1 功能测试评估
为了评估自动分析工具的功能准确性,我们定义误检率和漏检率。误检率即某处不是受硬件瞬时故障影响的敏感代码,而自动分析工具却标注其为敏感代码,在整个扫描过程中出现这种误报的概率。某处是受硬件瞬时故障影响的敏感代码,但自动分析工具却没有标注其为敏感代码,这种漏报的概率称为漏报率。
本文主要是对2.6.18的内核版本应用我们的自动分析工具,验证了这种分析方法能够正确的查找到所有驱动模块的敏感代码,按照驱动类划分查询到的敏感代码数量如下表1所示。
通过静态分析和有效性检查,我们对整个Linux内核驱动代码进行全盘分析扫描,主要是查找两类硬件依赖故障即无限轮询和数组越界错误。对于短暂性硬件故障导致驱动程序的无限轮询,在网卡驱动类中查找到110个潜在的错误,在scsi驱动类290个潜在的错误,在整个Linux驱动中总体查找到740个错误。对于静态数组越界的敏感代码,我们在Linux驱动中共查找到43个潜在的错误。上述错误充分说明潜在的硬件依赖性错误在内核驱动代码中是普遍存在的且是亟待的问题。
通过对2.6.18和3.2.1内核版本的驱动源码使用自动分析工具进行比较,内核版本为3.2.1的内核源码所检测到的敏感代码比2.6.18少,并通过抽样检查其误报率和漏报率均较小且差别不大。由于内核版本为3.2.1是2.6.18的升级,所以检测到的敏感代码少是合理的。比较结果证明,我们的自动分析工具能够对不同内核版本的驱动的敏感代码进行准确的分析。
我们针对本机的网卡驱动通过引用故障注入工具模拟短暂性的硬件故障,对修改前和修改后的驱动进行测试,修改前的网卡驱动会产生无限轮询错误导致本机网卡驱动出现无法修复的问题,而修改后的驱动能正确的容忍硬件依赖性故障,唤起通用的驱动恢复服务并告知系统管理员发生了短暂性的硬件故障,达到我们预期的效果。
4.2 性能评估测试
性能评估主要是评估检测敏感代码系统能力及故障恢复性能,识别出系统存在的弱点并验证该系统的稳定性。本文采用系统性能测试,对系统性能进行全面的评估,主要包括宏观性能测试和微观性能测试。宏观性能测试主要是对检测敏感代码系统的耗时和准确性进行评估。微观性能分析主要是对自动分析工具进行CPU利用率评估。
本文所采用的测试是在Ubuntu 12.04系统下完成,该系统中Linux内核版为3.11.0-26,GCC编译器版本4.8.1,计算机硬件配置为CPU Pentium Dual-Core,内存2G,硬盘200G。
本文宏观性能测试主要测试检测敏感代码系统的耗时及其准确性。检测耗时主要方法是使用UNIXdate命令。在对Linux驱动源码进行遍历扫描的脚本中,增加驱动分析开始时间及结束时间,通过计算得出分析检测驱动代码所消耗的时间为4700s。对检测到的敏感代码的准确性我们主要是采用加权随机抽样的方式,通过对自动检测敏感代码的结果和手动抽样检查进行比较,其检测到的敏感代码的准确率较高。
微观性能评估主要是对自动分析工具进行CPU利用率评估,检测方法主要是利用Linux操作系统的ps命令获取该分析工具的CPU利用率。在对内核驱动代码进行自动分析的过程中,我们使用ps命令获取其CPU利用率为2.2%,结果表明,该工具性能开销较小,符合我们的设计要求。
5 结论
系统的可靠性由于设备及对应驱动的可靠性而受到限制,Linux系统出现的很多故障都是由于硬件瞬时故障导致的。为解决该问题,本文提出一种在驱动软件中容忍硬件瞬时故障的机制,讨论了面向硬件瞬时故障的驱动程序敏感代码的自动分析方法,通过对Linux驱动源码进行预处理、词法分析、语法分析和静态分析,查找出可能产生无限轮询和静态数组越界的敏感代码并进行标注。为修复该类敏感代码可能引起的驱动问题,我们通过插入时间检查和范围检查,并引入了一个通用的恢复服务隐匿驱动程序对发生短暂性硬件故障的设备进行重置,驱动重新读取寄存器中的硬件数据以达到修复的目的。同时,当发生短暂的硬件依赖性故障的时候,我们通过系统日志的方式通知管理员硬件发生问题,方便系统管理员对系统进行管理和维护。由于偶发性的硬件故障比较罕见的发生,为测试实验结果,我们使用了故障注入工具来模拟我们期望的硬件故障,通过测试,经加固后的驱动程序能够很好的容忍硬件瞬时故障,从而很好的实现了在软件中容忍硬件故障的目的。