Clang工具链的未来挑战与革新之路

本文深入剖析Clang编译器在工具链开发中的局限性,包括AST信息缺失、CFG近似性问题、LLVM IR架构依赖等核心痛点,并介绍PASTA封装库与DARPA支持的VAST项目如何推动下一代工具链发展。

基于Clang工具链的未来

Clang是一款卓越的编译器,堪称“编译器的编译器”!但它并非工具开发者的理想编译器。作为工具开发者,我理想的编译器应该像一本开放的书,允许我从任意位置访问所有内容。理想编译器操作的数据(文件、宏、词法单元)、最终解释(声明、语句、类型)及其关系(数据流、控制流)都应相互关联。

Clang本身并不具备这些特性。libClang看似是解决C、C++和Objective-C解析问题的现成方案,但事实并非如此。本文将探讨驱动Clang流行的因素,为何在这些因素下其工具能力仍显不足,以及哪些新解决方案让Clang未来可期。

Clang成功的背后是什么?

Clang作为“编译器前端”,负责将C、C++和Objective-C源代码转换为中间表示(IR)。生成的IR随后由LLVM编译器后端接收并转换为机器码。本博客读者可能通过我们的反编译工具了解LLVM。

我十年前将Clang作为主力编译器,因其可操作性强(且美观!)的诊断信息。但Clang直到最近才成为最流行的生产级编译器之一。我认为这是因为其逐渐积累了以下驱动编译器流行的关键因素:

  • 快速编译时间:开发者不希望等待过长的编译时间
  • 生成高效机器码:所有人都希望代码运行更快,对部分用户而言,微小性能提升可节省数百万美元成本(从而降低云支出!)
  • 端到端正确性:开发者需要信任编译器能(几乎总是)将源代码转换为语义等价的机器码(毕竟漏洞难免)
  • 诊断信息质量:开发者需要可操作的错误提示及解决方案建议
  • 生成可调试机器码:机器码必须与现有调试器格式兼容
  • 支持与动力:学术界(时间资源)与工业界(资金资源)需要共同推动编译器持续改进

然而,这份清单缺少一个重要因素:工具链支持。尽管过去几年已有诸多改进,Clang的工具链生态仍任重道远。本文旨在客观审视基于Clang的工具链现状,让我们深入探究!

Clang AST是个谎言

Clang的抽象语法树(AST)是所有工具链的基础抽象层。AST从源代码捕获关键信息,并作为语义分析(如类型检查)和代码生成的脚手架。

但当涉及未显式编写的代码时呢?以C++为例,开发者通常不会显式调用类析构函数,而是在对象生命周期结束时隐式调用。C++充满此类隐式行为,但几乎 none 在Clang AST中显式表示。这对基于Clang AST的工具构成巨大盲区。

Clang CFG是个(相当不错的)谎言

前文我曾抱怨编译器丰富的信息未被充分利用而转向临时方案。公平地说,这过于简化了——例如Clang并非为IDE交互性而优化设计。但同时,也存在一些优秀且活跃的基于Clang的工具,如Clang静态分析器。

由于Clang静态分析器“基于Clang构建”,人们可能认为其分析基于忠实于Clang AST和LLVM IR的表示形式。但如前所述,Clang AST存在缺失(如隐式C++析构调用)。Clang静态分析器通过操作称为CFG(控制流图)的数据结构规避了此问题。

Clang CFG表示理论计算机如何执行AST中的语句。分析结果的准确性取决于CFG的准确性。但CFG实际上并未用于Clang的代码生成过程(该过程产生包含控制流信息的LLVM IR)。Clang CFG只是对实际重要实现的近似。作为工具开发者,我关心准确性,不希望猜测抽象层的漏洞。

LLVM IR作为唯一真IR是个谎言

Clang的中间表示LLVM IR直接由Clang AST生成。LLVM IR表面上是机器无关的,但细看容易发现机器相关部分(如内置函数、目标三元组、数据布局)。这些部分本就不期望可重定向,因为它们明确针对特定架构。

LLVM IR未能成为实用可重定向IR的原因与其自身关系不大,更多在于Clang生成它的方式。Clang为不同架构编译相同代码时不会生成完全相同的LLVM IR。简单示例如:LLVM IR包含常量值,而源代码包含sizeof(void *)等表达式。这些是“已知的已知”,开发者可合理预测差异。不合理的差异发生在Clang过度急切地选择类型、函数参数和返回值表示以“适应”目标应用二进制接口(ABI)。实践中,这意味着你的std::pair<int, int>函数参数可能被表示为单个i64、两个i32、两个i32的数组,甚至结构体指针……但从不以结构体形式表示。可笑的是,LLVM后端能完美处理结构体类型参数并正确执行目标特定ABI降级。我打赌在这两种完全不同的ABI降级系统间存在潜在漏洞。这让人想起CFG的处境,对吧?

关键点在于:Clang AST缺失了LLVM IR代码生成器创造的信息,而LLVM IR也缺失了被该代码生成器销毁的信息。若要弥合这一差距,你需要依赖近似方案:Clang CFG。

附加篇:libClang中的“lib”是个谎言

