LLVM 是一套编译器基础设施项目,为自由软件,以 C++ 写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端和后端。它是为了任意一种编程语言而写成的程序,利用虚拟技术创造出编译时期、链接时期、运行时期以及「闲置时期」的最优化。它最早以 C/C++ 为实现对象,而目前它已支持包括 ActionScript、Ada、D 语言、Fortran、GLSL、Haskell、Java 字节码、Objective-C、Swift、Python、Ruby、Crystal、Rust、Scala 以及 C# 等语言。
(摘录自中文维基百科,有改动)
这篇博文是我学习 LLVM 编译器架构的编译器前端 Clang 的笔记。
该笔记的侧重点在如何使用 Clang AST 相关的 API 进行静态源码分析工具的开发,而非如何使用 Clang 系列二进制工具。
参考资料
课程 Slides
Clang AST 相关
- Clang - An Introduction
- Clang AST 基础学习
- Introduction to the Clang AST
- 2013 EuroLLVM Developers’ Meeting: “The Clang AST - a tutorial”
RecursiveASTVisitor
相关
- How to write RecursiveASTVisitor based ASTFrontendActions.
- clang::RecursiveASTVisitor< Derived > Class Template Reference
ASTMatcher
相关
- Clang ASTMatcher 基础学习
- Matching the Clang AST
- Tutorial for building tools using LibTooling and LibASTMatchers
- AST Matcher Reference
入门 Clang AST
Clang 是 LLVM 编译器框架的前端部分。Clang 首先运行预处理器以进行宏展开,然后将解析源代码 (词法分析、语法分析等) 并生成 AST (Abstract Syntax Tree,抽象语法树)。与 C/C++ 源代码相比,Clang AST 提供了更加便于分析和操作的程序表示形式,同时还具有便于找到 AST 节点所对应的源代码行列数的特性。事实上,Clang 使用的各种数据结构 (AST、CFG (Control Flow Graph,控制流图) 等) 都能轻易地转换回源代码,因此 Clang 特别适合用于进行静态代码分析、代码重构等工作。
如果需要在源代码层级上进行分析和修改,Clang 是比 LLVM (编译器后端) 更好的选择,因为使用 LLVM 进行分析需要使用与汇编代码类似的 LLVM IR (Intermediate Representation,中间表示)。
通过简单案例学习 Clang AST
1 | $ cat test.cc |
可以看出:
- 一个翻译单元 (Translation Unit) 的顶层节点是
TranslationUnitDecl
。 - 在这个例子中,第一个来自用户代码的节点是
FunctionDecl
。 f
的函数体是一个复合语句CompoundStmt
,其子节点是变量result
的声明,以及返回语句ReturnStmt
。
通过课程 Slides 学习 Clang AST
这份 Clang Tutorial Slides 来自 KAIST (韩国科学技术院) 的 Moonzoo Kim 教授的课程 CS492A Automated Software Analysis, Fall 18,内含详细的解释和丰富的图例,非常推荐阅读。
Clang AST 的类型
Clang AST 中大部分类型都可以根据其名称得知其含义。也可以在搜索引擎中搜索 “clang functiondecl” 等,以找到对应类型的详细文档。
核心基本类型
下面是 Clang AST 的三大核心基本类型,以及它们的子类的例子:
Decl
(声明)FunctionDecl
(函数声明)VarDecl
(变量声明)
Stmt
(语句)CompoundStmt
(复合语句)BinaryOperator
(二元运算符)Expr
(表达式)CallExpr
(函数调用表达式)CastExpr
(类型转换表达式)
Type
(类型)PointerType
(指针类型)
ASTContext
在一个翻译单元中,所有有关 AST 的信息都在类 ASTContext
,包括:
- 标识符表
SourceManager
- AST 的入口节点:
TranslationUnitDecl* getTranslationUnitDecl()
胶水类型 (Glue Classes)
DeclContext
- 包含其他
Decl
的Decl
需要继承此类。
- 包含其他
TemplateArgument
- 模板参数的访问器。
NestedNameSpecifier
QualType
- Qual 是 qualifier 的意思,将 C++ 类型中的
const
等拆分出来,避免类型的组合爆炸问题。
- Qual 是 qualifier 的意思,将 C++ 类型中的
访问 AST 的方法
Clang 主要提供了 2 种对 AST 进行访问的类:RecursiveASTVisitor
和 ASTMatcher
。
RecursiveASTVisitor
建议结合 How to write RecursiveASTVisitor based ASTFrontendActions. 中的代码样例来学习 RecursiveASTVisitor
,并尝试使用 cmake 编译并运行样例程序。
- 特性
- 由用户所关心的类型触发 (例如可以访问所有
Stmt
)。 - 无法充分利用 AST 的上下文信息 (无法利用节点之间的关系来筛选节点)。
- 由用户所关心的类型触发 (例如可以访问所有
- 使用方法
TraverseDecl(Decl *x)
用于遍历以x
为根的 AST。它将自动调用TraverseFoo(Foo *x)
,进而调用WalkUpFromFoo(x)
,然后递归地以前序或后序的方式深度优先遍历x
的子节点。TraverseStmt(Stmt *x)
和TraverseType(QualType x)
函数的功能类似。WalkUpFromFoo(Foo *x)
并不尝试访问x
的子节点,而是向上搜索节点x
的类型层级,直到达到 AST 的核心基本类型之一 (Stmt
/Decl
/Type
) 。它首先调用WalkUpFromBar(x)
(Bar
是Foo
的直接父类 (如果存在)),然后调用VisitFoo(x)
。VisitFoo(Foo *x)
接受类型为Foo
的节点x
,并调用可被用户覆盖的虚函数来访问该节点 (对访问到的具体某类节点的操作逻辑应当写在这个函数里)。
- 注意事项
- 在上述三个成员函数中,返回
true
则遍历继续,但若任一函数返回false
,则整个遍历终止。 - 上述三个成员函数具有层级关系 (
Traverse*
>WalkUpFrom*
>Visit*
) ,可以调用同级函数或低级函数,但不能调用高级函数。 - 由于
WalkUpFromFoo()
首先调用WalkUpFromBar(x)
,因此在类型层面上Visit*()
具有自顶向下的调用顺序 (例:对于NamespaceDecl
类型的节点,调用顺序是VisitDecl()
->VisitNamedDecl()
->VisitNamespaceDecl()
) 。 - 遍历 AST 时,默认只访问未实例化的 (而不访问任何隐式或显式实例化的) 模板类和模板函数。可以覆盖虚函数
shouldVisitTemplateInstantiations()
,使它的返回值为true
来启用对所有已知的隐式、显式实例化的模板类和模板函数的访问。 - 默认采用先序遍历 AST。可以覆盖虚函数
shouldTraversePostOrder()
,使它的返回值为true
来使用后序遍历。
- 在上述三个成员函数中,返回
- 缺点
- 需要进行递归遍历,效率较低。
ASTMatcher
建议结合 Tutorial for building tools using LibTooling and LibASTMatchers 中的代码样例来学习 ASTMatcher
,并尝试使用 cmake 编译并运行样例程序。
- 特性
- 本质上是一种 DSL (Domain Specific Language,领域特定语言)。
- 由表达式 (expressions) 触发 (用户使用表达式规定触发访问的条件)。
- 与 AST 上下文信息绑定 (用户可以在表达式中利用上下文信息来筛选节点)。
- 无需遍历,能直接匹配到表达式对应的节点。
- 使用方法
- 直接组合各种
ASTMatcher
来精确表示匹配节点的规则,语义非常清晰,例如binaryOperator(hasOperatorName("+"), hasLHS(integerLiteral(equals(0))))
匹配的是左操作数为字面量0
的加法操作表达式。 - 可以对任意层级的表示 Clang AST 节点 (而非 LHS、RHS、Type、Operand、 等节点属性) 的
ASTMatcher
使用.bind("foo")
操作,将该节点与字符串绑定。 - 可以继承回调类
MatchFinder::MatchCallback
,覆盖虚函数run(const MatchFinder::MatchResult &Result)
,然后使用Result.Nodes.getNodeAs<clang::FooType>("foo")
来访问此前与字符串绑定的 Clang AST 节点。
- 直接组合各种
- 注意事项
- 在 Clang AST 中,对变量的使用被表达为
declRefExpr
(declaration reference expressions,声明引用表达式),例如declRefExpr(to(varDecl(hasType(isInteger()))))
表示对一个整数类型变量声明的使用 (请注意,不是 C++ 中的引用) 。
- 在 Clang AST 中,对变量的使用被表达为