clang 编译器前端 分析

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

clang 编译器前端 分析

clang的python接口教程二

Python接口clang解析C语言AST抽象语法树

clang static analyzer源码分析

clang静态代码分析是clang相对于gcc一个比较能够引起关注的点特别是clang静态代码分析基于checker的架构和大部分的静态分析工具都不相同。clang静态代码分析使用符号执行的技术执行路径敏感的代码分析符号执行引擎并不实际进行报错而是使用挂载在引擎上的checker对程序状态进行检查并报错。这种方式方便用户对代码检查规则或者bug类型进行扩展但是这种架构也有其缺陷符号执行完成一条语句后clang静态分析引擎会遍历checker列表中的回调函数进行报错也就是说checker的数量越多clang静态分析扫描代码的速度越慢clang静态分析引擎的速度是不变的。

AnalysisASTConsumer 继承自ASTConsumer是一个虚基类只提供了一个添加报错机制的纯虚函数。

clang静态分析checker提供了两种方法一种是遍历AST进行语法层级的报错例如位运算符的操作数是有符号整数等这些直接从AST树上拿到相关信息就可以直接报错另外一种是需要构建CFG并在其上进行符号执行实现路径敏感的代码分析关于这个就有人曾经问过我clang静态分析中CFG和AST的关系是什么。现在我们从这个类的继承体系就可以看出clang静态代码分析是继承自于ASTConsumer也就是说无论是基于AST的检查还是基于CFG的检查都是在AST上实现的因为构建CFG也需要AST的协助。

pyclang 使用

 win10上安装LLVM 作用:能够安装各种lib
 pip install clang 作用:作为调用clangAPI的接口,注意这个clang只是一个接口
 目录AST树中调用的函数都在 D:\ProgramFiles\python3.6.8\Lib\site-packages\clang\cindex.py
 
 from clang.cindex import Index
类Index索引类型,clang.cindex库的主接口,通过提供一个接口来读写来解析翻译
index = Index.create() 创建一个索引
index.parse(filepath, 解析文件的路径
args=None, 额外参数通过命令行参数添加
Unsaveed_file=None, 列表,一项是映射文件名字 一项是替换内容
options=0) 其他参数

import clang.cindex
from clang.cindex import Index  #主要API
from clang.cindex import Config  #配置
from clang.cindex import CursorKind  #索引结点的类别
from clang.cindex import TypeKind    #节点的语义类别
from clang.cindex import TokenKind   #单词

# clang.cindex需要用到libclang.so共享库所以先配置共享库
libclangPath = r'D:/Program Files/LLVM/bin/libclang.dll'

#这个路径需要自己先在笔记本上安装	
if Config.loaded == True:
    print("Config.loaded == True:")
    #pass
else:
    Config.set_library_file(libclangPath)
    print("install path")
    
# 创建AST索引
file_path = r"test.c"
index = Index.create()

tu = index.parse(file_path)
AST_root_node= tu.cursor  #cursor根节点
print(AST_root_node)

