关键词
LLVM,Clang 12,AST,OCLint
预计阅读时间
5min
LLVM
先说下传统编译器的工作原理,基本上都是三段式的,可以分为前端、优化器和后端。前端负责解析源代码,检查语法错误,并将其翻译为抽象的语法树;优化器对这一中间代码进行优化,试图使代码更高效;后端则负责将优化器优化后的中间代码转换为目标机器的代码。
静态分析很大一部分是从源代码角度分析的,比较适合用于分析各种代码样式等东西,这也是我们一般可以想到的方法。但是更复杂的静态分析,从源码角度就很困难了。这个时候,代码编译的中间数据——抽象语法树就可以帮大忙了。
clang 12 AST
Clang 的 AST 与其他的编译器生成的 AST 的不同之处在于,它于 C++ 代码和标准非常相似。 比如括号表达式,编译时常量。
抽象语法树示例
这里先对抽象语法树的结构有一个初步的印象,下面是示例代码及结构
//Example.c
#include <stdio.h>
int global;
void myPrint(int param) {
if (param == 1)
printf("param is 1");
for (int i = 0 ; i < 10 ; i++ ) {
global += i;
}
}
int main(int argc, char *argv[]) {
int param = 1;
myPrint(param);
return 0;
}
检测抽象语法树
对一些简单的示例代码进行查看,是一个熟悉 Clang AST 的好方法。 Clang 具有内置的 AST-dump 模式。我们可以通过 -ast-dump 这个 tag 来使用。 下面查看一个栗子:
有如下测试代码 ast.c
int testFunc(int x) {
int result = (x / 18);
return result;
}
接着在终端,输入 clang -Xclang -ast-dump -fsyntax-only ast.cc 可以看到如下的内容
TranslationUnitDecl 0x7fe43f03c208 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fe43f03cae0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fe43f03c7a0 '__int128'
|-TypedefDecl 0x7fe43f03cb48 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fe43f03c7c0 'unsigned __int128'
|-TypedefDecl 0x7fe43f03ce78 <<invalid sloc>> <invalid sloc> implicit __NSConstantString '__NSConstantString_tag'
| `-RecordType 0x7fe43f03cc20 '__NSConstantString_tag'
| `-CXXRecord 0x7fe43f03cb98 '__NSConstantString_tag'
|-TypedefDecl 0x7fe43f03cf10 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fe43f03ced0 'char *'
| `-BuiltinType 0x7fe43f03c2a0 'char'
|-TypedefDecl 0x7fe43f840258 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list '__va_list_tag [1]'
| `-ConstantArrayType 0x7fe43f840200 '__va_list_tag [1]' 1
| `-RecordType 0x7fe43f03cff0 '__va_list_tag'
| `-CXXRecord 0x7fe43f03cf60 '__va_list_tag'
`-FunctionDecl 0x7fe43f840380 <ast.cc:1:1, line:4:1> line:1:5 testFunc 'int (int)'
|-ParmVarDecl 0x7fe43f8402c0 <col:14, col:18> col:18 used x 'int'
`-CompoundStmt 0x7fe43f8405d0 <col:21, line:4:1>
|-DeclStmt 0x7fe43f840570 <line:2:5, col:26>
| `-VarDecl 0x7fe43f840478 <col:5, col:25> col:9 used result 'int' cinit
| `-ParenExpr 0x7fe43f840550 <col:18, col:25> 'int'
| `-BinaryOperator 0x7fe43f840530 <col:19, col:23> 'int' '/'
| |-ImplicitCastExpr 0x7fe43f840518 <col:19> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fe43f8404d8 <col:19> 'int' lvalue ParmVar 0x7fe43f8402c0 'x' 'int'
| `-IntegerLiteral 0x7fe43f8404f8 <col:23> 'int' 18
`-ReturnStmt 0x7fe43f8405c0 <line:3:5, col:12>
`-ImplicitCastExpr 0x7fe43f8405a8 <col:12> 'int' <LValueToRValue>
`-DeclRefExpr 0x7fe43f840588 <col:12> 'int' lvalue Var 0x7fe43f840478 'result' 'int'
接着我们为了分析方便简化一下,输出结果。(减去 clang 的全局声明部分内容)
TranslationUnitDecl 0x7fe43f03c208 <<invalid sloc>> <invalid sloc>
... 减去了 clang 的全局声明部分
`-FunctionDecl 0x7fe43f840380 <ast.cc:1:1, line:4:1> line:1:5 testFunc 'int (int)'
|-ParmVarDecl 0x7fe43f8402c0 <col:14, col:18> col:18 used x 'int'
`-CompoundStmt 0x7fe43f8405d0 <col:21, line:4:1>
|-DeclStmt 0x7fe43f840570 <line:2:5, col:26>
| `-VarDecl 0x7fe43f840478 <col:5, col:25> col:9 used result 'int' cinit
| `-ParenExpr 0x7fe43f840550 <col:18, col:25> 'int'
| `-BinaryOperator 0x7fe43f840530 <col:19, col:23> 'int' '/'
| |-ImplicitCastExpr 0x7fe43f840518 <col:19> 'int' <LValueToRValue>
| | `-DeclRefExpr 0x7fe43f8404d8 <col:19> 'int' lvalue ParmVar 0x7fe43f8402c0 'x' 'int'
| `-IntegerLiteral 0x7fe43f8404f8 <col:23> 'int' 18
`-ReturnStmt 0x7fe43f8405c0 <line:3:5, col:12>
`-ImplicitCastExpr 0x7fe43f8405a8 <col:12> 'int' <LValueToRValue>
`-DeclRefExpr 0x7fe43f840588 <col:12> 'int' lvalue Var 0x7fe43f840478 'result' 'int'
最顶层的声明 (TranslationUnitDecl) 是一个 translation unit declaration(翻译单元声明),在这个例子里,我们自己写的第一个声明(FunctionDecl)是一个叫做 testFunc 的 function declaration(函数声明)。testFunc的主体是一个compound statement(复合语句),它的子节点(DeclStmt)是一个declaration statement(声明语句),定义了我们的 result 变量,以及另一个子节点(ReturnStmt) return statement(返回语句)。
这里可以清晰的看到,这一段代码的每一个元素与其子结点的关系。其中的结点有两大类型,一个是Stmt 类,包括 Expr 表达式类也是继承于 Stmt,它是语句,有一定操作;另一大类元素是 Decl 类,即定义,所有的类,方法,函数变量均是一个Decl类(这两个类互不兼容,需要特殊容器结点来转换,比如DeclStmt结点)。另外从数据结构中可以看到,这个树是单向的,只有从某一个顶层元素向下访问。
访问抽象语法树
无论是 Stmt 还是 Decl 都自带迭代器,可以方便的遍历所有结点素再判断其类型进行操作。不过在clang中还有更方便的方法:继承 RecursiveASTVisitor类。 它是一个 AST 树递归器,可以递归的访问一个 AST 树的所有结点。最常用的方法是 TraverseStmt和 TraverseDecl。
例如我要访问我这么一段代码中所有的函数,即 FunctionDecl,并且输出这些函数的名字,我就要重写(通过自定义 checker)这么一个方法:
bool VisitFunctionDecl(FunctionDecl *decl){
string name = decl->getNameAsString();
printf(name);
return true;
}
这样,我们就能够访问到这棵 ast 树中所有的 FunctionDecl 结点并且把其中函数名字给输出出来了。
接下来该做什么
到目前为止,我们已经知道了 Clang 是如何工作的,以及 AST 的大致结构,这些内容已经足够我们后续的学习了,所以这里就不展开了。 那么后续的文章会讲到 OCLint 是如何工作的,以及 OCLint 的规则的编写。
点击进入下一章 OCLint 入门到实战(中):OCLint 工作流及源码解析