如何开始使用 LLVM C API 已翻译 100%

oschina 投递于 2015/01/22 07:34 (共 13 段, 翻译完成于 02-08)
阅读 4887
收藏 4
2
加载中

我热衷于把玩有意思的编程语言,以求能更好的理解编译器(并且最终深入其所依赖的机器底层)是如何运作的,也会去尝试那些不在我拿手技艺之列的技术。LLVM 非常棒,因为摆弄它并将其作为一个后端进行连接,以使其生成能在很多的平台上快速运行的代码. 如果我仅仅只是想看看我的代码时怎么执行的,我可能只会去用一用简单的手动解释器, 但一上手 LLVM 的 JIT, 它的优化套件,以及对平台的支持就像一台超级跑车 — 你的小玩意儿的表现也能令人印象深刻。另外,LLVM 是诸如 Emscripten 和 Rust 这些东西的基础, 因此我喜欢靠直觉来了解我所感兴趣的新技术是如何实现的.

我将要向你展示的,是如何使用 LLVM API 去以编程的方式构建一个函数,你可以想调用其它函数一样调用它,并且可以让其直接以你所用平台的机器语言形式运行。

LeoXu
翻译于 2015/01/22 21:15
2

在本例中,我将使用 C 的 API, 因为它在 LLVM 中是可以使用的,再加上一个 C++ 的 API, 就是起步的最简单方式了.  其它的语言也有到 LLVM API 的绑定 — Python, OCaml, Go, Rust — 但 LLVM 生成代码这一过程后面的原理在所有封装的API中都是一样的.

本例会稍微跳到编译器构建的中间阶段. 假设前端 (词法、语法分析器,类型检查器) 已经构建了一个 AST 并且我们现在正在为后端遍历中间形式的代码,让机器代码得到优化后被生成出来.

在这种情况,我们就只是输入了直线形式的过程代码,得到的函数会在一个AST遍历函数中被动态的拼凑在一起, 当遇到树中特定的节点时就会调用 LLVM 的 API.

LeoXu
翻译于 2015/01/22 21:38
2

针对本例,我们将会构建一个简单的加法函数,它会以两个整型数作为入参并返回他们的和,同等的C标识方式如下:

int sum(int a, int b) {
    return a + b;
}

我们要清除现在正在做的事情: 我们正使用 LLVM 构建这个函数在内存中的表现形式, 使用它的 API 来设置项函数的进入和退出,返回和参数类型,以及实际的加法指令这些东西. 一旦这一内存表现形式构建完成, 我们就可以指示 LLVM 跳转到它,并使用我们提供的参数来执行它, 就好像它是一个从像C这样的语言编译而来的可执行的东西.

点击这里查询最终的代码.

LeoXu
翻译于 2015/01/22 21:49
2

模块

第一步是要去创建一个模块. LLVM中一个模块就是一个由全局变量,函数,外部引用以及其它数据组成的集合. 这里的模块不怎么样比方说Python这样的语言中的模块, 它们并不提供独立的命名空间. 但它们是所有构建在LLVM中的东西的顶层容器, 因此我们从创建一个这样的模块开始.

LLVMModuleRef mod = LLVMModuleCreateWithName("my_module");

传入模块工厂函数的字符串 "my_module" 是你所选择的模块标识.

请注意当你正在浏览 LLVM C API 文档 时, 不同的方面会在不同的头包含下被组织在一起. 我在这里详细介绍的大多数东西,比如模块和函数,都包含在 Core.h头下, 而随着我们继续深入,我也将会涵盖其它的东西.

LeoXu
翻译于 2015/01/22 21:59
1

类型

接下来,我创建sum函数,并将其添加到模块中。一个函数会包含如下元素:

  • 它的类型 (返回类型),

  • 一个由其参数类型组成的向量, 以及

  • 一个基础块的集合.

稍后我会解释基础块. 首先,我们要处理函数的类型和参数类型 — 用C的术语说,就它的原型 — 并将其添加到模块中.

LLVMTypeRef param_types[] = { LLVMInt32Type(), LLVMInt32Type() };
LLVMTypeRef ret_type = LLVMFunctionType(LLVMInt32Type(), param_types, 2, 0);
LLVMValueRef sum = LLVMAddFunction(mod, "sum", ret_type);

