本系列文章会展示一些系列源码到 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() #0define 代表这里定义了一个函数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