For the complete documentation index, see llms.txt. This page is also available as Markdown.

FreeBSD 10 中的 Clang

作者:David Chisnall

FreeBSD 10 内置支持大部分 C11 和 C++11 标准。走到这一步的路途漫长,期间替换了工具链的大部分组件。如今默认的 C/C++(和 Objective-C)编译器是 clang,默认的 C++ 标准库实现是 libc++。

那么为何需要新的编译器和新的 C++ 标准库?GNU 编译器集合(GCC)伴随 FreeBSD 度过了大部分时光。从 2.x 升级到 3.x,再到 4.x,每次都颇费周折。采用 BSD 许可证的操作系统依赖一款 GPL 许可的编译器,对项目而言始终是一处隐痛。

历史回顾

2007 年,GCC 转向 GPLv3。这份许可证中有一到两条条款让一些主要的下游用户无法接受,因此决定不再将任何 GPLv3 代码引入基本系统。GCC 版本停留在 4.2.1。

2008 年,Apple 的几位开发者发布了 clang——LLVM 编译器基础设施的 C 前端。LLVM 最初作为 GCC 的新后端提交给自由软件基金会,但遭到拒绝。Apple 开始使用 LLVM 后端(并聘请了 LLVM 的原作者以及其他许多人),持续改进它。

LLVM 远不止是 C 编译器。它提供统一的中间表示,语言前端可以生成它,优化器可以修改它,后端可以将其转换为本机代码。最早的应用之一是 Apple 的 OpenGL 着色器栈,基于 LLVM 的简陋 JIT 编译器性能比手写版本快约 20%,并且在 Apple 支持的所有架构上都能工作。

过去五年里,clang 与 LLVM 的组合已经成熟。如今它是 Apple 唯一支持的编译器,也是 Android NDK 附带的标准编译器之一。ARM、Qualcomm、Apple、谷歌、AMD、Intel 等公司都在向它贡献大量代码。

与此同时,我们老旧的 GCC 显得过时。2011 年底,C 和 C++ 标准委员会发布了各自语言新方言的规范。C 的扩展相对简单。C++ 的变动巨大。两者都需要修改编译器和标准库。

C 标准库

C 标准库是 FreeBSD 的核心特性。多人参与改进它以实现新的 C11 特性,包括 Unicode 字符串支持、原子操作和设计欠佳的线程库。后者被加入 C11,是因为 C 标准需要原子操作,而原子操作需要支持并行的内存模型,这意味着 C 标准需要一种创建线程的方式,这一点令人遗憾。

C 库现在还实现了 POSIX2008 扩展 locale 支持。在此之前,C 库中的许多函数(包括 printf())隐式感知 locale。如果你调用 setlocale(),可能从它们得到不同的结果。这包括一些相当细微的东西,比如浮点数值的分隔字符。setlocale() 函数全局设置 locale,这意味着在多线程程序中使用多个 locale 不安全。POSIX2008 locale 扩展提供了每线程 locale,但更重要的是,它们提供了一组标准 C 函数的变体,接受显式 locale 作为参数。许多东西使用这些 API,包括新版本的 GNOME,但我们这里关心的主要消费者是 libc++。

libc++ 是栈中又一个起源于 Apple 的组件。主要由 Howard Hinnant 开发,它是 C++ 标准库的全新实现,从头为 C++11 设计。在标准库中实现 C++11 支持需要做相当侵入性的修改(这些修改破坏了向后兼容性),所以从零开始似乎是个不错的起点。这也允许所有标准数据结构以更适合现代硬件的方式重新设计,例如更关注 std::string 中的缓存使用。

C++11 和 C11 的益处

既然我们有了新的 C++ 栈和改进的 C 栈,那给我们带来了什么?我简要提到过 C11 的一些新特性,但个人最喜欢的是新的原子类型。它们用 _Atomic() 类型限定符声明,例如:

该限定符确保你不会意外地对同一变量混用原子和非原子访问。简单操作会自动变为原子操作。这里需要注意一些事项。例如,考虑以下三行:

前两行会是顺序一致的原子自增。最后一行则是一次顺序一致的原子读、一次加法,然后一次原子存储。顺序一致意味着所有原子操作在全局以相同顺序可见。例如,如果一个核心自增 x 而另一个自增 y,那么要么对 x 的自增先可见,要么对 y 的自增先可见,但在所有核心上顺序相同。

在另一个极端,采用松散排序的原子操作只要求操作相对于该单一变量是原子的,与其他变量无关。如果 x 和 y 是松散的,那么一些线程看到新 x 和旧 y、另一些看到新 y 和旧 x 都是可接受的。

stdatomic.h 头文件包含许多操作原子变量的函数,我们的实现包含若干代码路径,使其能在旧编译器上工作。这是我们在其他地方也复用了的模式,像 _Thread_local 存储限定符等类似特性,在使用原生不支持它们的编译器时,通过我们的标准头文件中的扩展实现。

