常见问题解答 (FAQ)

许可证

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

可以。修改后的源代码分发必须保留版权声明,并遵循Apache License v2.0 with LLVM Exceptions中列出的条件。

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

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

源代码

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++)编写语言的编译器,有三种主要方法可以解决从前端生成 LLVM IR 的问题

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

  • 优点:最好跟踪 LLVM IR、.ll 语法和 .bc 格式的变化

  • 优点:能够在没有 emit/parse 开销的情况下运行 LLVM 优化过程

  • 优点:很好地适应 JIT 上下文

  • 缺点:需要编写大量难看的粘合代码

  1. 从你的编译器的本机语言发出 LLVM 汇编。

  • 优点:非常容易上手

  • 缺点:与中端交互时,.ll 解析器比位代码读取器慢

  • 缺点:跟踪 IR 的变化可能更难

  1. 从你的编译器的本机语言发出 LLVM 位代码。

  • 优点:与中端交互时,可以使用更高效的位代码读取器

  • 缺点:你需要在你的语言中重新设计 LLVM IR 对象模型和位代码写入器

  • 缺点:跟踪 IR 的变化可能更难

如果你选择第一个选项,include/llvm-c 中的 C 绑定将非常有帮助,因为大多数语言都对与 C 交互有很好的支持。从托管代码调用 C 最常见的障碍是与垃圾回收器交互。C 接口的设计几乎不需要内存管理,因此在这方面非常简单。

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

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

我不理解GetElementPtr指令。请帮忙!

参见经常被误解的 GEP 指令

使用 C 和 C++ 前端

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

不能。C 和 C++ 本质上是依赖于平台的语言。最明显的例子是预处理器。使 C 代码可移植的一种非常常见的方法是使用预处理器包含特定于平台的代码。在实践中,在预处理后会丢失有关其他平台的信息,因此结果本质上取决于预处理的目标平台。

另一个例子是sizeof。在不同平台上,sizeof(long)的值通常会有所不同。在大多数 C 前端中,sizeof会立即扩展为常量,从而硬编码特定于平台的细节。

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

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

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

如果你将<iostream>头文件包含到 C++ 翻译单元中,该文件可能会使用std::cin/std::cout/…全局对象。但是,C++ 并没有保证不同翻译单元中静态对象之间的初始化顺序,因此如果你的 .cpp 文件中的静态构造函数/析构函数使用了std::cout,例如,该对象不一定会在你使用之前自动初始化。

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

如果您希望更容易理解编译器在演示页面中生成的LLVM代码,请考虑使用printf()而不是iostream来打印值。

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

如果您正在使用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,这确保了它以正确的调用约定动态调用(因此,代码是完全定义良好的)。如果您通过内联器运行它,您将得到这个(显式的“或”是为了内联器不会删除大量内容)

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
}

在这里您可以看到,内联传递对@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将未定义的调用转换为不可达,因此我们最终会在一个条件上进行分支,该条件转到不可达:分支到不可达永远不会发生,因此“-inline -instcombine -simplifycfg”能够生成

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