曾广福,何浩辰,周书林
(国防科技大学计算机学院,湖南 长沙 410073)
当今随着计算机技术的迅速发展和计算能力持续提高,计算机软件系统也随之不断发展。在软件规模方面,目前常用的开源软件规模均已达到了百万行量级。据 Coverity公司[1]的年度开源软件代码分析报告显示,目前 Linux 内核源码规模正在以接近每年百万行量级的速度增长。
由于软件规模的不断扩大和软件可配置性的不断增强,软件系统的配置项数量显著增加,从而使用户使用软件系统时产生配置故障的几率显著升高。目前,软件配置故障已经成为系统异常、服务失效甚至系统崩溃的重要原因之一。Barroso等人[2,3]的报告称在谷歌的主要服务中,配置故障是导致服务失效的第2大原因,仅次于软件本身故障,占到了近 29% 的比例。
目前,软件配置项数量巨大,且系统中配置项均需要满足特定的配置约束,由于用户缺乏软件相关的领域知识,进行软件配置时极易违反对应配置约束而导致配置故障。目前软件用户和开发者常用的配置故障诊断方式是通过寻找配置需要满足的约束条件来定位出错的配置项。但是,上述配置约束条件的提取难点较多,如配置项之间或配置项与系统运行时环境之间存在复杂的配置关联约束关系,需要综合考虑源代码和系统运行环境。综合上述挑战,需要实现自动化的方法完成软件配置项的约束提取。
针对上述现状,本文首先对6款常用开源C/C++软件源码进行分析,提取了对应的配置相关特征。枚举类型配置作为软件系统的常用配置类型,其取值空间经常为固定的少数几个字符串或整型值,用户在不了解的情况下很容易错误设置对应的枚举配置值,导致配置失败或引发配置故障。另一方面,枚举类型配置在代码中通常存在普遍的代码分析段,具有一定的普遍特征,因此针对枚举类型的配置项取值空间进行提取很有必要。
陈伟等人[4]总结了配置相关的研究,并提出导致软件配置错误的主要原因之一是软件组件依赖导致的配置依赖。配置依赖存在单个配置依赖和配置间的依赖,本文所研究的配置约束属于单配置依赖范畴。
清华大学的李福亮等人[5]首先对互联网自动配置及配置案例进行概述;然后按照配置自动生成、配置验证、配置自动实现这3个方面对互联网自动配置研究进行分类总结和分析评价;最后总结了当前研究中存在的问题,其中配置约束的提取是提升当前相关工作的重要基础。
现有的关于配置约束的研究工作主要包括SPEX[6]和EnCore[7]。SPEX首先需要识别源代码中的配置变量,然后用程序分析手段跟踪数据流的每个程序变量与对应的配置参数,并记录所有数据流路径中发现的配置约束。EnCore则从配置数据集的角度,基于预定义的约束模板,分析可能存在的约束。
但是,上述研究工作均存在对应缺陷。SPEX可以基于正则表达式和简单语义判断推断出配置项简单的语法约束,包括是否符合基本数据类型等,但无法分析出配置项上下文语境或其本身语义所产生的约束,没有对枚举型配置项需要满足的约束进行系统的分析和提取,且成功率较低。EnCore方法则由于没有源码的支持,仅通过人工定义,从数据分析角度提取约束,模板的发现是不完备的。另外,SPEX和EnCore只关注Switch-case-statement型的代码段提取枚举配置约束的情况,然而通过调研发现,Switch-case-statement模式在本文调研的开源软件中并不常见。
本文研究发现,枚举类型配置项(与枚举约束一一对应)在开源软件中大量存在,如表1所示;不同类型的配置项(分类同文献[6],见表 2 )带有不同比例的约束。而其中枚举类型全部天然地带有约束,而前人工作对此研究十分简单。
Table 1 Numbers of constraints of enumeration-type configurations in some open source softwares表1 若干开源软件中枚举类型约束总数
开源软件的配置约束具有一定的规律性,本文所提出和实现的面向枚举类型的配置约束提取技术即基于此特性。
Table 2 Constraints ratio of restriction for different data types表2 不同数据类型存在约束的比例情况 %
软件配置的功能主要是作为软件系统功能的调节器,是软件系统中的一种常量。因此,从设计意图上来说,一般配置变量在程序中均应为直接使用,不涉及数据流赋值传播的情况(即将配置赋值给其他变量,再通过被赋值的其他变量进行程序控制)。在本文所调研的开源软件中,绝大部分枚举类型的配置项在源代码中是直接被使用,没有经过二次赋值。并且,枚举型配置变量的取值大多为宏定义类型或者C++中的枚举类型,这样可以很直接地获取配置变量的取值。
另外,通过建立配置项变量映射发现(即建立配置项名与源代码中相应的程序变量名之间的映射关系),目前常用开源软件的配置项映射通常聚集于少量源文件的代码段中,如 PostgreSQL 在“guc.c”文件中完成配置映射,Redis 在“config.c”文件中完成映射。通过统计发现,PostgreSQL软件源码中配置变量使用的分布情况如图1所示,除了配置映射所在源文件外,其他源文件中的配置项出现次数极少,且分布稀疏 。
Figure 1 Positions Distribution of configurations图1 配置变量使用位置分布
基于上述配置映射特征,本文提取的对应配置项变量映射策略如下所示:通过分析配置变量在程序中实际使用的位置,判断其可能的取值(宏或者枚举),通过可能的取值找到其配置项-配置变量映射代码段,在代码段中进行集中的配置约束提取,流程图如图 2 所示。相较于已有研究工作,本文的主要改进在于不需理解代码语义信息,通过提供配置项名即可实现自动化的配置项变量映射特征代码段,从而进一步实现配置约束的提取。
Figure 2 Extraction procedure of constraints for configurations图2 枚举配置变量取值空间的提取流程
3.2.1 枚举配置变量取值提取
本文基于Clang前端的库接口将所有的源代码文件转化为抽象语法树AST(Abstract Syntax Tree),并根据常见的2种配置变量(分支条件和选择条件)使用场景分别进行配置变量取值的提取。
分支条件中的配置变量使用是最普遍的配置变量使用场景,其中的条件语句并非简单的2个变量直接比较,而是存在复杂的逻辑操作判断。因此,本文主要针对2种情况分别使用不同方法规范化条件语句并分析是否存在配置变量的使用。
首先,针对If-Condition类型的条件语句,本文主要使用二叉条件树BCT(Binary Conditional Tree,即树的所有叶节点均为操作数,而非叶节点均为操作符,每个非叶节点只有2个子树)表示 if 语句中的条件表达式,其中叶节点为常量或变量,而非叶节点均为运算符。图3即为1个基本的If语句分支条件的二叉条件树。
Figure 3 An example of BCT图3 BCT示例
基于BCT实现配置相关分支条件中的分析记录具体步骤如下所示:
(1) 基于AST生成分支条件 BCT。基于 Clang 生成的源文件对应的AST,抽取配置相关的分支语句子树,并生成对应 BCT。
(2) 基于预定规则简化BCT。对于生成的 BCT,需要进行合并化简,将表达式中较复杂的形式化简为最简单的等价形式。主要的化简规则包括:
① 常量表达式运算,计算出表达式中的算术运算,例如“4+3”化简为“7”;
② 与配置变量无关的子树统一记为UNKNOWN节点;
③ 消除三元运算符,将三元运算符化简为等价的二元运算符,如从“a?b:c”化简为 “(ab)||(!ac)”。
然后通过遍历整个BCT,查找是否出现了配置变量和宏定义/枚举类型的直接比较,从而判断出1个配置变量的1个可能取值。在图3中,圆框为可以参与计算的叶子节点,方框叶节点分别为配置变量的可能值和配置变量。
除由 if 控制的分支条件外,switch 控制的选择条件也是常用场景之一。其句法结构相对比较简单,不存在复杂的嵌套关系,与分支条件类似,选择条件的配置变量使用分析也可以通过AST分析来实现。一般来说,switch语句的选择条件AST满足图4所示形式,其中方框叶节点分别代表配置变量及其可能的取值。基于上述2种针对不同形式的配置变量使用情况获取,即可获得对应的配置变量使用情况。
Figure 4 BCT Features of ‘switch’ statement图4 switch 语句 BCT 结构特征
3.2.2 枚举配置取值集中出现定位分析
在 PostgreSQL 中配置变量使用位置如图 1 所示,横坐标为文件,纵坐标为出现在该文件的第几行,不同的灰度表示不同配置变量的枚举值的使用位置。直观来看,左下角是集中出现的区域,在实际计算中,以“数据距离不超过5行”作为集中定义区域的判定阈值。
3.2.3 基于AST的启发式枚举配置取值空间提取优化
基于上述方法,如果通过简单的字符串操作及固定的区域判定方法进行枚举配置取值空间的提取,通常会存在一定的假阳性。本文主要通过以下方式进行过滤:首先,本文在调研过程中观察到配置项字符串取值的字符一般为“A~Z”“a~z”“0~9”“-_”几种类型的字符,而大部分假阳性是一些日志提示信息,含有大量的空格、“%”等符号,通过字符取值范围过滤,可以过滤掉绝大多数的假阳性。进一步,本文对初步过滤后仍然存在的假阳性特征进行分析,发现假阳性主要来自于同处在集中定义代码段的其他配置项的取值,以及其他配置项及自身配置项名称本身,这些特征都无法从文本特征进行分析。因此,本文从抽象语法树层面展开分析,通过分析字符串所在的抽象语法树上下文结构来判断其是否为本配置项的1个可能取值。
前文在对AST进行词频统计,分析配置项的所有取值时,可以发现配置项名在每个代码文件出现的频率,进而可以确定取值的集中出现位置及对应源代码文件(见图4)。本文通过查看对应源码文件发现,这些集中定义的代码段,通常情况下为开发人员实现配置项取值空间和配置变量取值空间映射的代码段。
通过进一步分析发现,在不同软件中,实现配置映射的方式有所区别。总的来说可以总结为2种类型:结构体映射和字符串比较。因此,在定位到集中定义代码段之后,针对这2种固定的类型定义规律,可以针对对应特征分别实现提取配置项的值。
首先,针对结构体映射的情况进行处理。配置变量定义于某个特定结构体中,如代码1所示,其第1个成员的第1个子成员表明配置项名称“wal_level”,第2个成员(wal_level)表示其对应配置变量,第4个成员(wal_level_options)代表其映射的结构体,其中存储了所有可能的枚举值,如代码2所示。
代码1 配置结构体
{
{"wal_level", PCG_POSTMASTER,WAL_SETTINGS,
gettext_noop("set the level of information written to the WAL."),
NULL
},
&wal_level,
WAL_LEVEL_MINIMAL,wal_level_options,
NULL,NULL,NULL
}
代码2 映射结构体
/*PostgreSQL-9.3.1 guc.c*/
const struct config_enum_entry wal_level_options[]={
{"minimal",WAL_LEVEL_MINIMAL,false},
{"archive",WAL_LEVEL_ARCHIVE,false},
{"hot_standby", WAL_LEVEL_HOT_STANDBY,false},
{NULL, 0, false}
}
本文通过调研发现,在大多数软件中,配置项和配置变量的取值范围都是通过上述方式定义的,具有一定的普遍性。基于此,本文可以便捷地获取配置项wal_level的取值空间定义结构体wal_level_options及对应的枚举取值空间。
另一方面,在少部分软件中,程序判断配置项枚举值的方式是通过直接调用字符串比较函数,通过比对文件中或者用户在终端输入的配置项值与预设枚举取值的方式为配置变量赋值。如代码3所示,配置项 hostname_lookups 的可能取值为on、off、double,对应配置变量的可能取值为 HOSTNAME_LOOKUP_ON、 HOSTNAME_LOOKUP_OFF、HOSTNAME_LOOKUP_DOUBLE。针对这种情况,同样可以通过抽象语法树的分析获取对应的枚举配置取值空间。
代码3 字符串直接比较
static const char *set_hostname_lookups(cmd_parms *cmd, void *d_, const char *arg)
{
core_dir_config *d=d_;
if (!strcasecmp(arg, "on")){
d→hostname_lookups = HOSTNAME_LOOKUP_ON;
}
else if (!strcasecmp(arg,"off")){
d→hostname_lookups = HOSTNAME_LOOKUP_OFF;
}
else if (!strcasecmp(arg,"double")){
d→hostname_lookups = HOSTNAME_LOOKUP_double;
}
else {
return "parameter must be 'on','off', or 'double'";
}
return NULL;}
综合考虑上述2种情况,本文主要通过以下方法实现AST中的枚举配置取值空间信息的分析和提取:配置项的字符串取值和其对应的程序配置变量取值一般都出现在同一InistListExpr 节点的子树下(如图5所示)。类似地,对于 Apache Httpd 这类软件,一般字符串取值出现在strcasecmp 或者 strcmp 函数调用中,而相应的程序变量取值一般出现在后面的 if 语句块中。针对上述2种情况,本文将其对应的stmt定义为特征 stmt,并针对特征 stmt 的结构特征进行枚举配置取值空间的定位与提取。具体方法为:
(1) 定义2个 AST stmt 的距离为在 AST 上从stmt1 到stmt2的路径步数。
(2) 计算区域中所有字符串距离最近的父特征 stmt 的距离(仅向上访问,若无父 stmt 则距离记为 INF)。
(3) 对于第1类特征 stmt,必须要求父特征 stmt 为同一 stmt,对于第2类,则计算是否共用一个 ifstmt。
(4) 对上述距离进行验证(距离差距不能超过 1),若正确,则提取对应配置取值。
通过对以上步骤编程实现,本文可自动获取枚举类型配置项的取值约束范围。
Figure 5 Example of AST for value space of enumeration-type configuration图5 枚举类型配置取值空间AST示例图
本文首先对4款软件(PostgreSQL,Apache Httpd,Nginx,Redis)的枚举类型配置项数量进行了人工计数,得到实际的数量作为实验评价的标准;然后使用本文的实现自动提取枚举配置项取值空间,对应的提取准确率如表3所示。而在过去已有研究工作中[6],仅对switch语句一种情况下的枚举配置约束进行了提取,且没有给出具体的提取率,本文使用已有方法提取4款开源软件的枚举型配置约束,发现已有研究的提取率在30%~70%,而本文方法则有较大提升,能够处理常见的代码特征如if-else语句,提取率均达到80%以上。
对于枚举型配置项枚举值的提取,本文将准确并完全地提取出某一配置变量的全部枚举值认定为提取成功,有遗漏或者假阳性均为不成功。由此计算得到的提取率如表3所示。
Table 3 Rate of extraction and reason for failure表3 枚举配置取值空间提取准确率
其中,部分实验提取结果如表4所示(以 PostgreSQL-9.3.1 为例):
Table 4 Results of PostgreSQL extraction表4 PostgreSQL中枚举配置取值空间提取结果
表4中,划横线的地方是使用了假阳性检验从而得以除去的取值。从结果来看,它们的确主要来自于其他配置项的取值空间,或者其他配置项的配置名称,此外还有少部分来自于程序中其他语句的一些字符串提示、日志等信息。经过对少部分失败的原因进行分析发现,主要是软件开发人员没有使用规范化的编码模式,如开发人员在配置变量位置使用的是宏定义类型的值,而在配置项集中赋值位置处使用的是整数类型,虽然不影响程序正确性,但属于不良的编码习惯。类似上述原因,导致本文总结的通用模式难以进行正确分析与提取。
在实际生产环境中,前人研究[6]已发现,如果用户错误地设置了配置项的取值,很容易导致系统故障。提取枚举类型配置项的取值空间可以让用户知道哪些值是合法的,从而减轻用户配置软件时的负担,降低故障率。同时,基于提取结果,当出现故障时,开发人员可迅速诊断基于配置的故障原因,避免进行源码分析。
通过对多款常用开源软件的人工分析发现,现有用于提取配置约束条件的研究工作仅能提取少部分特定类型的配置约束,且对于常见的枚举类型配置项约束提取的可用性较弱。基于此,本文基于AST,对6款常用主流开源软件配置处理使用相关源代码进行分析,通过增加对分支条件和选择条件的配置约束分析,设计和实现了一种针对枚举类型的自动配置约束提取方法。实验数据说明,该方法对所选各种软件枚举类型配置空间的提取准确率均超过80%。