作者:范洪宇 时间:2017年8月9日 联系:348043747@qq.com 环境:Win10专业版 QtCreator5.5 地点:北京清华大学东配楼11-321室 功能:对指定输入的C语言函数进行变异,尽量保证产生可以编译通过的变种程序
一、程序功能说明 主程序: File_Mutater.c
程序输入: original.c:这个文件是需要变种的原始的C程序文件,名称是固定的,当作输入的源程序文件必须修改成这个名称,这个是相对路径下的名称,需要放在IDE默认的工作区内,否则就需要写绝对名称。该文件无需进行任何预处理操作,从开始到最后结束都只进行读入操作,不会修改这个文件里面的数据。 test.c: 这个文件中存放从original.c文件中自行提取出来的函数,可以是一个也可以是多个,注意仅包括函数的定义,其余的东西不能写入这个文件。变种只会从这个文件中限定的函数中进行,不会越界。文件中各个函数之间用单独的一行隔开,该行内只添加'$'一个字符。如果只有一个函数或者是最后一个函数,在最后另起一行,该行内不需要再写‘$’符,只需要添加一个'@'字符。程序遇到'@'标识符就会结束词法分析工作。
程序输出: mutate_0.c-mutate_8.c 一共九个文件。我用来测试时,程序默认产生变种文件的数量是9个,可以在程序内修改计数器的值来进行修改,变量名称为mutation_count,只需要修改这个变量的值就可以修改文件数目,其余无需修改。 log.txt 日志文件,其中主要记录各个阶段的具体执行情况,包括发生变种的函数在原始文件中的位置,即行数。变种期间记录各个变种发生的位置(具体到行),方式(变种方式下面会涉及),以及文本上具体的变化(变量替换的具体值)。此外还有文件变种结束后数据写回是否成功的记录。
功能需求: 目前变种器暂时支持单操作符变种、多操作符变种、常数数值变种、变量名称变种四种变种方式,每种方式都有程序已经定义好的规则,程序运行时每次变种也是随机选择变种模式。original.c中存放原始文件,其余变种文件每次仅仅改变一行,比如说mutate_0文件与original.c文件只有一行是不同的,mutate_1.c与mutate_0.c文件仅仅有一行是不同的,与original.c文件有两行是不同的。依次类推,九个变种文件就最多有9行是不同的。不需要担心变种之后与之前的文件重复,程序中都进行了相关处理,避免了这些情况。当然也存在一种可能,程序中并没有足够的条件来产生相应数量的变种,程序中也进行了处理,可以正常退出。
可用性分析: 目前可以进行一些C程序文件的变种需求,但是还有很多地方需要改进,再次列出: 1.作者水平有限,对于同等的功能可能有更高效便捷的实现方式但是我没有想到,愿意虚心学习,有想法或者是使用过程中有错误和疑问(很可能会有各种各样的错误)欢迎与我联系,邮箱是常用的。 2.变种的情况并不完全,如果想要全面的变种,只进行词法分析是远远不够的,每个token序列本身的信息远远不够用,因为同样的字符可能在不同环境下有不同的含义,况且自己实现的词法分析工具本身就具有局限性,所以对于变种的类型的支持仅限于上面提及的那些,还有很多种方式没有实现,已经实现的也有较大的局限性。 3.变种的正确性有待检验,之前我想直接生成编译一定可以通过的变种文件,但是发现我无法想全所有的可能情况,或者说有些想到也没有可行的解决办法,所以最后暂时选择放弃。但是已经尽最大的可能保证变异文件的编译正确性,自己定义了很多规则已经避免了绝大多数的编译错误,但是肯定不可能完全避免,所以使用时可以尽可能多产生一些变种文件,进行编译,最后根据编译的结果在其中选取可以正确编译的文件来进行使用。 4.相信使用的人肯定都有一些疑问,对于一些极端情况的处理怎么解决,比如选取了单操作符变异,但是文件中并没有单操作符或者是没有实现的条件。为了解决这种可能,程序内部有计数器,当多次尝试失败后会自动转换变异模式进行其他的类型的变异。如果程序本身就比较小,不满足变种的条件,则程序在有限次尝试之后会自动退出,不会无限运行,同时已经产生的变种还是会正常写入文件,可以拿来使用,具体的情况会记录在日志文件中方便查询和修改。 5.实际上在自己测试修改的过程中也出现了程序非正常退出的现象,也有很多其他错误还没有出现,可能会在后续出现。因为能想到的太有限了,所以很多地方需要完善和修改。
二、详细说明 1. 程序的前一小段定义了一系列的常量或者是常量数组,下面将进行介绍: const string Type_Kinds[]:这个常量数组中存放了所有的Token类型,包括标识符,关键字等等; int lines_count:记录了test.c文件中函数一共有多少行 int Lines_Mark[]:因为两次变种不允许发生在同一行,所以需要给所有的行进行标记,有些行在扫描的时候就禁止变种,例如带有函数名称的一行。或者是已经发生了变种,就要置1防止后来在同一行再次变异。 int line_base[]:用来记录test.c中的文件在original.c中的位置,可能在test.c中有多个函数,所以就需要一个数组来存放。 int line_base_count:用来给上面这个数组计数的。 static const int mutation_count:用来设置产生多少个变种文件。 string Mutation_File[mutation_count], string Final_File[mutation_count]:用来存放中间文件以及最终文件的文件名称,在后面进行初始化。 int keyword_num:用来记录keywords的数量。 const string KeyWord[]:中间存放了可能涉及到的所有的C语言的关键字,实际上最规范的C语言的关键字只有30多个,但是因为可能涉及到驱动程序等,有很多关键字只是对前面关键字的缩写。但是还必须得加上,否则进行词法分析的时候可能会出现错误。 int change_count:记录Changable数组中的元素的个数。 const string Changable[]:对于标识符,只有满足其类型属于Changable数组才可以,否则是不能进行name_mutation操作的。 const char Spec_Symbol[]:这个常量数组中存放的是所有的单操作符。 const string Spec_Operator[]:这个常量数组中存放的是可能涉及到的双(多)操作符。 const string Type_Mutation[]:这个常量数组中存放的是目前可以支持的所有的变种的模式。 int mutation_mode:上面紧邻的数组的索引项。
struct Token;每个Token是进行词法分析的最小的单位,其中包含了每个单位的一些最基本的属性: int pId代表着该Token序列属性的id,实际上就是前面定义的Type_Kinds[]数组的索引值; string type就是每个Token属性的具体值,即type=Type_Kinds[pId]; string value中存放的是每个Token的实际的值,即从源文件中读入的信息; string kind一般只对于属性为Identifier的单元才有用,其值可能是int,char等等,是为了方便后面进行变种的,只有两个单元的kind属性相同,才可能发生替换; int lines_base是用来记录该Token单元所属的函数,在原始文件中定义的位置,若两个Token单元是来自同一个函数,则该值是相同的; int lines_mark是用来记录该Token单元在test.c文件中的位置的,后续会用到。 结构体定义中有相应的构造方法以及set()函数和show()函数,就不在详细说明,相信读者可以理解。
bool IsEnd(char);该函数的功能判断输入的字符是否是结束符,是就返回true,否则就返回false。 bool IsLetter(char);该函数的功能是判断输入字符是否是字母或者是下划线,是就返回true,否则就返回false。 bool IsNumber(char);该函数的功能是判断输入字符在数值上是否是数字,是就返回true,否则就返回false。该函数判断的只是一位字符,至于负数以及多位数字的情况后面会有解决办法。 bool IsSpecSymbol(char):该函数的功能是判断输入的字符是否是单操作符以及其他的一些提前定义好的特殊字符,如果是的话就返回true,否则就返回false。 bool IsSpecOperator(char):该函数的功能是判断输入的字符串是否是双操作符,类似于"&&",如果是就返回true,否则返回false。 bool IsKeyWord(string);该函数的功能是判断输入的字符串是否是关键字,是就返回true,否则就返回false。 bool IsSpace(char);bool IsTab(char);bool IsEnter(char)分别是判断输入的字符是否是空格,行制表符以及回车,是就返回true,否则就返回false。 bool IsChangable(string):这个函数的功能是判断输入的字符串(预处理过,肯定是Identifier类型的值)是否是可以发生变种的,如果是就返回true,否则就返回false。
bool open_original_file(string):显而易见,该函数的功能就是打开文件,文件名称是作为参数输入到函数内部的。打开成功就返回true,否则返回false。 bool write_mutate_file(string):显而易见,该函数的功能就是将变种之后的Token_List[]中的值写回到文件中,生成相应的变种文件。成功写回返回true,否则返回false。
void init_line_base():test.c中存放的是提前选取出来需要变种的函数,original.c中存放的是原始的全部c程序,该函数的功能是定位前者的函数在后者文件中的位置,为了后续的定位工作等提供保障。 bool init():该函数完成了一些初始化的工作,包括打开日志文件,初始化输出文件名称等。相应操作全部执行成功就返回true,否则就返回false。
int scanner():这个函数实现的就是词法分析工作以及一些附加的工作,总体的程序就是一个大的while循环,每次读入一个字符,根据其类型进行相应的操作,最后结果就是扫描整个程序,并且将Token_List[]中的相应属性填充,方便后续使用。
紧接着的是int name_sel_random(int);int single_op_sel_random(int);int double_op_sel_random(int);int dec_sel_random(int);四个函数,这四个函数的功能相近,输入都是int类型的变量,具体说就是一个index值。拿name_sel_random(int)函数来举例: 当变种器扫描Token_List[]的某一项,检测发现其满足name_mutataion的条件,此时会有一个数组下标值记录的是该token项在Token_List[]中的位置,这个下标值就是上面函数的输入。为什么需要这个函数呢?因为每一行只允许变种一次,也就是说一行中可能有多个token项是具备进行name_mutation的条件的,但是扫描改行时遇到第一个具备条件的就进行变种了,后面的永远没机会,为了避免这种情况,就需要调用name_sel_random(int)。这个函数统计这一行内所有满足name_mutaiotn的token项的下标,然后采用随机筛选的方式选择一个进行变种,所以改程序返回值就是在其中选取的下标。如果该行只有一个token满足条件那当然还是返回那个,但是如果有多个满足条件就可以为这些项提供公平的机会进行变种,丰富变种的可能性。 其余三个函数的功能和name_sel_random(int)的功能是相同的,只是针对的对象不同。single_op_sel_random(int)是针对单操作符的,double_op_sel_random(int)是针对双操作符的,dec_sel_random(int)是针对常数数值变化的。变种器内根据变种的类型自动调用这几类不同的随机选择函数。
下面介绍几种变种器,其实就实现来说,对源文件只进行简单的词法分析是不够的,但是进行语法分析的工作了巨大,暂时没有时间精力去做,这就导致实现的正确性无法保证,典型的就是同一个字符在不同的环境下可能有不同的含义。'*'可能是乘号,也可能是指针类型变量标识;'-'可能是减号,也可能是负号;还有很多情况词法分析解决不了,所以自己定义相应的规则。 int dec_mutater():常数的变种器,这类变种限制较少,扫描扫描Token_List[]这个数组,对于其中满足条件的常数数字选择一个进行变种,改变其数值,目前定义的规则是在原始数值附近摆动。成功变种就返回数值1,如果扫描结束仍未进行变种就返回0代表变种失败。 int single_op_mutater():单操作符变种器.该函数的实现逻辑是扫描Token_List[]这个数组,对于其中满足条件的单操作符选择一个进行变种,变种的规则参考代码。成功变种就返回数值1,如果扫描结束仍未进行变种就返回0代表变种失败。 int double_op_mutater():双操作符的变种器,该函数的实现逻辑是扫描Token_List[]这个数组,对于其中满足条件的双操作符选择一个进行变种,变种的规则参考代码。成功变种就返回数值1,如果扫描结束仍未进行变种就返回0代表变种失败。 int name_mutater():标识符变种器。该函数的实现逻辑是扫描Token_List[]这个数组,对于其中满足条件的标识符选择一个进行变种,变种的规则就是在同类型的标识符中随机替换。成功变种就返回数值1,如果扫描结束仍未进行变种就返回0代表变种失败。
bool file_mutater():这个函数功能是根据提前设定好的变种次数进行循环,每次生成一种变种,并且将变种之后的文件入到temp_mutate_0.c-temp_mutate_9.c文件中,作为中间暂存的文件,其行数和test.c中是一样的,即只包含变种的函数。
void file_write():这个函数的功能是根据生成好的temp_mutate系列的文件以及原始的original文件,生成完整的变种文件,之前涉及到的记录函数位置等变量在这里都可以用上。其逻辑实现就是找到变种的函数在original.c中间的位置,然后每次读一个temp_mutate系列文件,将原始文件函数位置覆盖,最终就可以生成所需要的全部的变种文件,mutate_系列,其行数和original.c是相同的。
在主函数中依次调用函数init()进行初始化,调用sanner()进行词法分析,调用file_mutater()进行变种生成中间文件,然后调用file_writer()将中间文件转换成最终的文件并保存,这期间log穿插记录,方便查询和定位。