为啥
最近看了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 语言
类型
- 内存不可读
- 二进制 .bc文件
clang -emit-llvm MacJia.c -c -o MacJia.bc
将c源代码 编译成二进制文件 - 用户可读的 .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.ll
0级优化 编译成的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
发表回复