常见问题解答 (FAQ)

许可证

我可以修改 LLVM 源代码并重新发布修改后的源代码吗?

是的。修改后的源代码分发必须保留版权声明,并遵循 带有 LLVM 例外的 Apache License v2.0 中列出的条件。

我可以修改 LLVM 源代码并重新发布基于它的二进制文件或其他工具,而无需重新发布源代码吗?

是的。这就是为什么我们在比 GPL 限制更少的许可证下分发 LLVM,如上面第一个问题中所述。

我可以使用 AI 编码工具(如 GitHub Copilot)来编写 LLVM 补丁吗?

是的,只要最终作品可以根据项目许可证获得许可,如 LLVM 开发者策略 中所述。使用 AI 工具复制受版权保护的作品并不能消除其版权,也不会授予您重新许可它的权利。

源代码

LLVM 是用什么语言编写的?

所有 LLVM 工具和库都是用 C++ 编写的,并广泛使用了 STL。

LLVM 源代码的可移植性如何?

LLVM 源代码应该可以移植到大多数现代类 Unix 操作系统。LLVM 在 Windows 系统上也具有出色的支持。大多数代码是用标准 C++ 编写的,操作系统服务被抽象到一个支持库中。构建和测试 LLVM 所需的工具已被移植到许多平台。

我使用什么 API 将值存储到 LLVM IR 的 SSA 表示中的虚拟寄存器之一?

简而言之:你不能。一旦你理解了正在发生的事情,这实际上是一个有点愚蠢的问题。基本上,在像这样的代码中

%result = add i32 %foo, %bar

%result 只是赋予 add 指令的 Value 的名称。换句话说,%result *是* add 指令。“赋值” 并没有显式地将任何东西 “存储” 到任何 “虚拟寄存器”;“=” 更像是数学意义上的相等。

更长的解释:为了生成 IR 的文本表示,必须给每个指令一个名称,以便其他指令可以在文本上引用它。但是,您可以从 C++ 操作的同构内存表示没有这样的限制,因为指令可以简单地保留指向它们引用的任何其他 Value 的指针。实际上,像 %1 这样的虚拟编号临时变量的名称根本没有在内存表示中显式表示(参见 Value::getName())。

源语言

支持哪些源语言?

LLVM 目前通过 Clang 完全支持 C 和 C++ 源语言。许多其他语言前端已经使用 LLVM 编写,不完整的列表可在 使用 LLVM 的项目 中找到。

我想编写一个自托管的 LLVM 编译器。我应该如何与 LLVM 中端优化器和后端代码生成器交互?

您的编译器前端将通过在 LLVM 中间表示 (IR) 格式中创建模块来与 LLVM 通信。假设您想用该语言本身(而不是 C++)编写您的语言的编译器,那么有 3 种主要方法可以解决从前端生成 LLVM IR 的问题

  1. 使用您语言的 FFI(外部函数接口)调用 LLVM 库代码。

  • 优点: 最好地跟踪对 LLVM IR、.ll 语法和 .bc 格式的更改

  • 优点: 能够运行 LLVM 优化 pass,而无需 emit/parse 开销

  • 优点: 很好地适应 JIT 环境

  • 缺点: 需要编写大量丑陋的胶水代码

  1. 从您的编译器原生语言发出 LLVM 汇编代码。

  • 优点: 非常容易上手

  • 缺点: 在与中间端接口时,.ll 解析器比 bitcode 读取器慢

  • 缺点: 可能更难跟踪对 IR 的更改

  1. 从您的编译器原生语言发出 LLVM bitcode。

  • 优点: 在与中间端接口时可以使用更高效的 bitcode 读取器

  • 缺点: 您将不得不在您的语言中重新设计 LLVM IR 对象模型和 bitcode 编写器

  • 缺点: 可能更难跟踪对 IR 的更改

如果您选择第一个选项,则 include/llvm-c 中的 C 绑定应该会有很大帮助,因为大多数语言都对与 C 接口有强大的支持。从托管代码调用 C 最常见的障碍是与垃圾回收器接口。C 接口旨在只需要非常少的内存管理,因此在这方面很简单。

构建编译器的高级源语言结构有哪些支持?

目前,没有太多。LLVM 支持一种中间表示,它对于代码表示很有用,但不支持大多数编译器需要的高级(抽象语法树)表示。没有用于词法分析或语义分析的工具。

我不理解 GetElementPtr 指令。求助!

请参阅 常被误解的 GEP 指令

使用 C 和 C++ 前端

我可以将 C 或 C++ 代码编译为平台无关的 LLVM bitcode 吗?

否。C 和 C++ 本质上是平台相关的语言。最明显的例子是预处理器。C 代码实现可移植性的一个非常常见的方法是使用预处理器来包含特定于平台的代码。实际上,在预处理之后,有关其他平台的信息会丢失,因此结果本质上依赖于预处理所针对的平台。

另一个例子是 sizeofsizeof(long) 在不同平台之间变化是很常见的。在大多数 C 前端中,sizeof 会立即扩展为常量,从而硬编码特定于平台的细节。