C11 中的另一项新增使我们能够清理一些头文件。标准新增了 _Generic() 表达式,它类似于基于表达式的类型(而非值)进行选择的 switch 语句。这只在宏中有用,但在若干必须在 C 头文件中定义的标准宏中很有用。特别地,有若干与数值相关,为所有浮点类型定义。此类别中的两个例子是 isinf()isnan(),分别在参数为无穷大或非数字值时返回真。我们旧的代码根据参数大小决定调用哪个函数。这意味着如果你传递一个 32 位 int 值,它会调用期望 32 位 float 的函数。这总会返回假(因为通过转换 int 创建的 float 不可能为无穷大或非数字值),但几乎肯定隐藏了一个逻辑错误,因为没理由在 int 上检查这些属性。

我们现在对这些使用 _Generic(),因此它们总会去到正确的函数,如果你用错误参数调用会得到错误。我们因此发现了 Ports 中的若干 bug、相当古怪的行为。例如,谷歌的 v8 和 Mono 都有测试 isnan(1) 是否工作的配置脚本检查。在 Mono 的情形,如果它们检测到 isnan(int) 不工作,就声明自己的 isnan(double) 来使用,结果与系统版本冲突。

Clang 带来的挑战

让系统为 clang 准备就绪——先是作为系统编译器,然后作为基本系统中唯一的编译器——是一段有趣的经历。许多最初的体验来自让 FreeBSD 不带警告编译。Clang 给出的警告比我们旧的 gcc 多得多(gcc 4.8 现在质量类似,虽然仍有轻微的误报倾向),我们努力确保所有代码不带警告构建。颇为有趣的是,我们树中违反编译器警告最严重的是 gcc 自己。

在 Ports 树中,情况有所不同。我们确实为许多程序维护本地补丁,但理想情况下这些补丁应该很小(并尽可能推送上游)。大多数 C 代码用 clang 工作良好。我们在 Ports 树中面临的最大问题是,clang 在以 clangcc 调用时默认使用 C99 作为方言,而 gcc 默认使用 C89。有些令人沮丧的是,2013 年人们仍以 cc 调用 C 编译器,因为 cc 在 1997 年的 POSIX 版本中已被弃用,并定义为接受未指定版本的 C 语言。当时的选择是 K&R C 或 C89。如果你想要 C89,建议调用 c89 工具。下一个 POSIX 版本添加了 c99 工具。推测下一个发布的版本也会指定 c11 工具。

C99 规范经过精心设计,有效的 C89 程序也是有效的 C99 程序,所以这本不该成为问题。不幸的是,这并不完全奏效,因为很少有人写 C89 代码,而是写带 GNU 扩展的 C89。我说 GCC 默认为 C89 模式,但这并不完全正确:它默认为带 GNU 扩展的 C89(命令行上的 gnu89)。

带 GNU 扩展的 C89 与 C99 之间只有一个显著的不兼容性,那就是 inline 关键字的处理。差异意味着期望 GNU inline 规则的代码在 C99 模式下会导致函数被定义多次,因此链接会失败。这相对容易修复——只需向编译器 flags 添加 -fgnu89-inline——但每个有此类错误的 Port 都需要这样做。当你有超过 20,000 个 Port 时,即使是简单的修复也是大量工作。

在 C++ 中,问题更突出。C++ 中符号解析的规则极其复杂。在模板内部尤其如此,标准要求两阶段查找。GCC 和 Microsoft C++ 编译器都搞错了。当然,它们以不同的错误方式搞错,这就是为什么传统上在编译器之间迁移 C++ 代码非常困难。Clang 受益于所有这些经验,严格按照规范编写 C++ 解析器。这意味着任何符合标准的 C++98 代码都能用 clang 编译。如今,C++11 代码也能,一些 C++1y 代码也能(C++1y 是很可能成为 C++14 的草案)。不幸的是,当你拒绝编译一个流行的开源程序时,你不会得到多少同情,转而说“嗯,代码是无效的。”

迁移的挑战

对于 C 代码,迁移没有困难。C ABI 由目标平台定义,Clang 和 GCC 生成的代码完全兼容。对于 C++ 代码也是如此——如果你只谈论 C++ 这门语言。不幸的是,任一语言都不止于此——还有标准库。在 C 的情形,这是 FreeBSD libc。同样,它在编译器之间共享。

在 C++ 的情形,实际上是两个库。较小的一个实现语言的动态部分,比如异常和 dynamic_cast<> 操作符。较大的一个实现标准模板库(STL)。在我们旧的栈中,它们分别由 libsupc++ 和 libstdc++ 实现。最初,这两者静态链接在一起。

在新栈中,它们是 libcxxrt 和 libc++。作为迁移路径的一部分,我们希望让程序能链接使用 libc++ 和 libstdc++ 的库。这需要修改我们的 libstdc++,让它链接 libsup++ 作为动态(共享)库,然后允许 libmap.conf 在它们之间切换。

