LLVM 懂点东西

为啥

最近看了clang的编译,突然看到了LLVM 这个编译框架,感觉能搞点事情,先占个坑

LLVM

https://www.bookstack.cn/read/clang-llvm/llvm-docs.md

https://zhuanlan.zhihu.com/p/100241322

LLVM根据编译的三段式设计:前端 -》 IR -》 后端(执行文件)

根据(中间码)IR 语言生成后端执行文件,提出了一种面向于现代的编译器架构,
在架构中分成三个部分,
clang 属于前端到IR的编译器阶段,LLVM将IR编译成目标平台的执行文件,

由此看来如果要开发一个新的语言只要实现从前端到IR即可,IR到目标平台由LLVM保证。

这一点和jvm有点类似,java编译器将java源代码编译成中间字节码class,然后放在jvm上解释执行, jvm语言也是实现了前端到class文件,不过后端是在虚拟机上执行的,并不是后端编译成目标文件.

为啥要这样搞,其实还是因为gcc 太过耦合,导致增加新的语言特性很麻烦,从llvm的角度,IR 实现了前后端分离,新的语言特性可以在前端实现, 如果有新的指令集 就在后端增加,将IR翻译成新的后端指令.

在以前的场景中都是代码前端编译成c 这种语言,然后c编译成目标语言来运行在目标平台,早期的go,python 等,现在由llvm 来做这种事情了,代码前端编译成IR,llvm后端编译成目标平台,这些目前应用场景在新的cpu架构上较多,比如rsic V,GPU上.

果然映了那句话, 计算机世界的问题都可以用分层来解决( ;´Д`)

IR 语言

类型

  1. 内存不可读
  2. 二进制 .bc文件 clang -emit-llvm MacJia.c -c -o MacJia.bc 将c源代码 编译成二进制文件
  3. 用户可读的 .ll汇编文件,clang -emit-llvm MacJia.c -S -o MacJia.ll 将c源代码编译成IR语言的格式. 并且使用3级优化

当然可以用llvm-dis 工具将二进制反编译成ll汇编语言
llvm-dis MacJia.bc MacJiaDecompile.ll

IR 中间码格式

MacJia.c源代码如下

#include<stdio.h>
#include<stdlib.h>
int main(){
    printf("woca");
    return 0;
}

使用clang -emit-llvm MacJia.c -S -o MacJiao0.ll0级优化 编译成的MacJiao0.ll文件大致如下:

; ModuleID = 'MacJia.c'
source_filename = "MacJia.c"
target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx11.0.0"

@.str = private unnamed_addr constant [5 x i8] c"woca\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i64 0, i64 0))
  ret i32 0
}
declare i32 @printf(i8*, ...) #1

attributes #0 = { noinline nounwind optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "min-legal-vector-width"="0" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { "correctly-rounded-divide-sqrt-fp-math"="false" "darwin-stkchk-strong-link" "disable-tail-calls"="false" "frame-pointer"="all" "less-precise-fpmad"="false" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "probe-stack"="___chkstk_darwin" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+cx8,+fxsr,+mmx,+sahf,+sse,+sse2,+sse3,+sse4.1,+ssse3,+x87" "unsafe-fp-math"="false" "use-soft-float"="false" }

!llvm.module.flags = !{!0, !1, !2}
!llvm.ident = !{!3}

!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 11, i32 1]}
!1 = !{i32 1, !"wchar_size", i32 4}

上面的ll文件 开头的两个固定字段:这些东西其实是编译器根据平台自动填写的,如果要在别的平台,就要改这两个值了.
1. datalayout 是规定大小端,int 占几个字节,double占几个字节,char啥的,
2. triple是设定目标平台

我们主要看一下main方法: 返回值是i32,里面还调用了一个pringf 方法

define i32 @main() #0 {
  %1 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  %2 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([5 x i8], [5 x i8]* @.str, i64 0, i64 0))
  ret i32 0
}
declare i32 @printf(i8*, ...) #1

IR 语法介绍

  • 数据格式
    在target 里面可以规定,和c类似,不同编译器不同用法,以具体为准,i32 为int,i8 为char, double 为double 等

  • 数据赋值
    alloca: 申请空间 %2 = alloca i32, align 4 变量%2申请int32
    store: 赋值 store i32 10, i32* %2, align 4 变量%2 赋值为10
    load: 加载值 %5 = load i32, i32* %3, align 4 把变量%3 的值赋给%5

    上面其实源码就是 int i=10; a=i;

  • 运算:加减乘除,向量等
    add
    sub
    mul
    div
    rem
    fmul

  • 控制语句
    br: 条件判断跳转, 类似三元表达式br i1 %6, label %7, label %13,为真就跳转到label 7,为假就跳到label 13,
    switch: 就switch
    ret: 返回 ret i32 0 return 0;
    icmp: 比较 %6 = icmp slt i32 %5, 10 signed less then ,就变量%5是否小于10, i<10

  • 条件
    and
    or
    xor

  • 结构体
    struct RT{char A; int B[10][20]; char C};
    struct ST{jint X,double Y,struct RT Z};
    %struct.RT = type { i8, [10 x [20 x i32]], i8 }
    %struct.ST = type { i32, double, %struct.RT }

  • 上面都是简单的运算,其实IR 给了很多高端指令https://llvm.org/docs/LangRef.html 可以看这个.

通过上面的介绍其实可以很容易阅读下面的编译出来的IR的代码

每次都是先申请空间,然后赋值

for 和while 或者函数都是跳转使用br 跳转,

跳转都要先load 入参,然后 进行计算

条件运算都是 先icmp 返回0,1 ,然后通过br来进行流程跳转.

最后ret 返回值

int main(){
  int a=10;
  int i=20;
  for (i=0;i<10;i++){
    a=a*2;
  }
 return 0;
}
define i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  %3 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 10, i32* %2, align 4
  store i32 20, i32* %3, align 4
  store i32 0, i32* %3, align 4
  br label %4

4:                                                ; preds = %10, %0
  %5 = load i32, i32* %3, align 4
  %6 = icmp slt i32 %5, 10
  br i1 %6, label %7, label %13

7:                                                ; preds = %4
  %8 = load i32, i32* %2, align 4
  %9 = mul nsw i32 %8, 2
  store i32 %9, i32* %2, align 4
  br label %10

10:                                               ; preds = %7
  %11 = load i32, i32* %3, align 4
  %12 = add nsw i32 %11, 1
  store i32 %12, i32* %3, align 4
  br label %4

13:                                               ; preds = %4
  ret i32 0
}

AST

https://zhuanlan.zhihu.com/p/51174224


评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注