#前序遍历AST
'''
前序遍历严格来说是一个二叉树才有的概念。这里指的是对于每个节点先遍历本节点再遍历子节点的过程。
'''
node_list = []
def preorder_travers_AST(cursor):
    # 函数声明 CursorKind.FUNCTION_DECL 查找
    if  (not CursorKind._kinds[cursor._kind_id] is None) and cursor.kind == CursorKind.FUNCTION_DECL:
        # 函数被定义的地方
        if cursor.is_definition():
            # 打印有定义的函数名 有函数体的 函数
            print(cursor.spelling + " " + str(cursor.extent.start.file)) # 函数名和所在的文件名
            # cursor.extent.start.line, cursor.extent.start.column  # 起始行列
            #  cursor.extent.end.line, cursor.extent.end.column     # 终止行列
            # 遍历该函数代码 经词法分析得到的每一个单词 token
            for token in cursor.get_tokens():
                # 如果是 c/c++ 等语言关键字
                if token.kind == TokenKind.KEYWORD:
                    if token.spelling == "static":
                        # 出现在 函数名 之前的 static 标记该函数为静态函数
                        print("this func is static" )
                    elif token.spelling == cursor.spelling: 
                        # 如果已经遇到函数名了 则 该函数不是 静态函数
                        print("this func is not static" )
                        break
            # 遍历函数内容
            for child in cursor.get_children():
                # 查找函数参数 CursorKind.PARM_DECL
                if (not CursorKind._kinds[child._kind_id] is None) and child.kind == CursorKind.PARM_DECL:
                    print("param: "child.spelling )
                # 查找函数调用 CursorKind.CALL_EXPR
                if (not CursorKind._kinds[cursor._kind_id] is None) and cursor.kind == CursorKind.CALL_EXPR:  
                    print("func call "child.spelling" )
                # 函数语句
                if (not CursorKind._kinds[child_node._kind_id] is None) and child_node.kind == CursorKind.COMPOUND_STMT:
                    for stmt in child_node.get_children():
                        # 打印语句
                        print(stmt.spelling)
                        # 若未进来说明是空函数
                        
        else
            # 有可能被定义过得 函数 被多次声明
            # 这里虽然没有定义但是名字在上面分支出现也是有定义的
            print(cursor.spelling + " current have not body")
    for cur in cursor.get_children():
        #do something
        #print(cur.spelling)
        preorder_travers_AST(cur)

preorder_travers_AST(AST_root_node)

输出
main

printf
printf
printf


"hello world\n"



# 提取每个分词token 的方法。
cursor_content=""
for token in AST_root_node.get_tokens():
#针对一个节点调用get_tokens的方法。
    print(token.spelling)
    

前端clang分析

Clang是LLVM的C/C++前端从原理上他会产生用于后端的IR指令。但实际上Clang会有两种执行方式 我们可以使用”-###”观察Clang的执行过程

1. 以Driver的方式执行 
   会自动调用相关后端程序并生成可执行文件这也是之所以Clang虽然只是前端却可以直接产生目标代码的原因.
   在驱动模式下clang实质只是一个调度管理程序.
   
   
2. 作为编译器-cc1前端方式运行
   最后仅生成LLVM IR 

Clang执行初期是作为driver执行的因此程序的入口是tools/driver/driver.cpp

代码位置

如果不是-cc1则进行相关命令解释生成相容的命令行

通过Driver建立与GCC相容的编译过程并由TheDriver.ExecuteCompilation执行该相容的

注意因为clang两种工作模式下驱动模式实际是在补足参数后再通过-cc1的方式执行;

在Driver方式下只是为clang补齐相关执行的各参数如类库的名字然后是通过“系统”执行clang -cc1命令而并没有在“内部”继续clang的其余的操作此时clang会等待相关的执行操作完成后执行下一个命令如ld

驱动方式过程

1。ParseOption Parsing 编译参数解析

   在这个阶段完成后命令行将被分解为具有适当参数定义好的选项。

2. Pipeline编译动作构造Compilation Action Construction 编译流水线构造

子进程作业树需要确认构造编译序列。这包含确认输入文件及其类型、对他们进行什么样的工作预处理、编译、汇编、链接等并为每个人物构造动作实例链表。这样的结构是一个或更多的顶层动作列表每个通常对应一个单一的输出例如对象或链接的可执行文件多数的动作Actions对应一个实际的任务但是这里有两个特殊的任务Actions第一个是InputActions他只是简单将输入参数匹配到另一个Actions的输入。第二个是BindArchAction从概念上给所有使用输入的动作替换架构Clang驱动可以使用命令“-ccc-print-phases”转印这一阶段的结果。

2.5 Action

每次的 option 都会完整的走一遍从预处理静态分析backend 再到汇编的过程。

下面列下一些编译器的前端 Action大家可以一个个用着玩。

InitOnlyAction - 只做前端初始化编译器 option 是    -init-only
PreprocessOnlyAction - 只做预处理不输出编译器的 option 是 -Eonly
PrintPreprocessedAction - 做预处理子选项还包括-P、-C、-dM、-dD 具体可以查看PreprocessorOutputOptions 这个类编译器 option 是 -E
RewriteIncludesAction - 预处理
DumpTokensAction - 打印tokenoption 是 -dump-tokens
DumpRawTokensAction - 输出原始tokens包括空格符option 是 -dump-raw-tokens
RewriteMacrosAction - 处理并扩展宏定义对应的 option 是 -rewrite-macros
HTMLPrintAction - 生成高亮的代码网页对应的 option 是 - emit-html
DeclContextPrintAction - 打印声明option 对应的是 -print-decl-contexts
ASTDeclListAction - 打印 AST 节点option 是 -ast-list
ASTDumpAction - 打印 AST 详细信息对应 option 是 -ast-dump
ASTViewAction - 生成 AST dot 文件能够通过 Graphviz 来查看图形语法树。 option 是 -ast-view
AnalysisAction - 运行静态分析引擎option 是 -analyze
EmitLLVMAction - 生成可读的 IR 中间语言文件对应的 option 是      -emit-llvm
EmitBCAction - 生成 IR Bitcode 文件option 是                   -emit-llvm-bc
MigrateSourceAction - 代码迁移option 是 -migrate


3. BindTool & Filename Selection 工具、文件绑定

Bind 主要是与工具链 ToolChain 交互
根据创建的那些 Action在 Action 执行时 Bind 来提供使用哪些工具比如生成汇编时是使用内嵌的还是 GNU 的还是其它的呢这个就是由 Bind 来决定的具体使用的工具有各个架构平台系统的 ToolChain 来决定

驱动与工具链互动去执行工具绑定The driver interacts with a ToolChain to perform the Tool bindings。每个工具链包含特定架构、平台和操作系统等编译需要的所有信息一个单一的工具在编译期间需要提取很多工具链为了与不同体系架构的工具进行交互。

这一阶段不会直接计算但是驱动器可以使用”-ccc-print-bindings”参数打印这一结果会显示绑定到编译序列的工具链工具输入和输出。

4. TranslateTool Specific Argument Translation 工具参数翻译

当工具被选择来执行特定的动作工具必须为之后运行的编译过程构造具体的命令。主要的工作是翻译gcc格式的命令到子进程所期望的格式。

5.Execute  工具执行

其执行过程大致如下Driver::ExecuteCompilation -> Compilation::ExecuteJobs -> Compilation::ExecuteCommand-> Command::Execute -> llvm::sys::ExecuteAndWait此时执行的ExecuteAndWait为Support/Program.cpp中的程序其调用相关操作系统执行其系统相关的执行程序并等待执行过程完成。

clang cc1前端运行 真正有意义的前端操作

    auto FirstArg = std::find_if(argv.begin() + 1, argv.end(),
                               [](const char *A) { return A != nullptr; });
  if (FirstArg != argv.end() && StringRef(*FirstArg).startswith("-cc1")) {
    // If -cc1 came from a response file, remove the EOL sentinels.
    if (MarkEOLs) {
      auto newEnd = std::remove(argv.begin(), argv.end(), nullptr);
      argv.resize(newEnd - argv.begin());
    }
    return ExecuteCC1Tool(argv);
  }  

如果是 -cc1 的话会调用 ExecuteCC1Tool 这个函数先看看这个函数

static int ExecuteCC1Tool(ArrayRef<const char *> argv) {
  llvm::cl::ResetAllOptionOccurrences();
  StringRef Tool = argv[1];
  void *GetExecutablePathVP = (void *)(intptr_t) GetExecutablePath;
  if (Tool == "-cc1")
    return cc1_main(argv.slice(2), argv[0], GetExecutablePathVP);
  if (Tool == "-cc1as")
    return cc1as_main(argv.slice(2), argv[0], GetExecutablePathVP);
  if (Tool == "-cc1gen-reproducer")
    return cc1gen_reproducer_main(argv.slice(2), argv[0], GetExecutablePathVP);
  // Reject unknown tools.
  return 1;
}

最终的执行会执行 cc1-main 、cc1as_main 、cc1gen_reproducer_main。这三个函数分别在 driver.cpp 同级目录里的 cc1_main.cpp 、cc1as_main.cpp 、cc1gen_reproducer_main.cpp中。

依照关于Driver的示意图clang将藉由Action完成具体的操作在clang中所有action定义在include/clang/Drivers名字域clang::driver下其Action即其派生的Actions定义如下

这一阶段完成编译过程被分为一组需要执行并产生中间或最终输出某些情况下类似-fsyntax-only不会有“真实”的最终输出的Action。阶段是我们熟知的编译步骤类似预处理、编译、汇编、链接等等。

所有相关Action的定义在FrontendOptions.h(clang/include/clang/Frontend/FrontendOptions.h )中

代码

在clang中允许通过FrontendAction编写自己的Action使用FrontendPluginRegistryclang/frontend/FrontendRegistry.h注册自己的Action:其核心是通过继承clang::FrontendAction来实现详细示例参考clang/examples/AnnotateFunctions/AnnotateFunctions.cpp该示例通过继承PluginASTAction并使用FrontendPluginRegistry::Add将其注册

细节分析

clang 在线文档

最初的C/C++源码经过词法分析Lexical analysis-> 语法分析Syntactic analysis-> 语义分析Semantic analysis-> 与平台无关的IRLLVM IR generator

从词法分析开始——将C语言 源码分解成token流每个token可表示标识符、字面量、运算符等
token流会传递给语法分析器语法分析器会在语言的CFGContext Free Grammar上下文无关文法的指导下将token流组织成AST抽 象语法树接下来会进行语义分析检查语义正确性然后生成IR。

libclang clang-c/Index.h c接口调用clang 示例

Parser部分涉及到的目录有 tools/clang/lib/AST语法树定义Sema语义分析Lex词法分析器Parse语法分析器。

1. 词法分析Lexical analysis clang/lib/Lex/

token

词法分析器读入组成源程序的字节流并将他们组成有意义的词素Lexeme序列。对于每个词素词法分析器产生词单元token作为输出并生成相关符号表。词法库包含了几个紧密相连的类他们涉及到词法和C源码预处理。

相关诊断: clang/include/clang/Basic/DiagnosticLexKinds.td

词法单元Token的定义TokenKinds.defclang/include/clang/Basic/

Clang的保留字定义在 TokenKinds.def如常见的if或for关键字

说明TokenKinds.def中定义了许多诸如TOK和KEYWORD等宏实际相关宏只有在外部引用程序引用TokenKinds.def前定义才会真正有意义而且引用位置不同其意义可能不同

Preprocessor类 ----- 返回下一个Token

Preprocessor是词法分析中的一个主要接口,可以看到词法分析时在预处理过程中初始化的即Preprocessor.

在词法分析中预处理程序被初始调用的主要程序是CompilerInstance::createPreprocessorlib/Frontend/CompilerInstance.cpp

之后是 FrontEnd Action -> /clang/lib/Lex/Preprocessor.cpp

其核心程序是Preprocessor::Lex,该程序返回下一个Token

Token类 clang/include/clang/Lex/Token.h

Token类用于表述电仪的词法单元。Token被用于词法/预处理单元但并不会在这些库以外存在如Token不会存在于AST中.
Token.h

2. 语法分析Syntactic analysis libclangParse libclangAST

语法分析主要是解析词法分析产生的词法单元token并生成抽象语法树ATS。如同自然语言一样语法分析并不检查语法的上下文的合理性其仅仅是从语法角度确认是否正确。

语法分析的核心数据结构Decl、Stmt、Type对应于声明、指令、类型。所有的被Clang描述的C/C++类都是继承于这三个类

源码 clang/lib/Parse/ 和clang/lib/AST/

诊断信息 DiagnosticParseKinds.td

语法分析器是使用递归下降recursive-descent的语法分析。

clang的Parser是由clang::ParseAST执行的。

libclangAST

    提供了类用于表示C AST、C类型、内建函数和一些用于分析和操作AST的功能visitors、漂亮的打印输出等
    源码定义lib/AST
    重要的AST节点Type、Decl、DeclContext、Stmt

Type类和他的派生类 clang/lib/AST/Type.cpp

Type类及其派生类是AST中非常重要的一部分。通过ASTContext访问Typeclang/ast/ASTContext.h在需要时他隐式的唯一的创建他们。Type有一些不明显的特征1他们不捕获类似const或volatile类型修饰符See QualType2他们隐含的捕获typedef信息。一旦创建type将是不可变的。

声明Decl类 clang/AST/DeclBase.h

表示一个声明declaration或定义definition。例如变量、typedef、函数、结构等

声明上下文DeclContext类 clang/AST/DeclBase.h

程序中每个声明都存在于某一个声明上下文中类似翻译单元、名字空间、类或函数。Clang中的声明上下文是由类DeclContext类进行描述各种AST节点声明上下文均派生于此TranslationUnitDecl、NamespaceDecl、RecordDecl、FunctionDecl等

DeclContext类对于每个声明上下文提供了一些公用功能

  1. 与源码为中心和语义为中心的声明视图

    DeclContext提供了两种声明视图源码为中心视图准确表示源程序代码包括多个实体的声明参见再宣告和重载。而语义为中心的视图表示了程序语义。这两个视图在AST构造时与语义分析保持同步。

2.在上下文中保存声明

每个声明上下文中都包含若干的声明。例如C++类被表示为RecordDecl包含了一些成员函数、域、嵌套类型等等。所有这些声明都保存在DeclContext中即可以由容器迭代操作获得

这个机制提供了基于源码视图的声明上下文视图。

  1. 在上下文中查找声明

由DeclarationName类型指定的声明在声明上下文中查找声明 lookup()

该机制提供了基于语义为中心的声明上下文视图

  1. 声明所有者

    DeclContext包含了所有在其中声明的声明上下文并负责管理他们并以及序列化反他们所有声明都保存在声明上下文中并可以查询每个保存在其中的声明信息。关于声明上下文可以查看词法和语义分析一节.

  2. 再宣告和重载

    在翻译单元中公共的实体可能会被声明多次。

    表达式”f”在以源码为中心的和以语义为中心的上下文中的视图有所不同在以源码为中心的声明上下文中再宣告与他在源码中声明的位置有关。在语义为中心的视图中会使用最近的视图替换最初的声明。而基于DeclContext::look操作将返回基于语义视图的上下文。

  3. 词法和语义上下文

对于每个声明可能存在两个不同的声明上下文词法上下文对应于源码视图的声明上下文。语义上下文对应于语法视图的。Decl::getLexicalDeclContextclang/AST/DeclBase.h返回词法声明上下文。而Decl::getDeclContext返回基于语义上下文返回的两个值都是指向DeclContext的指针。

7.透明声明上下文TransparentDeclaration Contexts

现于枚举类型Red是Color中的成员但是我们在Color外引用Red时并不需要限定名Color

8.多重定义的声明上下文Multiply-DefinedDeclaration Contexts

可以由DeclContext::isTransparentContext确认是否是透明声明。

Stmt 指令 类 clang/AST/Stmt.h for do goto return …

QualType类 查询类

QualType被设计为一个微不足道的、微小的通过传值用于高效查询的类。QualType的思想是将类型修饰符const、volatile、restrict以及语言扩展所需的修饰符与他们自己的类型分开保存。QualType概念上是一对“Type *”和他们的类型修饰符。类型限定符只是占用指针的低位。

声明名字Declarationnames

DeclarationNameclang/AST/DeclarationName.h用来描述clang中的声明名字。声明在C族语言中有一些不同的形式。多数的声明被命名为简单的标识例如f(int x)中的声明f和x。在C++中声明可以构造类的构造函数、类的析构函数、重载操作符合转换函数。

CFG类 控制流程图 Context Free Grammar上下文无关文法

CFG是用于描述单个指令Stmt *的源码级控制流程图。典型的CFG实例为构造函数体典型的是一个CompoundStmt实例但是也可以表示任何Stmt派生类的控制流。控制流图通常对给定函数执行流-或路径-敏感的分析特别有用。

3.语义分析Semantic Analysis

libclangSema

4. 中间代码生成IR Generator libclangCodeGen

libclangCodeGen

5. 其他库介绍

libclangAnaylysis用于进行静态分析用的

libclangRewrite编辑文本缓冲区代码重写转换非常重要如重构

libclangBasic诊断、源码定位、源码缓冲区抽象化、输入源文件的文件缓冲区

Clang诊断子系统是一个编译器与人交互的重要部分。诊断是当代码不正确或可疑时产生警告和错误。在clang中每个诊断产生最少一个唯一标识ID、一个相关的英文、SourceLocation源码位置“放置一个^”和一个严重性例如WARNING或ERROR。他可以选择包含一些参数给争端如使用%0填充字串以及相关源码区域。

Clang diagnostics Diagnostic::Level enum [ NOTE WARNING EXTENSION EXTWARN ERROR ]

SourceLocation表示源代码的位置。Seeclang/Basic/SourceLocation.h。SourceLocation因为被嵌入到许多AST中因此该类必须足够小。

SourceLocation通常是和SourceManager一同使用用于对一个位置信息的两条信息进行编码。见clang/Basic/SourceManager.h

SourceRange是SourceLocation.h中类表示源码的范围【firstlast】。First和last都是SourceLocation

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6