使用Csmith自动挖掘编译器的Bug

        编译器作为系统软件,无论在小型还是大型软件系统中都有着相当重要的作用。以C程序为代表的计算机软件都需要通过编译来得到可执行文件,因此由编译器导致的 Bug 会对软件带来灾难性的影响。那么如何挖掘编译器中的bug呢?Csmith是一种广泛使用的编译器验证工具,本文首先介绍用Csmith验证编译器的基本思想,然后演示Csmith的用法,最后展示一个基于Csmith的自动寻找编译器Bug的脚本。

一、用Csmith验证编译器的基本思想

        Csmith论文原文:Finding and understanding bugs in C compilers | Proceedings of the 32nd ACM SIGPLAN Conference on Programming Language Design and Implementation

        Csmith是一种针对编译器的测试工具,它可以随机产生有效的C代码来帮助发现编译器中的Bug。
        Csmith使用差分测试的方法,如下图所示:

        测试的具体步骤如下:

  • 使用Csmith生成大量测试程序。
  • 将同一程序运行在不同的编译器上,或是同一编译器的不同编译选项。
  • 比较编译后运行的结果,如果不同,则说明有错误产生。作者认为多数相同的结果为正确输出,少数的则为错误输出。

​        Csmith会使用C语言的大量特征来生成测试程序,但并不会包含C99标准中的52种未指定行为(J.1节)和191种未定义行为(J.2节),这些行为会让C程序产生不同的解释,因此生成测试程序时要将其避开。

        由Csmith生成的测试程序在编译后运行所输出的内容非常简短,是统计其中各项测试结果所得到的校验和。在最后一步的验证阶段只需要比较校验和即可判断编译器是否有bug。

二、 Csmith源码编译安装以及用法

        在了解使用Csmith的基本思想以后,我们可以做实验来展示使用Csmith的具体过程。

        Csmith源码可以在其官网中下载:Csmith

        将下载的源码包解压,进入解压后的文件夹:

cd csmith-master

        编译安装:

sudo apt install g++ cmake m4
cmake -DCMAKE_INSTALL_PREFIX=~/csmith .
make && make install

        编译安装完成后,首先将Csmith加入启动路径:

export PATH=$PATH:~/csmith/bin

        然后就可以使用Csmith生成测试代码并编译运行:

csmith > random1.c
gcc random1.c -I$HOME/csmith/include -o random1
./random1

        运行结果如下,输出了校验和1A1A31FB。

         在实际应用时,我们会将随机生成的测试程序用不同的编译器(或不同版本)编译,或者使用同一编译器的不同编译选项进行编译,再比较其运行结果,即校验和。

        下面的实验就是将生成的程序分别用gcc和clang进行编译,并将运行结果输出到文本文件中进行查看。

csmith > random2.c
gcc random2.c -I$HOME/csmith/include -o random2_gcc
clang random2.c -I$HOME/csmith/include -o random2_clang
./random2_gcc > gcc_output.txt
./random2_clang > clang_output.txt

        打开上述两个文本文件,其内容如下图所示。可以看到两者运行所输出的校验和是一致的,这种情况就可以认为该测试程序所涉及的部分,gcc与clang都是正确的。

        Csmith还提供了许多选项来自定义生成的测试程序是否包含C语言的指定特征,方便对编译器的某些方面进行针对性的验证。使用csmith -h和csmith -hh命令可以查看各个选项的定义,如下图所示:

        例如用上图中的--argc或--no-argc选项指定生成的测试程序的main函数是否包含argv和argc参数;用--array或--no-array选项可以指定生成的程序是否包含数组。

三、 使用shell脚本自动进行测试

        在上一节的实验中,我们验证了Csmith可以生成随机C程序,并在编译运行后得到运行结果的校验和进行比较。但是在Csmith的实际应用中,需要大量执行测试用例的生成、编译、运行,以及比较其校验和这些动作。为了使用Csmith进行自动化的编译器验证,可以使用shell脚本。

        我编写了一个进行自动测试的脚本test.sh:

num_pass=0;
num_failed=0;
#创建文件夹用于存储校验和不同的测试程序
mkdir -p failed_cases

#循环进行测试
for ((i=0; i<5; i=i+1))
do
  #使用Csmith生成测试程序
  csmith > test.c;
  #使用gcc4.8编译测试程序
  gcc-4.8 -I$HOME/csmith/include test.c -w -o test_gcc48;
  #使用gcc默认版本编译测试程序,在我的环境中为gcc7.5
  gcc -I$HOME/csmith/include test.c -w -o test_gcc75;
  #使用clang编译测试程序
  clang -I$HOME/csmith/include test.c -w -o test_clang;

  #运行三个编译版本,存储结果
  output_of_gcc48=`./test_gcc48`;
  output_of_gcc75=`./test_gcc75`;
  output_of_clang=`./test_clang`;

  echo $output_of_gcc48;
  echo $output_of_gcc75;
  echo $output_of_clang;

  #判断校验和是否相同
  if [[ $output_of_gcc48 =~ $output_of_clang ]] && [[ $output_of_gcc48 =~ $output_of_gcc75 ]] ;then

  ((num_pass++));
  echo "Pass";

  else

  ((num_failed++));
  #将校验和不同的测试程序拷贝到先前创建的文件夹中
  cp test.c ./failed_cases/test${num_failed}.c
  echo "Failed";

  fi

done

#统计测试结果
echo "Results:";
echo "pass times: ${num_pass}";
echo "failed times: ${num_failed}";

        该脚本对三个编译器进行了验证,分别是gcc4.8,gcc7.5以及clang。验证过程共循环进行了5次,每次循环都使用了Csmith新生成的测试程序。

        该脚本中,若出现了校验和不同的情况,则将该次测试所生成的c程序文件拷贝到failed­_cases文件夹中。因为现代编译器的质量都已经很高,发生错误的概率极小,也就是说,出现failed的用例不会太多,因此可以测试完成后,可以针对每一个failed用例,人工确认是哪个编译器编译的程序与另外两个不同,也就是被判定未存在Bug的编译器,然后通过分析failed程序的内容来定位该编译器的Bug。

        运行结果示例如下:

         该结果显示五次验证中各编译器的输出一致,测试通过,可以认为这三款编译器都是正确的。只需要修改循环次数,就可以实现更多次验证,让该脚本长时间连续运行,比如循环成千上万,甚至上百万次,就可以覆盖编译器内部逻辑尽可能多的分支,以发现编译器中的潜在Bug。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值