> For the complete documentation index, see [llms.txt](https://freebsd-journal-cn.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://freebsd-journal-cn.bsdcn.org/2014-0102-freebsd-10/clang-in-10.md).

# 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()` 类型限定符声明，例如：

```c
_Atomic(int) x;
```

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

```c
x += 1;
x++;
x = x + 1;
```

前两行会是顺序一致的原子自增。最后一行则是一次顺序一致的原子读、一次加法，然后一次原子存储。顺序一致意味着所有原子操作在全局以相同顺序可见。例如，如果一个核心自增 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 在以 `clang` 或 `cc` 调用时默认使用 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 和阿根廷探戈。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://freebsd-journal-cn.bsdcn.org/2014-0102-freebsd-10/clang-in-10.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
