赞
踩
Clang
工具和LibTooling
你会见到有很多工具库利用Clang
前端,为了不同的目的
而操作C/C++
程序.特别地,它们都依赖一个Clang
的LibTooling
库,让你可编写独立工具
.
此时,利用Clang
的解析
能力,可设计一个完全属于你自己的工具,让用户直接调用你的工具
,而不是编写一个插件
以适应Clang
的编译.
工具可在Clang
额外工具包中找到.
一般,从构建
脚本(如Makefile
)调用编译器,用一系列参数
配置它,使之恰当
地使用项目头文件和定义
.这些参数让前端正确地分词
并解析
输入的源码文件
.
然而,这里,学习独立运行
而不按Clang
编译管线的一部分的独立工具
.因此,理论上,需要一个专门
的脚本,用正确参数
运行工具来处理每个源码文件
.
如,如下显示了Make
所用的完整命令行
,它调用编译器
以从LLVM
库构建
文件:
$ /usr/bin/c++ -DNDEBUG -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS
-D__STDC_LIMIT_MACROS -fPIC -fvisibility-inlines-hidden -Wall -W -Wnounused-
parameter -Wwrite-strings -Wmissing-field-initializers -pedantic
-Wno-long-long -Wcovered-switch-default -Wnon-virtual-dtor -fno-rtti
-I/Users/user/p/llvm/llvm-3.4/cmake-scripts/utils/TableGen -I/Users/
user/p/llvm/llvm-3.4/llvm/utils/TableGen -I/Users/user/p/llvm/llvm-3.4/
cmake-scripts/include -I/Users/user/p/llvm/llvm-3.4/llvm/include -fnoexceptions
-o CMakeFiles/llvm-tblgen.dir/DAGISelMatcher.cpp.o -c /Users/
user/p/llvm/llvm-3.4/llvm/utils/TableGen/DAGISelMatcher.cpp
为了让工具
方便处理源码文件
,任意使用LibTooling
的项目都按输入
接受命令数据库
.该命令数据库
为具体项目
的每个源文件
设置正确的编译器参数
.
为了更容易,如果用-DCMAKE_EXPORT_COMPILE_COMMANDS
参数调用CMake
,它就会为你生成该数据库
文件.
如,假设期望对Apache
项目的一个具体源码文件
运行基于LibTooling
的工具.为了无需输入
准确的编译器参数
以正确地解析
该文件,可用CMake
如下生成一个命令数据库
:
$ cd httpd-2.4.9
$ mkdir obj
$ cd obj
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../
$ ln -s $(pwd)/compile_commands.json ../
与用CMake
构建Apache
所用的构建
命令类似,但是并不实际构建
它,-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
参数指示CMake
用编译器参数
生成一个用来编译每个Apache
源文件的JSON
文件.
要创建到该JSON
文件的链接,把它保存在Apache
源码的根目录
中.然后,运行LibTooling
程序去解析一个Apache
源文件时,它搜索父目录
以找到compile_commands.json
,并用得到的正确参数
去解析
该文件.
可选地,如果不想在运行工具前构建
编译命令数据库
,也可用(--)
双短线,直接
传递用来编译该文件的编译器命令
.
项目不需要很多参数
来编译时,这是有用
的.如下:
$ my_libtooling_tool test.c -- -Iyour_include_dir -Dyour_define
clang-tidy
工具所有其它Clang
工有与clang-tidy
类似,从而可愉快
地探索它们.
clang-tidy
是一个基于Clang
的linter
.一般,linter
是个暴露不符合
最优形式代码的分析代码工具
.它可检查特定特征,如:
1,代码是否适应不同的编译器
2,代码是否按特定习语或编码
惯例
3,代码是否滥用语言特性
而导致漏洞
就clang-tidy
而言,该工具可运行两类
检测器:原始的Clang
静态分析器的检查器
和专门为clang-tidy
编写的检查器.
尽管可运行静态
分析检查器,注意clang-tidy
和其它基于LibTooling
的工具是基于源码分析
的,这和前面描述的复杂
静态分析引擎是相当不同
的.
这些检查
只是遍历ClangAST
,而不是模拟
程序运行,它们也更快.不同于Clang
静态分析器的检查,为clang-tidy
编写的检查
一般以检查是否符合特定编码习惯
为目标.
特别地,它们检查LLVM
和Google
编码惯例,还有其它一般
检查.
如果要遵守特定编码惯例
,clang-tidy
非常有用,用它定期
检查代码.甚至可花时间配置
它,这样可在一些文本编辑器
里直接运行
.
clang-tidy
检查你的代码此例,演示如何用clang-tidy
检查前面写的代码.为静态分析器写了一个插件
,如果想把该检查器
提交到官方的Clang
源码树,要严格地遵循LLVM
编码惯例.
现在检查
是否真遵循它了.一般clang-tidy
命令行接口
如下:
$ clang-tidy [options] <source0> [... <sourceN>] [-- <compiler command>]
可小心地通过-checks
参数中的名字
,激活每个检查器
,但也可利用*通配符
选择许多有相同开始子串
的检查器
.
需要关闭
检查器,就在检查器名字
前缀加个短划线
.如,如果想运行所有属于LLVM
编码惯例的检查器,就如下:
$ clang-tidy -checks="llvm-*" file.cpp
注意,只有安装了Clang
连同Clang
额外工具代码仓库,才能运行工具
,它与Clang
树是分开的.
因为代码是和Clang
一起编译的,需要一个编译器数据库
.开始生成它.进入LLVM
源码所在的目录,用如下
创建兄弟目录来存放CMake
文件:
$ mkdir cmake-scripts
$ cd cmake-scripts
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../llvm
注意,如果遇见指向前面创建的检查器
代码的未知源错误
的错误,需要用你的检查器源文件
名字更新CMakeLists.txt
文件.
用如下命令行
编辑该文件,然后再次运行CMake
:
$ vim ../llvm/tools/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
然后,在LLVM
根目录中,创建指向编译器命令数据库
文件的快捷方式.
$ ln -s $(pwd)/compile_commands.json ../llvm
现在,终于可运行clang-tidy
了:
$ cd ../llvm/tools/clang/lib/StaticAnalyzer/Checkers
$ clang-tidy -checks="llvm-*" ReactorChecker.cpp
应该看到许多关于检查器
所包含的头文件
的抱怨,它们没有严格地遵循LLVM
规则,它要求每个namespace
结尾的大括号有注释(见http://llvm.org/docs/CodingStandards.html#namespace-indentation
).
好消息是,包括头文件
的我们的工具代码
,没有违反这些规则
.
ClangModernizer
ClangModernizer
是革命性的独立工具
,它帮助人们用最新
的如C++11
标准重写旧的C++
代码.它执行如下变换以达到该目标:
1,转换循环
变换:把旧的C
风格的for(;;)
循环转变为更新
的基于区间
的for(auto&...:..)
形式的循环
2,用nullptr
变换:把旧的C
风格的空针
的NULL
或0常数
转变为更新的nullptr
,C++11
关键字
3,用auto
变换:用auto
关键字来声明一些类型
,这提高了代码可读性
4,添加override
变换:为覆盖
基类函数的虚成员函数
声明,加了override
修饰
5,按值
转递变换:用复制的常
引用替换按值传递
6,替换auto_ptr
变换:用std::unique_ptr
替换已过时
的std::auto_ptr
源到源
的变换工具
利用了ClangLibTooling
基础设施,ClangModernizer
是个很好的示例.要想使用它,观察下面的模板:
$ clang-modernize [<options>] <source0> [... <sourceN>] [-- <compiler command>]
注意,如果除了源码文件名
,不提供额外选项
,该工具就会直接全部变换
源文件.用-serialize-replacements
参数强制
把补丁
写到磁盘
,这样可先阅读
它们,再应用
它们.
Clang
应用替代如何协调大型代码基
的源到源
的变换.如,当分析不同
翻译单元时,可能会多次分析
同一个头文件
.
或许,序化
替换建议
,并把它们写到
文件.第二个
工具负责读入
这些建议文件
,丢弃
冲突的和重复
的建议,并对源文件
应用这些替换
建议.
这是Clang应用替换
的目的,它生来就是用来帮助ClangModernizer
修复大型
代码基的.
ClangModernizer
和Clang应用替换
,前者生产替换建议
,后者消费
这些建议,它们都利用clang::tooling::Replacement
类的序化
版本.
序化
用到了YAML
格式.
代码版本
工具所用的补丁文件
,正好是修改建议
的序化格式
,但是Clang
开发者选择使用YAML
,直接利用Replacement
类的序化
,避免解析
补丁文件.
因此,Clang应用替换
工具不打算成为一个通用代码补丁工具
,而是个专门负责处理
依赖使用工具API
的Clang
工具所作出的修改
.
注意,如果编写源到源
的变换工具
,仅当想协调
多个修改建议
以消除重复修改
时,才要用Clang应用替换
工具.
否则,可直接简单
修改源文件.
为了看清Clang应用替换
如何工作,首先要用ClangModernizer
,强制序化
它的修改建议
.假设想转换下面的C++
源文件,让它使用更新的C++
标准:
int main() {
const int size = 5;
int arr[] = {1,2,3,4,5};
for (int i = 0; i < size; ++i) {
arr[i] += 5;
}
return 0;
}
根据ClangModernizer
的用户手册,转换该循环
让它用新的auto
迭代器是安全
的.为此,要用ClangModernizer
的循环变换
:
$ clang-modernize -loop-convert -serialize-replacements test.cpp
--serialize-dir=./
指定用当前目录
来存放替换文件
的最后参数
是可选
的.如果不指定
它,工具会创建Clang应用替换
会用的临时目录
.
因为会转存所有替换文件
到当前目录
,可直接分析
生成的YAML
文件.可以当前目录
为它唯一
参数,简单地运行clang-apply-replacements
:
$ clang-apply-replacements ./
注意,运行
该命令后,如果得到此错误信息:迭代.目录错误,太多符号链接级...
,可用/tmp
目录来保存替换文件
,来重试最后
两个命令.
或,可创建新的目录
以存放
这些文件.
除了简单示例
,还可用这些工具,处理大型代码基
.因此,Clang应用替换
不会问问题,只是直接开始解析
指定目录
中的所有YAML
文件,分析
并转换.
甚至可指定
具体源文件的编码
标准.这就是-style=<LLVM|Google|Chromium|Mozilla|Webkit>
参数的目的.
它是LibFormat
库提供的,允许任意重构工具
按某种具体格式或编码
惯例编写新代码
.
ClangFormat
这是正确
代码吗?是的.访问http://www.ioccc.org/2013/birken
可下载它.现在,演示ClangFormat
会怎样处理此代码.
$ clang-format -style=llvm obf.c --
ClangFormat
不只是个工具,还是一个库,LibFormat
,它格式化
代码以适应
某种编码惯例
.
这样,如果新建工具恰好会生成C
或C++
代码,你可专注
你的项目,而把格式化
等交给ClangFormat
.
Modularize
先介绍链接器如何处理
符号导入.在编译并汇编gamelogic.c
之后,将得到叫gamelogic.o
的目标文件,它的符号表
显示,num_lives
符号占用4个字节
,其它翻译单元
可用它.
$ gcc -c gamelogic.c -o gamelogic.o
$ readelf -s gamelogic.o
Num | Value | Size | Type | Bind | Vis | Index | Name |
---|---|---|---|---|---|---|---|
7 | 00000000 | 4 | OBJECT | GLOBAL | DEFAULT | 3 | num_lives |
该表
只显示了关注
的符号,省略
了其它符号.readelf
工具仅在依赖ELF
格式的Linux
平台上可用.
如果使用其它平台
,可用objdump -t
打印符号表.这样理解该表:在表中按第7个
位置赋值num_lives
符号,占用相对3索引
的段(.bss
段)的首地址(零)
.
反之,.bss
段持有用0
初化的数据实体
.为了验证节名
和索引
对应关系,用readelf -S
或objdump -h
打印节头信息
.
从该表,还知道,num_lives
符号是全局可见的(globalbind)
包含4个字节
的一个(数据
)对象
.
类似,screen.o
文件的符号表
会显示该翻译单元依赖
属于另一个翻译单元
的num_lives
符号.要想分析screen.o
,可用之前gamelogic.o
上的相同命令
:
$ gcc -c screen.c -o screen.o
$ readelf -s screen.o
Num | Value | Size | Type | Bind | Vis | Index | Name |
---|---|---|---|---|---|---|---|
10 | 00000000 | 0 | NOTYPE | GLOBAL | DEFAULT | UND | num_lives |
表项类似exporter
中的那个,只是它的信息更少.它没有size
和type
,及显示哪个ELF
节包含该符号.index
标记为UND
(未定义),表示该翻译单元是importer
.
如果把该翻译单元
包含进最终
程序,链接
必须解决该依赖
,否则会失败.
链接器按输入
接收这两个文件
,用在exporter
中请求的符号地址
给importer
打补丁.
$ gcc screen.o gamelogic.o -o game
$ readelf -s game
Num | Value | Size | Type | Bind | Vis | Index | Name |
---|---|---|---|---|---|---|---|
60 | 0804a01c | 4 | OBJECT | GLOBAL | DEFAULT | 25 | num_lives |
现在,该值
反映了加载程序
时,变量的完整虚内存地址
,向importer
的代码段
提供了符号位置
,完成了不同翻译单元
之间的导出-导入
协议.
可知:在链接器
这里,在多个翻译单元
间共享实体
是简单而高效
的.
处理目标文件
很简单,但是这并不反映在语言
中.不同于链接器
,在导入
实现中,编译器不能仅依靠导入实体的名字
,因为它要验证
,该翻译单元的语义
是否违反语言
的类型系统,即它需要知道num_lives
是否为整数.
因此,除了导入实体的名字
,编译器还期望得到类型信息
.而C
是通过请求头文件
解决该问题.
头文件
包含实体名字及类型信息,并被不同
翻译单元使用.该模型中,导入者
用include
指令加载
要导入的实体的类型信息
.
然而,头文件
用法不止于此,事实上,不只是声明
,它还可引入任意的C
或C++
代码.
C/C++
预处理器的问题与Java
语言中的import
指令不同,Include
指令的语义不要求为编译器提供导入符号
的必要信息,而是展开
成更多需要解析的C
或C++
代码.
它由预处理器
实现,它直接在实际编译
前复制并修补代码,相当于是个文本处理
工具.
大型编译器项目往往用预编译
头文件方法来避免重复词法解析
每个头文件
,如,Clang
的PCH
文件.然而,这仅缓解了问题,因为如有新的宏定义
,编译
仍需要再解释整个头文件
,这影响当前翻译单元
如何解释
该头文件.
如,假设游戏如下实现gamelogic.h
:
#ifdef PLATFORM_A
extern uint32_t num_lives;
#else
extern uint16_t num_lives;
#endif
当screen.c
包含该文件
时,导入的num_lives
实体类型依赖于,是否在screen.c
翻译单元的环境
中定义了PLATFORM_A
宏.
而且,对另一个
翻译单元,该环境
不是必须相同
的.这样每当不同
翻译单元包含它们时,就强制编译器加载头文件
的额外代码
.
为了控制C/C++
导入及如何编写库接口
,模块提出了一个描述此接口
的新方法
.此外,Clang
已支持模块
.
不是包含头文件
,翻译单元
可导入一个定义清晰无歧义的接口
来使用具体库的模块.import
指令无需给翻译单元
注入额外的C
或C++
代码,会加载
给定库导出的实体
.
Clang
提供了一个叫-fmodules
的额外标志,包含允许模块化库
的头文件时,它直接按模块的import
指令解释include
.
解析模块
头文件时,Clang
会用干净预处理器状态
,生成一个自身实例
,来编译
这些头文件,并按二进制形式
缓存编译结果
,以加速
编译后续依赖相同头文件
构成特定模块
的翻译单元.
因此,已成为模块
一部分的头文件
,不能再依赖先前
定义的宏,或其它预处理器
先前的状态.
为了把一组头文件
映射为具体模块
,可定义一个叫module.modulemap
的单独文件
,来提供此信息.应该在定义库的API
的头文件的相同目录
中放置该文件
.
如果有该文件
,且用-fmodules
调用Clang
,编译就会使用模块
.
扩展简单游戏示例
以使用模块
.假设游戏API
是由gamelogic.h
和screenlogic.h
两个头文件
定义的.game.c
主文件从这两个
文件导入
实体.游戏API
源码的内容如下:
gamelogic.h
文件的内容:
extern int num_lives;
screenlogic.h
文件的内容:
extern int num_lines;
gamelogic.c
文件的内容:
int num_lives = 3;
screenlogic.c
文件的内容:
int num_lines = 24;
且在游戏API
中,每当用户包含gamelogic.h
头文件时,他也会想要包含screenlogic.h
,以在屏幕
上打印游戏数据
.
因此,要结构化
逻辑模块以表达该依赖
.因此,如下定义项目的module.modulemap
文件:
module MyGameLib {
explicit module ScreenLogic {
header "screenlogic.h"
}
explicit module GameLogic {
header "gamelogic.h"
export ScreenLogic
}
}
module
关键字,后面跟着期望识别
它的名字.示例中,叫它MyGameLib
.每个模块可有封闭
的子模块
.
explicit
关键字告诉Clang
,仅当显式
包含它的其中一个头文件
时,才导入该子模块
.可列举
很多头文件
来表示单个子模块
,但是这里,对每个子模块
只用到一个头文件
.
因为使用
模块,可利用它们让生活更简单,让include
指令更简单.注意,在GameLogic
子模块域,在ScreenLogic
子模块名字
前面,使用export
关键字,表示每当用户导入GameLogic
子模块时,也让ScreenLogic
的符号可见.
为了说明,如下编写game.c
即该API
的用户,
//文件:`game.c`
#include "gamelogic.h"
#include <stdio.h>
int main() {
printf("lives= %d\nlines=%d\n", num_lives, num_lines);
return 0;
}
注意,用到了在gamelogic.h
中定义的num_lives
,和在screenlogic.h
中定义的num_lines
,它们都不是显式包含的.
然而,当clang
以-fmodules
参数解析
该文件时,它会转换第一个include
指令,为导入GameLogic
子模块的效果,使得可见在ScreenLogic
中定义的符号
.
因此,如下
可正确
地编译该项目
:
$ clang -fmodules game.c gamelogic.c screenlogic.c -o game
另一方面,不调用
模块系统的Clang
报告导致缺失符号定义
:
$ clang game.c gamelogic.c screenlogic.c -o game
screen.c:4:50: error: use of undeclared identifier 'num_lines'; did you
mean 'num_lives'
printf("lives= %d\nlines=%d\n", num_lives, num_lines);
^~~~~~~~~
num_lives
然而,记住尽量让项目可移植
.
最好,简化库API
的使用,并加速
依赖很多公共头文件
的翻译单元的编译
.
Modularize
在模块
框架中,独立编译每个子模块
的头文件.
如,不能导入很多项目
依赖在包含指令
前的其它文件
中定义的宏
,来使用
模块.
modularize
的目的就是帮助
完成此任务.它分析一系列头文件
,报告是否有重复
的变量定义,宏定义
,或可能依赖预处理器
状态,求值为不同
结果的宏定义.
它帮助你诊断
根据一系列头文件创建模块
时遇见的常见的阻碍
.它还检测
项目是否在,强制
编译器在与模块
不兼容的不同
域中解释包含文件
的名字空间块
中使用include
指令.
因此,头文件
中定义的符号
必须不依赖包含头文件
的环境.
Modularize
要使用modularize
,必须提供会被相互检查
的头文件的列表
.继续游戏项目
示例,写个叫list.txt
的新文本文件
,如下:
gamelogic.h
screenlogic.h
然后,以该列表
为参数,简单运行modularize
:
$ modularize list.txt
如果改变其中一个头文件
,用其他符号定义
相同符号,modularize
会报告有不安全
的模块行为
,在为项目写入module.modulemap
文件前,应该修复
头文件.
修复
头文件时,记住每个头文件
应该尽量
独立,不应根据包含此头文件
的文件所定义的值
,修改定义的符号
.
如果依赖此行为,应把该头文件
分成两个或更多
,使用一组特定宏
时,每个定义编译器
看到的符号
.
Clang
工具模块映射检查器,检查module.modulemap
文件,确保它涵盖
了目录中的所有头文件
.对前面的示例,如下调用它:
$ module-map-checker module.modulemap
讨论了使用include
指令对比
模块,预处理器
是个麻烦.
PPTrace
Clang
中的lexer
类,按词
识别大块的文本,之后由解析器
来解释.
lexer
没有语义
信息,也不关心预处理器负责的,包含的头文件和宏展开
.
Clang
的pp-trace
独立工具输出预处理
过程的踪迹
.通过实现clang::PPCallbacks
接口的回调函数
,实现此功能
.
首先按预处理器
的观察者注册
自己,然后启动Clang
来分析输入文件
.对预处理器的每个动作
,如解释#if
指令,导入模块,包含头文件等等,该工具
都会打印消息.
考虑下面专门编写的"helloworld"C
程序:
#if 0
#include <stdio.h>
#endif
#ifdef CAPITALIZE
#define WORLD "WORLD"
#else
#define WORLD "world"
#endif
extern int write(int, const char*, unsigned long);
int main() {
write(1, "Hello, ", 7);
write(1, WORLD, 5);
write(1, "!\n", 2);
return 0;
}
前面代码的第一行,用了总是取值
为假的#if
预处理指令,强制编译器直到下个#endif
指令,忽略源码块
内容.
接着,用#ifdef
指令,检查是否定义了CAPITALIZE
宏.根据是否定义了该宏,定义WORD
宏为大写的WORD
串,或小写的word
串.
最后,代码
调用了一系列write
系统调用,以在屏幕上输出消息.
类似
运行其它Clang
源码分析独立工具
,运行pp-trace
:
$ pp-trace hello.c
结果是一系列实际处理源码
前,关于宏定义
的预处理器事件
.最后事件涉及具体文件
,如下:
- Callback: If Loc: "hello.c:1:2" ConditionRange: ["hello.c:1:4", "hello.c:2:1"] ConditionValue: CVK_False - Callback: Endif Loc: "hello.c:3:2" IfLoc: "hello.c:1:2" - Callback: SourceRangeSkipped Range: ["hello.c:1:2", "hello.c:3:2"] - Callback: Ifdef Loc: "hello.c:5:2" MacroNameTok: CAPITALIZE MacroDirective: (null) - Callback: Else Loc: "hello.c:7:2" IfLoc: "hello.c:5:2" - Callback: SourceRangeSkipped Range: ["hello.c:5:2", "hello.c:7:2"] - Callback: MacroDefined MacroNameTok: WORLD MacroDirective: MD_Define - Callback: Endif Loc: "hello.c:9:2" IfLoc: "hello.c:5:2" - Callback: MacroExpands MacroNameTok: WORLD MacroDirective: MD_Define Range: ["hello.c:13:14", "hello.c:13:14"] Args: (null) - Callback: EndOfMainFile
第一个事件
涉及的第一个#if
预处理器指令.该区域触发了三次回调:If,Endf
,和SourceRangeSkipped
.注意,未处理且跳过了#include
指令.
类似地,看到WORD
宏相关的事件:IfDef,Else,MacroDefined
,和Endif
.最后,pp-trace
通过MacroExpands
事件报告用到了WORD
宏,然后到达了文件尾
,并调用了EndOfMainFile
回调函数.
预处理后,前端
的下一步是词法分析和解析
.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。