当前位置:   article > 正文

2312llvm,08clang工具上_llvm clang8

llvm clang8

Clang工具和LibTooling

你会见到有很多工具库利用Clang前端,为了不同的目的而操作C/C++程序.特别地,它们都依赖一个ClangLibTooling库,让你可编写独立工具.

此时,利用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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为了让工具方便处理源码文件,任意使用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 ../
  • 1
  • 2
  • 3
  • 4
  • 5

与用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
  • 1

clang-tidy工具

所有其它Clang工有与clang-tidy类似,从而可愉快地探索它们.

clang-tidy是一个基于Clanglinter.一般,linter是个暴露不符合最优形式代码的分析代码工具.它可检查特定特征,如:
1,代码是否适应不同的编译器
2,代码是否按特定习语或编码惯例
3,代码是否滥用语言特性而导致漏洞

clang-tidy而言,该工具可运行两类检测器:原始的Clang静态分析器的检查器和专门为clang-tidy编写的检查器.
尽管可运行静态分析检查器,注意clang-tidy和其它基于LibTooling的工具是基于源码分析的,这和前面描述的复杂静态分析引擎是相当不同的.

这些检查只是遍历ClangAST,而不是模拟程序运行,它们也更快.不同于Clang静态分析器的检查,为clang-tidy编写的检查一般以检查是否符合特定编码习惯为目标.

特别地,它们检查LLVMGoogle编码惯例,还有其它一般检查.

如果要遵守特定编码惯例,clang-tidy非常有用,用它定期检查代码.甚至可花时间配置它,这样可在一些文本编辑器直接运行.

clang-tidy检查你的代码

此例,演示如何用clang-tidy检查前面写的代码.为静态分析器写了一个插件,如果想把该检查器提交到官方的Clang源码树,要严格地遵循LLVM编码惯例.
现在检查是否真遵循它了.一般clang-tidy命令行接口如下:

$ clang-tidy [options] <source0> [... <sourceN>] [-- <compiler command>]
  • 1

可小心地通过-checks参数中的名字,激活每个检查器,但也可利用*通配符选择许多有相同开始子串检查器.

需要关闭检查器,就在检查器名字前缀加个短划线.如,如果想运行所有属于LLVM编码惯例的检查器,就如下:

$ clang-tidy -checks="llvm-*" file.cpp
  • 1

注意,只有安装了Clang连同Clang额外工具代码仓库,才能运行工具,它与Clang树是分开的.

因为代码是和Clang一起编译的,需要一个编译器数据库.开始生成它.进入LLVM源码所在的目录,用如下创建兄弟目录来存放CMake文件:

$ mkdir cmake-scripts
$ cd cmake-scripts
$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ../llvm
  • 1
  • 2
  • 3

注意,如果遇见指向前面创建的检查器代码的未知源错误的错误,需要用你的检查器源文件名字更新CMakeLists.txt文件.
用如下命令行编辑该文件,然后再次运行CMake:

$ vim ../llvm/tools/clang/lib/StaticAnalyzer/Checkers/CMakeLists.txt
  • 1

然后,在LLVM根目录中,创建指向编译器命令数据库文件的快捷方式.

$ ln -s $(pwd)/compile_commands.json ../llvm
  • 1

现在,终于可运行clang-tidy了:

$ cd ../llvm/tools/clang/lib/StaticAnalyzer/Checkers
$ clang-tidy -checks="llvm-*" ReactorChecker.cpp
  • 1
  • 2

应该看到许多关于检查器所包含的头文件的抱怨,它们没有严格地遵循LLVM规则,它要求每个namespace结尾的大括号有注释(见http://llvm.org/docs/CodingStandards.html#namespace-indentation).
好消息是,包括头文件的我们的工具代码,没有违反这些规则.

重构工具

ClangModernizer

ClangModernizer是革命性的独立工具,它帮助人们用最新的如C++11标准重写旧的C++代码.它执行如下变换以达到该目标:

1,转换循环变换:把旧的C风格的for(;;)循环转变为更新的基于区间for(auto&...:..)形式的循环
2,用nullptr变换:把旧的C风格的空针NULL0常数转变为更新的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>]
  • 1