库应能被嵌入大型程序,因此应避免触发导致进程终止的中止操作!尤其在执行只读非状态变更操作时。我说libClang中的“lib”是个谎言,因为“Clang API”并非真正的外部API,而是Clang内部的API。当Clang自身使用不当时,触发断言并中止执行是合理的(这可能是漏洞迹象)。但巧合的是,Clang大量API以库形式暴露,于是今天我们有了libClang——它假装是库却未按库标准设计。

第二附加篇:compile_commands.json是个谎言

在整个程序或项目上运行基于Clang工具链的公认方式是通过名为compile_commands.json的JSON格式。该格式以命令行形式嵌入编译器调用(字符串形式——糟糕!或参数列表)、编译器操作目录和正在编译的主源文件。

不幸的是,此格式缺少环境变量(那些烦人的东西!)。是的,环境变量实质影响编译器的操作和行为。较知名的变量如CPATHC_INCLUDE_PATHCPLUS_INCLUDE_PATH影响编译器解析#include指令的方式。但你知道CCC_OVERRIDE_OPTIONS吗?如果不知道,猜猜看:compile_commands.json也不知道!

或许这些环境变量不常用?但PATH变量总是被使用。当在命令行输入clang时,PATH变量部分决定了执行哪个Clang二进制文件。根据系统和设置,这可能意味着Apple Clang、Homebrew Clang、vcpkg Clang、Debian包管理器中的某个Clang版本,或自定义构建版本。这很重要,因为clang可执行文件具有自省能力。Clang使用其二进制文件路径来发现资源目录位置(包含stdarg.h等头文件)。

作为工具开发者,我希望能忠实复现原始构建,但凭借当前的compile_commands.json格式无法实现。

最终附加篇:编译器教科书在欺骗你(某种程度上)

我保证这是最后一次抱怨,但这直击问题核心。编译器完美契合流水线架构:源代码文件被词法分析为词法单元,然后由解析器结构化为AST。AST经类型检查器进行语义正确性分析后转换为IR进行通用优化。最后,IR由后端定向降级为特定机器码。

这种理论流水线架构具有许多优点。流水线架构允许在任意两阶段间插入第三方工具,只要工具消费正确输入格式并产生正确输出格式。事实上,正是这种流水线特性使LLVM后端擅长优化。LLVM优化器是逻辑上消费和产生LLVM IR的“通道”。

事实是,在Clang中,词法分析、解析和语义分析是难以分离的协作组件集合。语义分析器驱动预处理器,后者与词法分析器协程以识别、注释并在不再需要时立即丢弃词法单元。Clang仅保留足够信息以输出美观诊断和处理C++等语言的解析歧义,其余部分为追求速度和内存效率而被丢弃。

实践中这意味着:令人惊讶的是,Clang预处理器无法在预词法分析后的词法单元流上正确操作。还有更微妙的后果:例如,拦截预处理器以捕获宏展开看似被支持,但实际几乎不可用。该支持通过回调机制实现,但回调常缺乏足够上下文或在错误时机被调用。仅从回调流中,无法区分宏参数展开与类函数宏调用前的展开,或条件指令内外的宏展开。这对需要展示源代码和宏展开树的工具至关重要。优秀工具如Woboq Code Browser在回调中调用第二个预处理器的原因就在于此:别无他法看清实际发生的情况。

归根结底,编译器教科书描述的传统编译器流水线心智模型过于简化,并未体现Clang的实际工作方式。预处理是非常复杂的问题,现实往往需要复杂解决方案。

基于Clang工具链的未来已在路上

若你认同我的观点,请关注PASTA——一个覆盖大部分Clang API的C++和Python封装库。它事无巨细:小到为所有API方法提供规范一致的命名方案、自动管理底层数据结构内存、正确管理编译命令;大到提供文件词法单元与AST节点的双向映射,并使API方法即使在不该使用时也能安全使用(因为Clang不文档化何时触发断言并终止进程)。

PASTA并非我所有抱怨的万能解。但幸运的是,DARPA正慷慨资助编译器研究的未来。作为DARPA V-SPELLS计划的一部分,Trail of Bits正在开发VAST——一个基于MLIR的Clang新中端(我们在VAST-checker博客文章中介绍过)。VAST将Clang AST转换为高层次、信息丰富的MLIR方言,同时保持与AST的溯源关系并包含显式控制和数据流信息。VAST逐步降级该MLIR,最终直达LLVM IR。或许教科书并未说谎,因为这听起来像是连接Clang AST与LLVM IR的流水线。

没错:我们并未良莠不分。尽管我长篇大论,Clang仍是优秀的C、C++和Objective-C前端,LLVM仍是出色的优化器和后端。时代需求将这两颗宝石以不甚理想的方式组合,而我们正努力打造皇冠上的明珠。请保持关注,因为我们将在不久的将来以宽松开源许可证发布结合PASTA和VAST的工具。

本研究由国防高级研究计划局(DARPA)资助。所表达的观点、意见和/或发现均为作者个人观点,不应解释为代表美国国防部或美国政府的官方观点或政策。

分发声明A——批准公开发布,无限制分发。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计