博客 阅读时间 5 分钟

快速了解 Clang 的内部工作原理

分享本文
A quick overview of how Clang works internally

Clang 已被证明是一个成熟的 C 和 C++ 编译器,与 GCC 和微软的编译器一样。然而,它的特别之处在于:它不仅仅是一个编译器,更是一套用于构建工具的基础设施。得益于其基于库的架构,组件可以更容易地被复用,新特性也可以更灵活地集成到其他项目中。

Clang 的设计

与许多其他编译器设计一样,Clang 编译器分为三个阶段:

  • 前端解析源代码、检查错误,并构建特定语言的抽象语法树(AST)来表示输入代码。
  • 优化器对前端生成的表示执行优化。
  • 后端生成最终由机器执行的代码;它取决于目标平台。

Clang 与其他编译器有什么区别?

其设计中最重要的区别在于:Clang 基于 LLVM;LLVM 背后的理念是使用 LLVM 中间表示(IR),它之于 LLVM 就像字节码之于 Java。LLVM IR 被设计用来承载编译器优化部分中的中层分析和变换。它在设计时有许多具体目标,包括支持轻量级运行时优化、跨函数/过程间优化、全程序分析,以及激进的重组变换。

有了这种设计,我们可以复用编译器的一大部分来创建其他编译器。例如,我们可以只更换前端来处理其他语言。

一、前端

Clang 被设计为模块化的,每个编译阶段都由特定的模块完成。下面是参与前端阶段的一些项目:

与任何前端解析器一样,我们需要一个词法分析器(lexer)和语义分析。Clang 前端可以通过传入 -cc1 参数来执行。它支持多项特性,包括 AST 生成:

clang -cc1 -ast-dump test.c

这个命令行由 cc1_main 函数处理;下面是执行过程中一些值得注意的方法的调用顺序:

clang11

ExecuteAction 方法有一个 FrontendAction 类型的参数;其用途是指定要执行哪个前端动作。FrontendAction 是抽象的,因此我们需要继承它来实现一个具体的前端动作。

让我们使用CQLinq来探索 Clang 实现的所有前端动作;为此,我们可以搜索所有直接或间接继承自它的类。

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.FrontendAction”)
where depth0  >= 0 orderby depth0
select new { t, depth0 }

有许多前端动作可用;例如,ASTDumpAction 可以在不创建最终可执行文件的情况下生成 AST。几乎所有前端动作都继承自 ASTFrontendAction,这意味着它们都基于生成的 AST 工作。

这种设计的有趣之处在于,我们可以轻松插入自己的 FrontendAction;我们只需实现一个新的即可。

我们该如何对 AST 执行一些处理?

每个 ASTFrontendAction 都会创建一个或多个 ASTConsumer;ASTConsumer 类是抽象的,我们必须实现自己的 AST 消费者来满足特定需求。

FrontendAction 会调用 AST 消费者,如下图所示。

让我们使用CQLinq中说得更好了:

from t in Types
let depth0 = t.DepthOfDeriveFrom(“clang.ASTConsumer”)
where depth0  == 1
select new { t, depth0 }

CodeGenerator 就是 AST 消费者的一个例子

如前所述,LLVM 的优势之一是使用 IR,而生成 IR 需要处理 AST。CodeGenerator 就是派生自 ASTConsumer、负责生成 IR 的类。有趣的是,这部分处理被隔离在另一个名为 ClangCodeGen 的项目中。

下面是参与 LLVM IR 生成的一些类:

二、优化器

要解释这个阶段,没有人比 LLVM 之父 Chris Lattner 在这篇文章中说得更好了:

“要直观地理解优化是如何工作的,最好是过一遍一些例子。编译器优化有许多不同的种类,因此很难给出一个解决任意问题的套路。话虽如此,大多数优化都遵循一个简单的三段式结构:

  • 寻找一个待变换的模式。
  • 验证该变换对匹配到的实例是安全/正确的。
  • 执行变换,更新代码。
优化器读入 LLVM IR,对它咀嚼一番,然后输出 LLVM IR——希望它执行得更快。在 LLVM 中(与许多其他编译器一样),优化器被组织成一条由不同优化遍(pass)组成的流水线,每一遍都在输入上运行并有机会做点什么。常见的优化遍包括内联器(把函数体替换到调用点)、表达式重结合、循环不变量代码外提等等。根据优化级别的不同,会运行不同的优化遍。

让我们搜索继承自 “Pass” 类的类,来探索 LLVMCore 的优化遍。

from t in Types
let depth0 = t.DepthOfDeriveFrom(“llvm.Pass”)
where t.ParentProject.Name==”LLVMCore” && depth0  >= 0 orderby depth0
select new { t, depth0 }

当然,其他 LLVM 模块中还存在许多其他优化遍。

三、后端

与其他阶段一样,后端负责为特定目标生成输出。在 Clang 中,它是高度模块化的。以 LLVMX86Target——为 x86 目标生成代码的模块——为例。

下面这张图展示了参与为 x86 目标生成二进制文件的所有模块。

许多模块参与了这个阶段,每个模块都有特定的职责。这促进了内聚、干净的 API 和关注点分离,让系统更易于开发者理解,因为他们可以专注于整体架构中更小的部分。

结语

LLVM/Clang 这对组合不仅仅是 C/C++ 编译器;它还是一套构建工具的基础设施,其行为易于扩展。LLVM/Clang 源代码中开箱即用地包含了许多工具,网上还能找到更多。

如果您需要一个 C/C++ 解析器来构建工具,Clang 是一个非常好的候选。

分享本文