Clang 编译数据库信息扩展
1. 背景介绍
LLVM + Clang 是目前使用非常火的编译器,其优点这里不再赘述,总之,该工具成为当前做编译器、程序分析等相关工作和研究的企业和学者的首选。说 LLVM + Clang 养活了全球一半做编译器和程序分析的企业和高校团队也不为过。
因为 LLVM 和 Clang 的优势,在 SAST 领域,Clang 和 LLVM 已经成为很多 SAST 工具的基础框架,目前已经延伸出了相当多的优越的 SAST 工具,包含但不仅限于:
Clang Static Analyzer,clang 原生提供的基于符号执行的检查工具
Clang-Tidy,clang 原生提供的基于 ASTMatcher 的检查工具,主要是风格检查
Klee,基于 llvm bc 的基于符号执行的程序验证工具
Phasar,基于 llvm bc 的基于 IFDS 实现的程序分析框架,其中包含了很多其他的分析框架
IKOS,基于 llvm bc 的基于抽象解释的程序分析工具
SVF,基于 llvm bc 的程序分析工具,有非常优越地指针分析的能力
...
但是,为了能够应用这些检查工具,就要求代码必须使用 Clang 进行构建。在这个过程中,Clang 的编译数据库[1]起到了非常重要的作用。目前也有很多工具支持导出相关的编译数据库,例如:
bear[2]
compiledb[3]
cmake、ninja、bazel 等相关构建工具可以直接导出相关的编译数据库
但是,为了更好地使用上面的 SAST 工具,Clang 的原始的编译数据库中的信息,是不够的,需要结合更多地信息,才能完成分析(所有的基于编译的 SAST 工具都有类似的需求,并不仅仅是 Clang,本文中论述的各种扩展的编译信息需求,来自于对 Coverity 的编译适配的理解)。
下面,我们就结合实际的情况,给大家介绍一下还需要哪些编译信息。
2. 扩展信息介绍
2.1 环境变量信息
从我们的开发实践中,发现存在部分的编译器或者源代码在编译过程中,依赖特定的环境变量。从这个角度讲,我们需要将这些环境变量的信息收集起来。Coverity 在编译捕获过程中,会记录每一个识别到的编译器特定命令的环境变量信息。
在下面,我们获取编译器其他信息时,也需要这些环境变量信息。
2.2 编译器的默认 target、bit
比如下面的例子,原始命令为:
我们知道,clang 的编译数据库会比较忠实地体现编译命令的信息。上面的编译命令,记录到 clang 编译数据库,也是一样的信息。那么,上面的信息足够使用了吗?
我们知道,c/c++编译的结果文件,有特定的运行环境,比如运行在 x86-64 上面,还是运行在 arm 上面,是运行在 32 位系统上,还是 64 位系统上面。这些在编译的时候,就需要确定,如果在编译命令中不指定,就会有默认值,比如默认的 target 和 位数。但是不同的编译器的默认值并不一定是 clang 编译器的默认值。比如 交叉编译器(aarch64_be-linux-gnu-gcc、mips-linux-gcc 等),我们看名字就知道,如果这些编译器编译,即使没有默认的 target 和位数,应该默认值不会和 clang 一样。
那么如何确认编译器默认的 target 和 位数 呢?
对于 target 信息,可以使用 -dumpmachine 确认,如下:
如上,我们可以看到,gcc 在我们的这个操作系统中,默认是 x86_64-linux-gnu.
对于 bits 信息,可以采用编译一个测试文件,然后查看测试文件的位数确定,如下:
我们可以看到,在默认编译参数编译完成后,是 ELF 64-bit 的文件,如果额外添加了 -m32,就是 ELF 32-bit 的文件了。
2.3 编译器系统头文件路径
不同的 c/c++编译器,都会在标准 c/c++语法的基础上,扩展自己的语法,或者是新增一些函数、类型等(扩展的非标准语法不再本文讨论范围,这里只讨论新增的函数、类型)。新增的函数、类型等,一般是定义在编译器的系统头文件路径下面的。因此,我们在捕获到编译命令之后,还需要获取特定的系统头文件路径信息。
系统头文件路径,和编译器的 target、bits 等信息有关,获取系统头文件里面,可以在命令中添加 -E -v 来获取,对应的系统头文件路径,可以在 “#include <...> search starts here:”和 “End of search list.”之间解析。如下:
使用 gcc 默认的命令,结果如下:
在 gcc 默认的命令的基础上,添加 -m32,结果如下:
我切换成 arm 交叉编译器,使用默认命令,结果如下:
如上面的三个例子:我们展示了不同编译器、或者相同编译器的不同参数,生成的系统头文件路径的差异。在实际使用中,要获取特定编译命令的系统头文件路径,一般可以反向去掉肯定不影响系统头文件路径的参数之后,直接添加 -E -v 运行程序即可(可以不使用源文件,使用一个空源文件替代,如果捕获到的命令过多,可以通过缓存,减少执行量)。
2.4 预定义宏信息
预定义宏和系统头文件类似,都会影响到源文件的编译。需要获取到原始编译器的预定义宏,原因在于两点:① 编译命令中的编译参数会通过影响预定义宏来对源码生效;② 不同的编译器中,支持的预定义宏不同,因此有可能存在从原始编译器到 clang 后,有宏缺失的情况。因此,我们需要获取原始编译器的预定义宏,传递给 clang。
获取预定义宏的方式也非常简单,在编译命令中添加 -E -dM 参数即可。
下面,我们也同样通过三种情况举例:
使用 gcc 默认的命令,结果如下:
在 gcc 默认的命令的基础上,添加 -m32,结果如下:
我切换成 arm 交叉编译器,使用默认命令,结果如下:
如上,前面两个,我们都使用 gcc 的情况下,不同的位数,会导致类型的长度不同,比如 FLOAT80,在 64 位是 16,在 32 位是 12。在 arm 交叉编译器情况下,存在一些宏是 x86 下所没有的,比如 __ARM_x 类型。
2.5 链接信息
链接命令信息,更多地可以为跨文件分析提供辅助。虽然在没有链接命令的情况下,也可以通过启发式的方法获取 CallGraph 的信息,比如通过头文件、mangle function 等,但是,在多 target 的情况下,可能会存在错误链接的情况。因此,链接命令可以辅助执行更加准确的跨文件分析(一定程度上,链接命令确实不是必须的)。
2.6 编译器版本、软链接、wrapper 等信息
即使是相同的编译器,版本不同,支持的语法也有所差异,最后需要适配的工作也不同。比如我们针对 gcc 4.8.0 适配的内容,放到 gcc 7.3.0 中,可能就会有问题。因此编译器版本的获取和适配,是工程化的需要。编译器版本获取方式如下:
通过预定义宏,也可以获取到版本的信息,如下(还能获取到一些更详细的信息):
对于 wrapper,主要是有些编译器名,看着是编译器,但是实际上是脚本,并不是真正的编译命令,需要加以区分。
对于软链接,主要目的是可以减少重复编译命令获取,减少不必要的错误。
3. 总结
如果要实现一款工业级的基于编译的面向 C/C++的 SAST 工具,编译命令信息获取必不可少。本文介绍了除了 clang 编译数据库 中的信息外,其他的一些必要的信息,帮助提高编译准确率,完善工程实现。
参考
[1] https://clang.llvm.org/docs/JSONCompilationDatabase.html
[2] https://github.com/rizsotto/Bear
[3] https://github.com/nickdiego/compiledb
版权声明: 本文为 InfoQ 作者【maijun】的原创文章。
原文链接:【http://xie.infoq.cn/article/07bb0781a985b359b9adbb798】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
评论