通过 LLVM IR 看语言特性(1)

时间:2022-07-28
本文章向大家介绍通过 LLVM IR 看语言特性(1),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

前言

本系列文章会展示一些系列源码到 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
  • 编译
    • 解析预处理后的文件,构建 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. 申请一块空间
  2. 1 存到这块空间

由此可见,本例中,在编译阶段,编译器就已经将 1.23456 转化为 1

扩展阅读

揭秘 @available

http://llvm.org/docs/LangRef.html

http://llvm.org/docs/LangRef.html#abstract