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

本文深入探讨Clang编译器在工具开发中的局限性,包括AST信息缺失、CFG近似问题、LLVM IR不一致性,并介绍PASTA和VAST等新兴解决方案,展望Clang工具链的未来发展。

基于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++充满了这些隐式行为,几乎没有一个在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的原因实际上与LLVM 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”并非真正 intended 作为外部API;它是Clang其余部分的内部API。当Clang不正确使用自身时,触发断言并中止执行是有意义的——这可能是错误的标志。但碰巧的是,Clang API的很大一部分以库形式暴露,所以我们今天有了libClang,它假装是一个库,但并未如此设计。

第二次再来:compile_commands.json是一个谎言

在整个程序或项目上运行基于Clang的工具的公认方式是一种恰名为compile_commands.json的JSON格式。这种JSON格式以命令形式嵌入编译器调用(要么作为字符串——恶心!,要么作为参数列表),编译器操作的目录,以及正在编译的主要源文件。

不幸的是,这种格式缺少环境变量(那些烦人的东西!)。是的,环境变量 materially 影响编译器的操作和行为。更知名的变量如CPATH、C_INCLUDE_PATH和CPLUS_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格式做到这一点。

最终再来:编译器教科书在欺骗你(有点)

我保证这是我最后的 rant,但这个切中问题的核心。编译器 neatly 适合管道架构:源代码文件被词法分析为标记,然后由解析器结构化为AST。AST然后由类型检查器分析语义正确性,然后转换为IR进行通用优化。最后,IR由后端 targeted 并降低为特定的机器代码。

这种理论管道架构有许多好特性。管道架构 potentially 使第三方工具能够在任何两个阶段之间引入,只要工具消耗正确的输入格式并产生正确的输出格式。事实上,正是这种管道性质使LLVM后端在优化方面表现出色。LLVM优化器是逻辑上消耗和产生LLVM IR的“通道”。

事实是,在Clang中,词法分析、解析和语义分析是协作组件的分形,不易分离。语义分析器驱动预处理器,它与词法分析器协同工作,以识别、注释,然后在不再需要时丢弃标记。Clang保留足够的信息来报告漂亮的诊断和处理像C++这样的语言中的解析歧义,并丢弃其余部分以尽可能快速和内存高效。

这在实践中意味着,令人惊讶的是,Clang的预处理器实际上无法在预词法分析的标记流上正确操作。还有更 subtle 的后果;例如,介入预处理器以捕获宏扩展似乎被支持,但在实践中几乎不可用。这种支持通过回调机制实现。不幸的是,回调通常缺乏足够的上下文或在错误的时间调用。仅从回调流中,无法区分像宏参数的宏扩展与在函数式宏调用之前发生的扩展,或条件指令之前与内部的宏扩展。这对于想要呈现源代码和宏扩展树的工具很重要。像优秀的Woboq代码浏览器这样的基于Clang的工具在回调内部调用第二个预处理器是有原因的;没有其他方法可以看到实际发生的情况。

归根结底,编译器教科书 neatly 描述的传统编译器管道的心理模型是 simplistic 的,并不代表Clang实际工作的方式。预处理是一个 remarkably 复杂的问题,现实 often 需要复杂的解决方案。

基于Clang工具的未来正在路上

如果你同意我的 rant,查看PASTA,一个C++和Python包装器,覆盖了Clang API表面的大部分。它做大大小小的事情。在小事中,它为所有API方法提供 disciplined 和一致的命名方案,所有底层数据结构的自动内存管理,以及编译命令的适当管理。在大事中,它提供从文件词法分析的标记和AST节点之间的双向映射,并使API方法 conventionally 安全使用,即使你不应该使用它们(因为Clang不记录何时事物断言并摧毁你的进程)。

PASTA并非我所有抱怨的万能药。但是——幸运的是,有抱负的Clang工具开发者或读者——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的管道。

没错:我们不会把孩子和洗澡水一起倒掉。尽管我 long rant,Clang仍然是一个伟大的C、C++和Objective-C前端,LLVM是一个伟大的优化器和后端。时代的需求 conspired 将这两个宝石 fit 在一起在 less-than-ideal 设置中,我们正在努力开发皇冠上的宝石。关注这个位置,因为我们将在不久的将来在 permissive 开源许可证下发布一个结合PASTA和VAST的工具。

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

分发声明A – 批准用于公开发布,分发无限制

如果你喜欢这篇文章,分享它: Twitter LinkedIn GitHub Mastodon Hacker News

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