LLVM 的类型对应我们的目标平台上的类型, 比如固定位宽的整型和浮点数, 指针, 结构,以及数组. (没有像C中那样的平台独立的类型,在C中,整型的大小, 32- 或者 64-位,是依赖于机器的架构的。)

LeoXu
翻译于 2015/01/22 22:07
1

LLVM类型拥有构造函数,并遵循“LLVM*TYPE*Type()”格式。在我们的例子中,传递到sum函数的参数,以及函数类型本身都是32位整型,所以我们可以使用LLVMInt32Type()。

按照顺序,传递到LLVMFunctionType()的参数如下:

1.函数的类型(返回类型)

2.函数的参数类型向量(函数的参数个数应该和数组中的类型个数相匹配)

3.函数的参数个数

4.一个boolean类型,表示函数是否是可变的,或者接受一个可变的参数

请注意,函数类型构造函数返回一个类型引用。这强化了一个概念,即我们在LLVM里面所做的等同于C中的函数原型声明。

这里的第三行增加了函数类型到模块,并命名为sum。我们获取到一个值引用,可以将它认作是代码(实际上是内存)中的固定位置,在它之上可以增加函数体,这是我们下面要做的。


gones945
翻译于 2015/02/04 22:50
1

基本块

下一步是增加基本块到函数。基本块是只有一个入口点和出口点的部分代码,换句话说,除了一步步按照一系列指令执行外,没有其它的方式来执行。没有if/else,while,loop,或任意类型的jump。基本块是模型控制流以及后续优化的关键,因此,LLVM具备增加这些到进展中的模块的一流支持。

LLVMBasicBlockRef entry = LLVMAppendBasicBlock(sum, "entry");

注意函数名中的“append”:它有助于我们了解,当运行中的代码块不断增加的时候,我们正在做什么。由此,相对于我们之前增加到模块中的函数,基本块是增加的。

gones945
翻译于 2015/02/05 23:03
1

指令创建者

与指令创建者相符合的概念,即我们如何增加指令到函数基本块。

LLVMBuilderRef builder = LLVMCreateBuilder();
LLVMPositionBuilderAtEnd(builder, entry);

类似于增加基本块到函数,我们设定创建者在基本块留下的入口编写指令。

gones945
翻译于 2015/02/05 23:19
1

LLVM中间表示

补充说明:LLVM的主要用途就是使用LLVM实现中间表示,或者简写为IR。我认为LLVM的中间表示是C语言和汇编语言之间的中间表示。LLVM中间表示采用的是一种定义非常严格的语言,这就意味着这种语言优化程度高,与平台无关,LLVM就是因这两个特性而出名的。你再查看一下中间表示,你就会明白每个指令是如何转换为最终生成汇编语言的装载、存储和跳转指令的。LLVM的中间表示可看作以下三种东西:

  • 一系列内存对象,我们所举的例子就是这样的。

  • 文本语言,就像汇编语言那样。

  • 一系列由简单的二进制编码所组成字节,或者称作位码。

你可以把clang或者其他工具所生成的LLVM中间表示看做文本语言或者位码。

几点人
翻译于 2015/02/07 12:19
1

回到我们刚才的示例。现在来到了我们加法函数的核心部分:最终将传递过来的两个整型的参数进行相加并返回结果给调用者的指令。

LLVMValueRef tmp = LLVMBuildAdd(builder, LLVMGetParam(sum, 0), LLVMGetParam(sum, 1), "tmp");
LLVMBuildRet(builder, tmp);

LLVMBuildAdd()持有编译器的引用,其中包括两个待相加的整数,一个提供返回结果的名字。(结果的名字是必须的,因为LLVM IR严格要求全部指令必须产生一个中间结果。这一点稍候可以在LLVM进一步简化或者被优化掉,但在当前生成指令过程中,我们先遵循它的约定。)显然我们希望进行相加的个数就是我们后面提供给编译器的参数,并且可以通过使用函数的参数中的LLVMGetParam()来获取:即对应我们所看到此方法中的第二个、第三个参数。

调用LLVMBuildRet()即可生成返回声明,以及相加指令执行后返回临时结果的序列。

暗夜在火星
翻译于 2015/02/07 22:07
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(0)

返回顶部
顶部