不幸的是,事情从没那么简单。ELF 符号版本控制将符号与版本和它来自的库都关联起来,因此现有二进制在符号突然从 libstdc++.so 移到 libsupc++.so 时会链接失败。解决方案是让 libstdc++ 成为过滤库。这让它能有效地将符号解析转发给它链接的库。完成这步后,链接两者成为可能。STL 符号有不同的名称,因此你会使用你在源代码中包含的头文件中的那一个。不幸的是,由于它们有不同的符号名(和不同的二进制布局),你不能互换使用。如果你有使用 STL 类型(例如 std::string)接口的库,那么该库和调用它的东西必须使用相同的 STL 实现。

这在 Ports 中造成一些问题,因为少数库不能用 clang 编译,因此不能用 libc++,而另一些需要 C++11,因此不能用我们的基本系统 GCC 编译。使用 Ports 中的 GCC 也不能真正解决此问题,因为许多旧的 C++ 程序也不能用新 GCC 编译,而且新的 libstdc++ 与我们基本系统中的也不二进制兼容。

调试

clang 切换的另一个不幸问题是,clang 现在发出符合 DWARF 标准版本 4 的调试信息。不久,它将默认使用 DWARF 5,其中包含对更小调试信息表的支持、将调试信息在编译期间分离到单独文件中以便单独链接的支持。

不幸的是,我们基本系统中包含的旧版 GNU 调试器(GDB)只能支持 DWARF 2。对于 10,我们导入了 LLVM 调试器(LLDB),Ed Maste 一直在进行 FreeBSD 移植(FreeBSD 基金会资助)。LLDB 与 LLVM 的其余部分一样,非常模块化。它作为一组库提供,允许将调试功能添加到各种应用中,从命令行工具到 IDE。它主要由 Apple 开发,因此远程调试是设计的核心部分,允许从 x86 桌面调试 ARM 设备。

所有这些都是不错的特性,但不幸的是 LLDB 尚未完全准备好在 10 版本中默认启用。它在树中,所以欢迎你升级源代码并尝试构建最新版本。我们预计在 10.1 中默认启用它。

架构问题展望

如今,FreeBSD 有 tier 1 架构:x86(32 位和 64 位变体)。ARMv6 及更新版本非常接近 tier 1。这两者都得到 Clang 和 LLVM 后端良好支持。不幸的是,我们还有许多 tier 2 架构,比如 SPARC、PowerPC 和 MIPS,支持不那么好。LLVM 对 64 位 PowerPC 支持相当好,主要由 Argonne National Laboratory 开发,但对 32 位 PowerPC 支持远不如前者。自从 Apple 转向 Intel 以来,这些架构在消费 PC 市场已死,但在许多人们交付嵌入式 FreeBSD 衍生系统的地方仍然流行,尤其在汽车工业。

MIPS 后端现在能编译 LLVM 本身,考虑到代码库的规模和复杂性,这是相当了不起的成就,但要能编译 FreeBSD 内核还有一段路要走。

SPARC 和 IA64 是两个未来较不确定的架构。LLVM 中的 SPARC 后端是最古老之一,但仍然没有达到生产就绪状态。Itanium 后端在无人维护一段时间后被移除。Intel 似乎并没有大力推动 Itanium,Oracle 似乎将 SPARC 视为运行基于 Solaris 的 Oracle 设备的平台,因此这些架构的未来本身并不那么确定,但仍有系统在使用它们,在 FreeBSD 中放弃支持会是遗憾。

向前推进

这一切的目标是让 FreeBSD 成为现代开发平台。我们实现了这一点。FreeBSD 10 附带迄今为止任何系统中最完整的 C11 和 C++11(和 C++1y)实现。我们如今有现代编译器和 C++ 栈,上游社区活跃,作为消费者与 FreeBSD 紧密联系,还有若干人(包括我本人)同时为两个项目贡献。

我们仍然缺少若干组件以实现完全 BSD 许可的工具链。我们目前附带许多 GNU binutils。一些东西,比如 GNU 汇编器,容易替换。LLVM 库包含所有必需功能;只需编写小工具来实现它们。

例外是链接器。与编译器一样,链接器是相当复杂的软件。我们目前正在评估两个链接器以替换 GNU ld。第一个,MCLinker,最初由 MediaTek 使用 LLVM 库开发,现在有更大的社区。它目前是 Android SDK 中的链接器之一,能链接整个基本系统,但缺乏符号版本控制支持(在你阅读本文时可能已经完成,因为工作正在进行中)。

另一个选项是 lld,LLVM 链接器。这是更复杂的设计,尚未那么先进,但有一些大型企业支持者,比如 Sony(Sony 是 FreeBSD 消费者),因此可能是更好的长期前景。

无论我们选择哪个,FreeBSD 都将继续为任务挑选最佳工具。我们希望在 11.0 中默认提供完全 BSD 许可的工具链,并在 10.x 系列中作为可选组件。BSD 许可总是不错的,但我们不会在工具也变得更好之前切换。


David Chisnall 是剑桥大学计算机实验室的研究员,FreeBSD 核心团队成员。他也是其他几个开源项目的活跃贡献者,包括 LLVM、GNUstep 和 Étoilé。在编写代码之余,他写了几本书。不在电脑前时,他跳古巴 salsa 和阿根廷探戈。

最后更新于