Clang学习笔记
Clang 学习笔记
什么是 Clang
Clang 是 LLVM 的前端,可以分析 C 语言家族的所有源代码。
Clang 是如何工作的
- 预处理:展开所有的宏,将代码解析成抽象语法树
Clang AST
几乎所有的编译器 & 静态分析工具 都是用 AST 来表示原本的代码。
一般来说, Clang AST 由两个类组成:Decl & bStmt。
FunctionDecl:函数原型或者函数定义
BinaryOperator:二元表达式,(a+b)
CallExpr:函数调用,foo(x)
怎么使用 Clang
Clang Plugin
使用 Clang Plugin,写出的代码本身就是插件。在使用 Clang Plugin 的时候,我们不可以保留不同文件之间的全局信息和其他横跨多个文件的上下文信息。插件的运行是通过传递命令行参数给 build system(Clang/Make),在源文件分析前后,我们不能进行任何的 custom task。插件的存在形式是一个动态链接库。
LibTooling(Clang Tool)
使用 LibTooling,代码本身就是一个正常的 C++ 程序,已正常的 main() 函数作为入口。LibTooling 一般用来把程序构建的过程和程序的分析过程分开。针对每个源程序都会生成分析代码以及对应的 AST,但同时还可以维护不同源代码文件的全局信息。由于程序有 main()函数,我们还可以在分析源代码前后运行其他的任务。
LibClang
当我们想要一个稳定的 API 的时候,LibClang 是一个很好的选择,Clang 变化很块,如果使用 Plugin 或者 LibTooling,我们可能需要更新代码以应对 Clang 的变化。但如果需要在 C++ 以外的地方调用 Clang 的 API 的时候,必须要使用 LibClang。
LibClang 不可以使用完整的 AST(只能使用高层次的 AST),而另外两个选择(Plugin 与 LibTooling)则可以。如果还是无法选择,推荐使用 LibTooling interface,简单好用。LibTooling能够像 Plugin 一样完整的使用 AST,同时还不会丢掉源代码的全局信息。另外设置 LibTooling 比 Plugin 更容易。
开始使用 Clang
编译安装
- LLVM
1 | |
- Clang
1 | |
- Compile-RT
1 | |
- 构建
1 | |
LibTooling
假如我们要分析一个 C 语言文件如下:
1 | |
我们想要对上述函数进行重构。
- 把函数名do_math 改为 addFive
- 把所有的对 do_math 的调用都改为 addFive
- 把返回值改为 val
从 main 函数开始
1 | |
- 首先设置一个 ClangTool,将命令行参数:op.getCompilations() 以及源文件列表 op.getSourcePathList() 传给它,然后就运行这个工具就好了。
- LibTooling 的优点在于,可以再工具运行前后做其他的事情,比如说打印出修改前后的代码以及统计函数的个数。
创建 FrontendAction
现在创建自己的 FrontendAction,创建的原因:想要分析 test.c 的 AST 表示。
1 | |
这里不是很复杂,我们创建了一个ASTFrontendAction的子类,改写了CreateASTConsumer函数以返回我们自己的ASTConsumer。我们还将指向CompileInstance的指针传入,因为这里面包含很多我们需要分析的上下文信息。
构建 ASTConsumer
ASTConsumer 由Clang parser产生的AST。我们可以任意地重载ASTConsumer的成员函数,这样解析AST后我们的代码就可以被调用。首先,我们重载函数HandleTopLevelDecl,这在Clang解析完顶级的声明(像全局变量,函数定义等)后就可以被调用了。
1 | |
以上代码使用了ExampleVisitor(见下文),来访问整个源文件顶级声明(top-level declaration)的AST节点。对于test.c而言,两个FunctionDecl将会被访问,do_math()以及main()。
更好的 ASTConsumer 实现
重载HandleTopLevelDecl()意味着每当一个新的Decl出现的时候,函数中的代码就会被调用,而不是等到整个源文件被解析完成后。从parser的角度看,当访问do_math()的时候,它将完全不知道main()的存在,也就是说我们不能access到当前分析的函数之后的函数。
但是,这个功能很重要!
不过,ASTConsumer还有一个更好的函数用来重载,HandelTranslationUnit(),该函数只有在整个文件都解析完才被调用。这样的话,一个translation单元就是一整个源文件。ASTContext类用来表示那个源文件的AST,并且包含许多很有用的成员(去读文档吧!)。
所以,下面的代码重载了HandelTranslationUnit():
1 | |
大多数情况下,我们都应该使用HandelTranslationUnit(), 尤其在使用RecursiveASTVisitor的时候。
创建一个 RecursiveASTVisitor
前面两部分只不过在设置架构,现在到了正文部分了。RecursiveASTVisitor是一个特别有用的类,使用它可以访问任意类型的AST节点,比如FunctionDecl以及Stmt, 只要重载那个函数(比如VisitFunctionDecl以及VisitStmt)就可以了。当然,其它AST类也同样适用这样的规则。Clang提供了一个官方的文档,虽然很短,但是很全面。
像Visit..(表示Visit任意节点的函数,如VisitStmt)这样的函数,我们必须返回true以继续遍历AST或者返回false以终止遍历,退出Clang。我们不可以直接调用Visit..,而是应该调用TraverseDecl(正如我们前面的那个例子一样),调用Visit..函数则是在背后调用的。
由于我们只需要改写函数定义和一些statement,我们只需要重载VisitFunctionDecl和VisitStmt。下面是部分代码:
1 | |
以上的代码引入了Rewriter类,可以让我们对源代码进行修改,这在代码重构或者小规模的代码修改里面很常见。我们还在main()函数的末尾用它打印出了修改后的代码。
使用Rewriter意味着我们需要找到正确SourceLocation来插入或者替换相关的代码。同时,我们还使用了dyn_cast,来检查Stmt st是一个ReturnStmt还是CallExpr。而errs()是一个stderr流,在LLVM/Clang里面打印debug信息
写一个更具体的Visit..函数
除了更一般化地重载VisitStmt,我们可以更具体化地重载VisitReturnStme以及VisitCallExpr。VisitReturnStme和VisitCallExpr都是Stmt的子类。这就是Clang AST和RecursiveASTVisitor的美妙之处:我们可以选择一般化或者是具体化,下面就是代码
1 | |