注意,如果除了源码文件名,不提供额外选项,该工具就会直接全部变换源文件.用-serialize-replacements参数强制补丁写到磁盘,这样可先阅读它们,再应用它们.

Clang应用替代

如何协调大型代码基源到源的变换.如,当分析不同翻译单元时,可能会多次分析同一个头文件.

或许,序化替换建议,并把它们写到文件.第二个工具负责读入这些建议文件,丢弃冲突的和重复的建议,并对源文件应用这些替换建议.

这是Clang应用替换的目的,它生来就是用来帮助ClangModernizer修复大型代码基的.
ClangModernizerClang应用替换,前者生产替换建议,后者消费这些建议,它们都利用clang::tooling::Replacement类的序化版本.

序化用到了YAML格式.

代码版本工具所用的补丁文件,正好是修改建议序化格式,但是Clang开发者选择使用YAML,直接利用Replacement类的序化,避免解析补丁文件.

因此,Clang应用替换工具不打算成为一个通用代码补丁工具,而是个专门负责处理依赖使用工具APIClang工具所作出的修改.

注意,如果编写源到源变换工具,仅当想协调多个修改建议以消除重复修改时,才要用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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

根据ClangModernizer的用户手册,转换该循环让它用新的auto迭代器是安全的.为此,要用ClangModernizer循环变换:

$ clang-modernize -loop-convert -serialize-replacements test.cpp
--serialize-dir=./
  • 1
  • 2

指定用当前目录来存放替换文件最后参数可选的.如果不指定它,工具会创建Clang应用替换会用的临时目录.

因为会转存所有替换文件当前目录,可直接分析生成的YAML文件.可以当前目录为它唯一参数,简单地运行clang-apply-replacements:

$ clang-apply-replacements ./
  • 1

注意,运行该命令后,如果得到此错误信息:迭代.目录错误,太多符号链接级...,可用/tmp目录来保存替换文件,来重试最后两个命令.

或,可创建新的目录存放这些文件.

除了简单示例,还可用这些工具,处理大型代码基.因此,Clang应用替换不会问问题,只是直接开始解析指定目录中的所有YAML文件,分析并转换.

甚至可指定具体源文件的编码标准.这就是-style=<LLVM|Google|Chromium|Mozilla|Webkit>参数的目的.

它是LibFormat库提供的,允许任意重构工具按某种具体格式或编码惯例编写新代码.

ClangFormat

代码

这是正确代码吗?是的.访问http://www.ioccc.org/2013/birken可下载它.现在,演示ClangFormat会怎样处理此代码.

$ clang-format -style=llvm obf.c --
  • 1

ClangFormat不只是个工具,还是一个库,LibFormat,它格式化代码以适应某种编码惯例.
这样,如果新建工具恰好会生成CC++代码,你可专注你的项目,而把格式化等交给ClangFormat.

Modularize

链接器工作

先介绍链接器如何处理符号导入.在编译并汇编gamelogic.c之后,将得到叫gamelogic.o的目标文件,它的符号表显示,num_lives符号占用4个字节,其它翻译单元可用它.

$ gcc -c gamelogic.c -o gamelogic.o
$ readelf -s gamelogic.o
  • 1
  • 2
NumValueSizeTypeBindVisIndexName
7000000004OBJECTGLOBALDEFAULT3num_lives

该表只显示了关注的符号,省略了其它符号.readelf工具仅在依赖ELF格式的Linux平台上可用.

如果使用其它平台,可用objdump -t打印符号表.这样理解该表:在表中按第7个位置赋值num_lives符号,占用相对3索引的段(.bss段)的首地址(零).
反之,.bss段持有用0初化的数据实体.为了验证节名索引对应关系,用readelf -Sobjdump -h打印节头信息.

从该表,还知道,num_lives符号是全局可见的(globalbind)包含4个字节的一个(数据)对象.

类似,screen.o文件的符号表会显示该翻译单元依赖属于另一个翻译单元num_lives符号.要想分析screen.o,可用之前gamelogic.o上的相同命令:

$ gcc -c screen.c -o screen.o
$ readelf -s screen.o
  • 1
  • 2
NumValueSizeTypeBindVisIndexName
10000000000NOTYPEGLOBALDEFAULTUNDnum_lives

表项类似exporter中的那个,只是它的信息更少.它没有sizetype,及显示哪个ELF节包含该符号.index标记为UND(未定义),表示该翻译单元是importer.
如果把该翻译单元包含进最终程序,链接必须解决该依赖,否则会失败.

链接器按输入接收这两个文件,用在exporter中请求的符号地址importer打补丁.

$ gcc screen.o gamelogic.o -o game
$ readelf -s game
  • 1
  • 2
NumValueSizeTypeBindVisIndexName
600804a01c4OBJECTGLOBALDEFAULT25num_lives

现在,该值反映了加载程序时,变量的完整虚内存地址,向importer代码段提供了符号位置,完成了不同翻译单元之间的导出-导入协议.

可知:在链接器这里,在多个翻译单元共享实体简单而高效的.

前端对应部分

处理目标文件很简单,但是这并不反映在语言中.不同于链接器,在导入实现中,编译器不能仅依靠导入实体的名字,因为它要验证,该翻译单元的语义是否违反语言的类型系统,即它需要知道num_lives是否为整数.

因此,除了导入实体的名字,编译器还期望得到类型信息.而C是通过请求头文件解决该问题.

头文件包含实体名字及类型信息,并被不同翻译单元使用.该模型中,导入者include指令加载要导入的实体的类型信息.

然而,头文件用法不止于此,事实上,不只是声明,它还可引入任意的CC++代码.

依赖C/C++预处理器的问题

Java语言中的import指令不同,Include指令的语义不要求为编译器提供导入符号的必要信息,而是展开成更多需要解析的CC++代码.

它由预处理器实现,它直接在实际编译前复制并修补代码,相当于是个文本处理工具.

大型编译器项目往往用预编译头文件方法来避免重复词法解析每个头文件,如,ClangPCH文件.然而,这仅缓解了问题,因为如有新的宏定义,编译仍需要再解释整个头文件,这影响当前翻译单元如何解释该头文件.

如,假设游戏如下实现gamelogic.h:

#ifdef PLATFORM_A
extern uint32_t num_lives;
#else
extern uint16_t num_lives;
#endif
  • 1
  • 2
  • 3
  • 4
  • 5

screen.c包含该文件时,导入的num_lives实体类型依赖于,是否在screen.c翻译单元的环境中定义了PLATFORM_A宏.

而且,对另一个翻译单元,该环境不是必须相同的.这样每当不同翻译单元包含它们时,就强制编译器加载头文件额外代码.

为了控制C/C++导入及如何编写库接口,模块提出了一个描述此接口新方法.此外,Clang已支持模块.

理解模块

不是包含头文件,翻译单元可导入一个定义清晰无歧义的接口来使用具体库的模块.import指令无需给翻译单元注入额外的CC++代码,会加载给定库导出的实体.

Clang提供了一个叫-fmodules的额外标志,包含允许模块化库的头文件时,它直接按模块的import指令解释include.

解析模块头文件时,Clang会用干净预处理器状态,生成一个自身实例,来编译这些头文件,并按二进制形式缓存编译结果,以加速编译后续依赖相同头文件构成特定模块的翻译单元.

因此,已成为模块一部分的头文件,不能再依赖先前定义的宏,或其它预处理器先前的状态.

使用模块

为了把一组头文件映射为具体模块,可定义一个叫module.modulemap单独文件,来提供此信息.应该在定义库的API的头文件的相同目录中放置该文件.
如果有该文件,且用-fmodules调用Clang,编译就会使用模块.

扩展简单游戏示例以使用模块.假设游戏API是由gamelogic.hscreenlogic.h两个头文件定义的.game.c主文件从这两个文件导入实体.游戏API源码的内容如下:

gamelogic.h文件的内容:

extern int num_lives;
  • 1

screenlogic.h文件的内容:

extern int num_lines;
  • 1

gamelogic.c文件的内容:

int num_lives = 3;
  • 1

