llvm常见问题 (FAQ)
License
可以修改 LLVM 源代码并重新分发修改后的源代码吗?
可以修改 LLVM 源代码并重新分发基于二进制文件或其它工具,而无需重新分发源代码吗?
源代码
LLVM 是用什么语言编写的?
LLVM 源代码的可移植性如何?
使用什么 API,将值存储到 LLVM IR 的 SSA 表示中的一个虚拟寄存器?
源语言
支持哪些源语言?
想编写一个自托管的 LLVM 编译器。应该如何与 LLVM 中端优化器和后端代码生成器交互?
对用于构建编译器的更高级别的源语言结构有什么支持?
不明白GetElementPtr指令。帮助!
使用 C 和 C++ 前端
可以将 C 或 C++ 代码编译为独立于平台的 LLVM bitcode位码吗?
关于demo页面生成代码的问题
当#include <iostream>发生的事情?llvm.global_ctors以及_GLOBAL__I_a...是什么 ?
出现在代码中的这个“undef ”是什么?
“Go“代码在哪里使用?
为什么 instcombine +simplifiedcfg对不匹配的函数的调用变成“无法访问”?为什么不让验证者拒绝?
许可证license
可以修改 LLVM 源代码,重新分发修改后的源代码吗?
是的。修改后的源代码分发必须保留版权声明并遵循Apache License v2.0 with LLVM Exceptions 中列出的条件。
可以修改 LLVM 源代码,重新分发二进制文件或工具,而无需重新分发源代码吗?
是的。在比 GPL 限制更少的许可下分发 LLVM,如上面的第一个问题所述。
源代码
LLVM 是用什么语言编写的?
所有 LLVM 工具和库都是用 C++ 编写的,并广泛使用了 STL。
LLVM 源代码的可移植性如何?
LLVM 源代码应该可以移植到大多数modern Unix-like操作系统。LLVM 对 Windows 系统也有很好的支持。大多数代码是用标准 C++ 编写的,操作系统服务抽象支持库。构建和测试 LLVM 所需的工具已移植到大平台。
使用什么 API,将值存储到 LLVM IR 的 SSA 表示中的一个虚拟寄存器?
简而言之:不能。一旦理解了正在发生的事情,这实际上是一个愚蠢的问题。基本上,在代码中:
%result = add i32 %foo, %bar
%result只是给予一个add 指令Value。换句话说,%result 就是添加指令。“赋值”没有明确地将任何东西“存储”到任何“虚拟寄存器”;“ =”更像是数学意义上的相等。
详细解释:为了生成 IR 的文本表示,必须为每条指令指定某种名称,以便其它指令可以在文本上引用。但是,从 C++ 操作的同构内存中表示没有这样的限制,指令可以简单地保留指向Value引用的任何指针。事实上,虚拟编号临时变量的名称,如%1,根本没有在内存中明确表示(参见资料 Value::getName())。
源语言
支持哪些源语言?
LLVM 目前通过Clang完全支持 C 和 C++ 源语言。许多其它语言前端是使用 LLVM 编写的,在使用 LLVM 的项目中提供了一个不完整的列表 。
编写一个自托管的 LLVM 编译器。应该如何与 LLVM 中端优化器和后端代码生成器交互?
编译器前端将通过以 LLVM 中间表示 (IR) 格式,创建模块来与 LLVM 通信。用语言(不是 C++)编写编译器,有 3 种主要方法可以从前端生成 LLVM IR:
使用语言的 FFI(外部函数接口)调用 LLVM 库代码。
for:最佳跟踪对 LLVM IR、.ll 语法和 .bc 格式的更改
for:启用运行 LLVM 优化过程,无需发出/解析开销
for:很好地适应 JIT 上下文
against:要写很多丑陋的胶水代码
从编译器的本地语言发出 LLVM 汇编程序集。
for:非常容易上手
against:当连接到中间端时,.ll 解析器比bitcode位码读取器慢
against:跟踪 IR 的变化可能更难
从编译器的本地语言发出 LLVM 位码。
for:在与中端接口时,可以使用更高效的bitcode位码reader
against:必须用语言重新设计 LLVM IR 对象模型和位码编写器
against:跟踪 IR 的变化可能更难
如果选择第一个选项,include/llvm-c 中的 C 绑定应该会有很大帮助,因为大多数语言都强烈支持与 C 的接口。从托管代码调用 C 的最常见障碍,与垃圾收集器的接口。C 接口设计只需要很少的内存管理,在这方面很简单。
对用于构建编译器的更高级别的源语言结构,有什么支持?
目前,没有太多。LLVM 支持对代码有用的中间表示,但不支持大多数编译器所需的高级(抽象语法树)表示。没有用于词法或语义分析的工具。
不明白GetElementPtr指令。帮助!
请参阅 The Often Misunderstood GEP Instruction。
使用 C 和 C++ 前端
可以将 C 或 C++ 代码编译为独立于平台的 LLVM 位码吗?
不,C 和 C++ 本质上是依赖于平台的语言。最明显的例子是预处理器。使 C 代码具有可移植性的非常常见方法,使用预处理器来包含特定于平台的代码。实际上,预处理后,平台的信息会丢失,结果本质上取决于预处理所针对的平台。
另一个例子是sizeof。sizeof(long)在平台之间变化是很常见的。在大多数 C 前端,sizeof立即扩展为常量,从而硬连接特定于平台的细节。
此外,由于许多平台根据 C 定义它们的 ABI,并且由于 LLVM 比 C 级别低,因此前端当前必须发出platform-specific的 IR,以使结果符合平台 ABI。
关于demo页面生成代码的问题
当#include <iostream>发生的事情,llvm.global_ctors以及_GLOBAL__I_a...是什么?
如果#include的<iostream>header换成C ++翻译单元,该文件可能会使用std::cin/ std::cout/ ...全局对象。但是,C++ 不保证不同翻译单元中静态对象之间的初始化顺序,静态std::cout,例如,如果使用了 .cpp 文件中的静态 ctor/dtor ,该对象不一定会在使用自动初始化。
为了让std::cout和friends 在这些场景中正常工作,使用的 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 +simplifiedcfg 将对调用约定不匹配的函数的调用变成“无法访问”?为什么不让验证者拒绝?
这是使用自定义调用约定的前端作者遇到的一个常见问题:
需要确保在函数和每次调用函数时,都设置正确的调用约定。例如,这段代码:
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”。这通常会bites people难受,因为“他们所有的代码都消失了”。间接调用需要在调用者和被调用者上设置调用约定,所以人们经常问为什么不让验证者拒绝这种事情。
答案是这段代码有未定义的行为,但并不违法。如果将其设为非法,那么每个可能会创建的转换都必须确保不会发生,并且存在可以创建此类构造的有效代码(在死代码中)。可能导致这种情况发生的事情是相当contrived的,但仍然需要接受。下面是一个例子:
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/ falseinto bar,确保conv 正确的动态调用(因此,代码定义得很好)。如果通过内联程序运行,会得到这个(明确的“或”是存在的,这样内联程序就不会死代码,消除一堆东西):
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将 undefined 调用变为 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
}
参考链接:Frequently Asked Questions (FAQ