此外,由于许多平台根据 C 定义其 ABI,并且由于 LLVM 比 C 低级,因此前端目前必须发出特定于平台的 IR,以使结果符合平台 ABI。

关于演示页面生成的代码的问题

当我在 #include <iostream> 时,出现的 llvm.global_ctors_GLOBAL__I_a... 是什么?

如果您在 C++ 翻译单元中 #include<iostream> 头文件,则该文件可能会使用 std::cin/std::cout/… 全局对象。但是,C++ 不保证不同翻译单元中静态对象之间的初始化顺序,因此,例如,如果您的 .cpp 文件中的静态 ctor/dtor 使用了 std::cout,则该对象不一定会在使用前自动初始化。

为了使 std::cout 及其友元在这些场景中正常工作,我们使用的 STL 声明了一个静态对象,该对象在每个包含 <iostream> 的翻译单元中创建。此对象具有静态构造函数和析构函数,可在全局 iostream 对象可能在文件中使用之前初始化和销毁它们。您在 .ll 文件中看到的代码对应于构造函数和析构函数注册代码。

如果您想更轻松地理解演示页面中编译器生成的 LLVM 代码,请考虑使用 printf() 而不是 iostreams 来打印值。

我的所有代码都去哪儿了??

如果您正在使用 LLVM 演示页面,您可能经常想知道您输入的所有代码发生了什么。请记住,演示脚本正在通过 LLVM 优化器运行代码,因此如果您的代码实际上没有做任何有用的事情,那么它可能会全部被删除。

为了防止这种情况发生,请确保实际需要该代码。例如,如果您正在计算某个表达式,请从函数返回该值,而不是将其留在局部变量中。如果您真的想约束优化器,您可以读取和分配给 volatile 全局变量。

我的代码中出现的 “undef” 是什么?

undef 是 LLVM 表示未定义值的方式。如果您在使用变量之前未对其进行初始化,则可能会得到这些值。例如,C 函数

int X() { int i; return i; }

被编译为 “ret i32 undef”,因为 “i” 从未指定值。

为什么 instcombine + simplifycfg 会将对调用约定不匹配的函数的调用变成 “unreachable”?为什么不让验证器拒绝它?

这是使用自定义调用约定的前端作者经常遇到的问题:您需要确保在函数和对函数的每次调用上都设置正确的调用约定。例如,此代码

define fastcc void @foo() {
    ret void
}
define void @bar() {
    call void @foo()
    ret void
}

被优化为

define fastcc void @foo() {
    ret void
}
define void @bar() {
    unreachable
}

… 使用 “opt -instcombine -simplifycfg”。这经常困扰人们,因为 “他们的所有代码都消失了”。在调用方和被调用方上设置调用约定是间接调用工作所必需的,因此人们经常问为什么不让验证器拒绝此类事情。

答案是此代码具有未定义的行为,但它不是非法的。如果我们使其非法,那么每个可能创建此代码的转换都必须确保它不会这样做,并且存在可以创建此类构造的有效代码(在死代码中)。可能导致这种情况发生的事情是相当人为的,但我们仍然需要接受它们。这是一个例子

define fastcc void @foo() {
    ret void
}
define internal void @bar(void()* %FP, i1 %cond) {
    br i1 %cond, label %T, label %F
T:
    call void %FP()
    ret void
F:
    call fastcc void %FP()
    ret void
}
define void @test() {
    %X = or i1 false, false
    call void @bar(void()* @foo, i1 %X)
    ret void
}

在此示例中,“test” 始终将 @foo/false 传递到 bar 中,这确保了它以正确的调用约定动态调用(因此,代码是完全明确定义的)。如果您通过内联器运行此代码,您会得到这个(显式的 “or” 在那里是为了内联器不会死代码消除一堆东西)

define fastcc void @foo() {
    ret void
}
define void @test() {
    %X = or i1 false, false
    br i1 %X, label %T.i, label %F.i
T.i:
    call void @foo()
    br label %bar.exit
F.i:
    call fastcc void @foo()
    br label %bar.exit
bar.exit:
    ret void
}

在这里您可以看到,内联 pass 对 @foo 进行了未定义的调用,并且调用约定错误。我们真的不希望让内联器必须了解此类事情,因此它需要是有效的代码。在这种情况下,死代码消除可以轻松地删除未定义的代码。但是,如果 %X@test 的输入参数,则内联器将产生此结果

define fastcc void @foo() {
    ret void
}

define void @test(i1 %X) {
    br i1 %X, label %T.i, label %F.i
T.i:
    call void @foo()
    br label %bar.exit
F.i:
    call fastcc void @foo()
    br label %bar.exit
bar.exit:
    ret void
}

关于这一点有趣的是,%X *必须* 为 false 代码才能明确定义,但是再多的死代码消除也无法删除损坏的调用作为不可达代码。但是,由于 instcombine/simplifycfg 将未定义的调用变成 unreachable,因此我们最终得到一个分支,该分支在条件为 unreachable 时跳转到 unreachable:永远不会发生分支到 unreachable 的情况,因此 “-inline -instcombine -simplifycfg” 能够产生

define fastcc void @foo() {
   ret void
}
define void @test(i1 %X) {
F.i:
   call fastcc void @foo()
   ret void
}