screenlogic.c文件的内容:

int num_lines = 24;
  • 1

且在游戏API中,每当用户包含gamelogic.h头文件时,他也会想要包含screenlogic.h,以在屏幕上打印游戏数据.

因此,要结构化逻辑模块以表达该依赖.因此,如下定义项目的module.modulemap文件:

module MyGameLib {
    explicit module ScreenLogic {
    header "screenlogic.h"
  }
  explicit module GameLogic {
    header "gamelogic.h"
    export ScreenLogic
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

注意,用到了在gamelogic.h中定义的num_lives,和在screenlogic.h中定义的num_lines,它们都不是显式包含的.
然而,当clang-fmodules参数解析该文件时,它会转换第一个include指令,为导入GameLogic子模块的效果,使得可见在ScreenLogic中定义的符号.
因此,如下正确地编译该项目:

$ clang -fmodules game.c gamelogic.c screenlogic.c -o game
  • 1

另一方面,不调用模块系统的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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

然而,记住尽量让项目可移植.

最好,简化库API的使用,并加速依赖很多公共头文件的翻译单元的编译.

理解Modularize

模块框架中,独立编译每个子模块的头文件.

如,不能导入很多项目依赖在包含指令前的其它文件中定义的,来使用模块.

modularize的目的就是帮助完成此任务.它分析一系列头文件,报告是否有重复变量定义,宏定义,或可能依赖预处理器状态,求值为不同结果的宏定义.

它帮助你诊断根据一系列头文件创建模块时遇见的常见的阻碍.它还检测项目是否在,强制编译器在与模块不兼容的不同域中解释包含文件名字空间块中使用include指令.

因此,头文件中定义的符号必须不依赖包含头文件的环境.

使用Modularize

要使用modularize,必须提供会被相互检查头文件的列表.继续游戏项目示例,写个叫list.txt新文本文件,如下:

gamelogic.h
screenlogic.h
  • 1
  • 2

然后,以该列表为参数,简单运行modularize:

$ modularize list.txt
  • 1

如果改变其中一个头文件,用其他符号定义相同符号,modularize会报告有不安全模块行为,在为项目写入module.modulemap文件前,应该修复头文件.

修复头文件时,记住每个头文件应该尽量独立,不应根据包含此头文件的文件所定义的值,修改定义的符号.

如果依赖此行为,应把该头文件分成两个或更多,使用一组特定宏时,每个定义编译器看到的符号.

模块映射检查器

Clang工具模块映射检查器,检查module.modulemap文件,确保它涵盖了目录中的所有头文件.对前面的示例,如下调用它:

$ module-map-checker module.modulemap
  • 1

讨论了使用include指令对比模块,预处理器是个麻烦.

PPTrace

预处理器

Clang中的lexer类,按词识别大块的文本,之后由解析器来解释.

lexer没有语义信息,也不关心预处理器负责的,包含的头文件和宏展开.

Clangpp-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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

前面代码的第一行,用了总是取值为假的#if预处理指令,强制编译器直到下个#endif指令,忽略源码块内容.
接着,用#ifdef指令,检查是否定义了CAPITALIZE宏.根据是否定义了该宏,定义WORD宏为大写的WORD串,或小写的word串.
最后,代码调用了一系列write系统调用,以在屏幕上输出消息.
类似运行其它Clang源码分析独立工具,运行pp-trace:

$ pp-trace hello.c
  • 1

结果是一系列实际处理源码前,关于宏定义预处理器事件.最后事件涉及具体文件,如下:

- 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30

第一个事件涉及的第一个#if预处理器指令.该区域触发了三次回调:If,Endf,和SourceRangeSkipped.注意,未处理且跳过了#include指令.

类似地,看到WORD宏相关的事件:IfDef,Else,MacroDefined,和Endif.最后,pp-trace通过MacroExpands事件报告用到了WORD宏,然后到达了文件尾,并调用了EndOfMainFile回调函数.

预处理后,前端的下一步是词法分析和解析.

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/秋刀鱼在做梦/article/detail/805325
推荐阅读
相关标签
  

闽ICP备14008679号