本系列文章会展示一些系列源码到 LLVM IR 语言的转换。目标是让我们更好的理解编译器是怎么运作的。
首先,我们先从一个最简单的问题开始:我们都知道下面 i 值会因为类型转换变为 1。那么,这种类型转换是如何发生的?
int i = 1.23456;// i=1;
通常来说,它可能是通过下面的一种或者几种方式进行的。下面,我们会通过转换 LLVM IR 的方式进行验证。
1.23456
转为 1
吗[obj aMethod]
都会被翻译成 objc_msgSend(obj, sel/*@selector(aMethod)*/)
一样)1.23456
转化为 1
解答上面的疑问前,为了对新人友好一些,我们还是先回顾一下编译阶段的组成:
#includes
#defines
很明显,所有的源码都会在编译阶段转为 LLVM IR。
LLVM IR 是 LLVM intermediate representation (llvm 中间表示)的简称。
LLVM 除了是一个开源的编译器外,还代表一种基于静态单赋值(SSA)的语言,可以提供类型安全、低级操作、灵活性和代表所有“高级语言”的能力。
这门语言的语法很简单,我们会在后续的文章中逐渐介绍它的一些语法。
首先,我们先通过 clang -S -emit-llvm main.c
命令将文章开头的代码转为 LLVM IR
语言:
// clang -S -emit-llvm main.c
int main() {
int i = 1.23456;
}
我们重点看一下第7行至10行。
我们重点看一下第7行至10行。
define dso_local i32 @main() #0
define
代表这里定义了一个函数dso_local
是运行时抢占说明符(Runtime Preemption Specifiers
),可以先忽略。i32
代表32位整型,与 C 语言类似,它的返回类型在函数名之前。@main
代表函数名。
LLVM 标识符有两种基本类型:全局和本地。全局标识符(函数、全局变量)以 @
字符开头。本地标识符(寄存器名、类型)以 %
字符开头。#0
代表属性组。
虽然我们只是简单的定义了一个 main
函数。但是,对于编译器,这个函数具有大量的属性。本例中,它的属性是 { noinline nounwind optnone uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-frame-pointer-elim"="false" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
。
相信读者很快就能发现,它实际上就是第 13 行的内容。
因为函数的属性很长,又加上很多函数的属性都一样。为了保持可读性,LLVM IR
使用属性组来替代重复出现的属性。%1 = alloca i32, align 4
%1
代表一个本地变量。我们前面已经提到过 %
代表本地标识符。alloca
代表一个内存指令。alloca
指令表示在当前执行的函数的栈帧上分配内存,当此函数返回其调用方时自动释放内存。i32
代表 alloca
申请了一个32位整型大小空间align 4
代表 alloca
申请的地址会落在 4
的边界上store i32 1, i32* %1, align 4
store
同样是一个内存指令。它标志将值
存到某个地址
。i32 1
代表被存储的值 是32位整形 1。i32* %1
代表地址是前面在栈中申请的位置。ret i32 0
ret
是为了将控制权返回调用方。这里是将 整数0 返回给调用方。
简单总结一下上面的流程:
1
存到这块空间由此可见,本例中,在编译阶段,编译器就已经将 1.23456
转化为 1
http://llvm.org/docs/LangRef.html
http://llvm.org/docs/LangRef.html#abstract