前言
本系列文章会展示一些系列源码到 LLVM IR 语言的转换。目标是让我们更好的理解编译器是怎么运作的。
基本类型转换是如何发生的?
首先,我们先从一个最简单的问题开始:我们都知道下面 i 值会因为类型转换变为 1。那么,这种类型转换是如何发生的?
代码语言:javascript复制int i = 1.23456;// i=1;
通常来说,它可能是通过下面的一种或者几种方式进行的。下面,我们会通过转换 LLVM IR 的方式进行验证。
- 程序运行时,通过特殊的指令处理将
1.23456
转为1
吗 - 程序运行时,调用某些基础库方法(就像
[obj aMethod]
都会被翻译成objc_msgSend(obj, sel/*@selector(aMethod)*/)
一样) - 程序编译阶段,编译器就已经将
1.23456
转化为1
编译阶段的组成
解答上面的疑问前,为了对新人友好一些,我们还是先回顾一下编译阶段的组成:
- 预编译 对源码执行预处理操作,比如展开
#includes
#defines
- 编译
- 解析预处理后的文件,构建 AST(源码中间语言)
- 根据 AST 产出 LLVM IR(编译中间语言)
- 编译后端 根据目标机器特性,产出汇编码(可读性高于机器码)
- 汇编 将汇编码转化为机器码
- 链接 将多个对象文件组装为单个可执行文件
LLVM IR 是什么?
很明显,所有的源码都会在编译阶段转为 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行。
- 第 7 行
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
使用属性组来替代重复出现的属性。
- 第 8 行
%1 = alloca i32, align 4
%1
代表一个本地变量。我们前面已经提到过%
代表本地标识符。alloca
代表一个内存指令。alloca
指令表示在当前执行的函数的栈帧上分配内存,当此函数返回其调用方时自动释放内存。i32
代表alloca
申请了一个32位整型大小空间align 4
代表alloca
申请的地址会落在4
的边界上
- 第 9 行
store i32 1, i32* %1, align 4
store
同样是一个内存指令。它标志将值
存到某个地址
。i32 1
代表被存储的值 是32位整形 1。i32* %1
代表地址是前面在栈中申请的位置。- align 4` 同样代表这个操作必须是按照4对齐的
- 第 10 行
ret i32 0
ret
是为了将控制权返回调用方。这里是将 整数0 返回给调用方。
简单总结一下上面的流程:
- 申请一块空间
- 将
1
存到这块空间
由此可见,本例中,在编译阶段,编译器就已经将 1.23456
转化为 1
扩展阅读
揭秘 @available
http://llvm.org/docs/LangRef.html
http://llvm.org/docs/LangRef.html#abstract