LLVM学习
解释型语言
- vim 创建一个py文件
- 写完一个简单的print("hello word")后保存退出
- 打印内容结果就出来的
编译型语言
- 终端vi hello.c 后写入一些简单的c语句打印。
-
现在出现了这两个文件
-
执行这个hello.c
从上面的例子中我们发现解释型语言可以直接编译,编译型语言需要先编译成机器可执行的执行文件后才能执行。
- 这个就是我们编译c文件的编译器。
LLVM概述
LLVM是构架编译器的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)、以及空闲时间(idle-time),对开发保持开发,并且兼容已有的脚步。 LLVM计划启动于2000年,最初由美国UIUC大学的Chris Lattner博主主持研发的。2006年Chris Lattner加盟Apple Inc. 并致力于LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。 目前LLVM已经被Apple、Facebook、Google等公司采用。
1.传统的编译器设计
1.1 编译器前端(Frontend)
编译器前端的任务是解析源代码,进行词法分析、语法分析、语义分析,检查源代码是否存在错误,然后会构建为抽象语法树(Abstract Syntax Tree,AST),LLVM的前端还会生成中间代码(intermediate representation,IR)
1.2 优化器(Optimizer)
优化器负责进行各种优化。改善代码的大小与运行时间,例如消除冗余的计算。业务逻辑代码中有大量的冗余代码,优化这些冗余代码。
1.3 后端(Backend)
将代码映射到目标指令集.生成机器语言,并且进行机器相关的代码优化
传统的编译器就是这样的三部分组成。
2.苹果的编译器设计
Ojective C/C/C++使用的编译器前端是Clang,Swift是Swift编译器,后端都是LLVM。
3.LLVM的设计
- LLVM编译器不同与传统的编译器。他的优点在于。
LLVM也分三个阶段,但是设计上略微的有些区别, LLVM不同的就是对于不同的语言它都提供了同一种中间表示(IR): 前端可以使用不同的工具对代码进行词法分析、语法分析、语义分析,生成抽象语法树。然后转为LLVM的中间表示,中间部分的优化器通过一系列的pass对IR做优化,后端负责将优化好的IR解释成对应不同架构的机器码。所以LLVM可以为任何编程语言编写独立的前端以及后端。 即前后端分离 高级语言越来越多,终端设备也越来越多,LLVM则利用了一个通用的中间层IR,所以最后只要出现一个新的高级语言,我们只需要研发一个高级语言对应的前端,一个新的设备出现,只需要研发一个新的后端,就可以了。LLVM能适应不同的语言不同的设备。
3.1 Clang与LLVM的关系
Clang是一个C++编写的,基于LLVM架构的轻量级编译器,发布于LLVM BSD许可证下的C/C++/Objective-C/Objective-C++编译器。诞生之初是为了替代GCC编译器,相比GCC而言,它的编译速度快、占用内存小、更加方便进行二次开发。 它属于整个LLVM架构中的编译器前端。
练习流程
4.LLVM编译流程
LLVM编译一个源文件的过程:预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件 我们可以通过命令行工具打印出源码的编译主要流程
- 创建一个简单的main.m 文件
-
终端输入指令 clang -ccc-print-phases main.m 后打印
-
1.
输入文件(input):找到源文件读取代码 -
2.
预处理阶段(preprocessor):这个过程处理宏定义的替换、头文件的导入 -
3.
编译阶段(compiler):进行词法分析(生成Token)、语法分析(生成AST),最终生成中间代码IR -
4.
后端(backend):这里LLVM会通过一个一个的Pass去优化,每一个节点(Pass)做一些事情,最终生成汇编代码 -
5.
汇编(assembler):生成目标文件.o -
6.
链接(linker):链接需要的动态库和静态库(系统的动态库与静态库在共享缓存中),生成可执行文件镜像文件image -
7.
绑定不同的架构(bind-arch):,生成对应的可执行文件
分别分析不同阶段做了什么事情
预处理阶段(preprocessor)
-
终端输入
clang -E main.m >> main1.m生成一个main1.m文件 查看这个文件。 -
这个是另一个测试文件,测试 typedef int HK_INT_64;取别名并不是宏定义的一部分。
结论: 这个过程处理宏定义的替换、头文件的导入
编译阶段(compiler)
- 词法分析
- 终端输入
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
总结 :这里就是词法分析,预处理之后就会进行词法分析,这里会把代码切成一个个Token,比如大小括号,等于号和字符串等。
- 语法分析
- 终端输入
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
这一段就是main函数的语法分析,我们可以清晰的看到 FunctionDecl 是方法的意思,main方法 ParmVarDecl 是传入的参数的意思。可以看到传入了第一个参数argc和第二个参数argv.
-CompoundStmt 0x7f8ca0861028 <col:41, line:22:1> 是复合语句的意思,从当前行第41开始到 22行第1个结束。
- DeclStmt 复合语句中的第一个局部变量a,和局部变量b。地址完全是一样的。
这个是函数的调用调用了printf函数。说明函数的指针,输入类型以及返回类型。
函数的第一个参数
这里是加法运算,a+b的结果作为第一个参数,在+30.
这里是return 返回一个0
总结:它的任务就是验证程序的语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树(AST),语法分析程序判断源程序在结构上是否正确
-
生成中间代码IR(intermediate representation )
-
这里修改了一下源文件,加了一个函数的调用。
-
终端输入
clang -S -fobjc-arc -emit-llvm main.m
生成一个main.ll文件使用VSCode 打开
- IR的基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用函数
ret 返回
复制代码
- 源码
; ModuleID = 'main.m'
source_filename = "main.m"
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 [3 x i8] c"%d\00", align 1
; Function Attrs: noinline nounwind optnone ssp uwtable
define i32 @test(i32 %0, i32 %1) #0 { //这里是test函数, test(int a,int b)
%3 = alloca i32, align 4 //开辟32位的空间4字节对齐 int %3 局部变量
%4 = alloca i32, align 4 //开辟32位的空间4字节对齐 int %4 局部变量
store i32 %0, i32* %3, align 4 //写入内存 %3 = a 4字节对齐 把传入的值写入内存
store i32 %1, i32* %4, align 4 //写入内存 %4 = b 4字节对齐 把传入的值写入内存
%5 = load i32, i32* %3, align 4 //取出数据 %5 = %3
%6 = load i32, i32* %4, align 4 //取出数据 %6 = %4
%7 = add nsw i32 %5, %6 //加法运算 %7 = %5 + %6
%8 = add nsw i32 %7, 3 //加法运算 %8 = %7 + 3
ret i32 %8 //返回%8
}
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32 %0, i8** %1) #1 {
%3 = alloca i32, align 4
%4 = alloca i32, align 4
%5 = alloca i8**, align 8
%6 = alloca i32, align 4
store i32 0, i32* %3, align 4
store i32 %0, i32* %4, align 4
store i8** %1, i8*** %5, align 8
%7 = call i32 @test(i32 1, i32 2) //调用了test函数
store i32 %7, i32* %6, align 4
%8 = load i32, i32* %6, align 4
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 %8)
ret i32 0
}
declare i32 @printf(i8*, ...) #2
attributes #0 = { noinline nounwind optnone ssp uwtable "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { noinline optnone ssp uwtable "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 12, i32 0]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 13.0.0 (clang-1300.0.29.3)"}
- IR的优化
LLVM的优化级别分别是-O0 -O1 -O3 -Os -Ofast -Oz
-
-O3:最快,但是包最大
-
-Os:是xcode默认的优化级别,它平衡了包体积大小与速度
-
-Oz:加载慢,但是包体积小,它内部是将多个函数汇编代码相同的指令放到一个新的函数 我们可以通过以下命令行去查看优化之后的IR
-
可以使用优化终端输入
clang Os -S -fobjc-arc -emit-llvm main.m -o main.ll -
这是优化后的发现这个test函数是相当的精简同时main函数中的调用test函数直接拿到了结构6
; ModuleID = 'main.m'
source_filename = "main.m"
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 [3 x i8] c"%d\00", align 1
; Function Attrs: norecurse nounwind optsize readnone ssp uwtable willreturn
define i32 @test(i32 %0, i32 %1) local_unnamed_addr #0 {
%3 = add i32 %0, 3
%4 = add i32 %3, %1
ret i32 %4
}
; Function Attrs: nofree nounwind optsize ssp uwtable
define i32 @main(i32 %0, i8** nocapture readnone %1) local_unnamed_addr #1 {
%3 = tail call i32 (i8*, ...) @printf(i8* nonnull dereferenceable(1) getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i64 0, i64 0), i32 6) #3, !clang.arc.no_objc_arc_exceptions !9
ret i32 0
}
; Function Attrs: nofree nounwind optsize
declare noundef i32 @printf(i8* nocapture noundef readonly, ...) local_unnamed_addr #2
attributes #0 = { norecurse nounwind optsize readnone ssp uwtable willreturn "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #1 = { nofree nounwind optsize ssp uwtable "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #2 = { nofree nounwind optsize "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"="true" "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" "tune-cpu"="generic" "unsafe-fp-math"="false" "use-soft-float"="false" }
attributes #3 = { optsize }
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [2 x i32] [i32 12, i32 0]}
!1 = !{i32 1, !"Objective-C Version", i32 2}
!2 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!3 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!4 = !{i32 1, !"Objective-C Garbage Collection", i8 0}
!5 = !{i32 1, !"Objective-C Class Properties", i32 64}
!6 = !{i32 1, !"wchar_size", i32 4}
!7 = !{i32 7, !"PIC Level", i32 2}
!8 = !{!"Apple clang version 13.0.0 (clang-1300.0.29.3)"}
!9 = !{}
- bitCode
xcode以后开启bitcode苹果会进一步的优化,生成.bc的中间代码,我们可以优化后的IR代码生成.bc代码,这里是进一步优化
- 终端输入
clang -emit-llvm -c main.ll -o main.bc
- 生成汇编代码
我们可以通过最终的.bc或者.ll代码生成汇编代码
- 终端输入 对比一下结果
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main1.s
clang -S -fobjc-arc main.m -o main2.s
-
对比结果 bc和ll的优化是一致的
-
这是main2.s
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 11, 0 sdk_version 12, 0
.globl _test ## -- Begin function test
.p2align 4, 0x90
_test: ## @test
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
movl %edi, -4(%rbp)
movl %esi, -8(%rbp)
movl -4(%rbp), %eax
addl -8(%rbp), %eax
addl $3, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $32, %rsp
movl $0, -4(%rbp)
movl %edi, -8(%rbp)
movq %rsi, -16(%rbp)
movl $1, %edi
movl $2, %esi
callq _test //这里调用了test函数
movl %eax, -20(%rbp)
movl -20(%rbp), %esi
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "%d"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
main1.s这里是直接取值6
- 生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.bc -o main3.s
- 生成目标文件(汇编器) 后端的工作
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器语言,最后输出目标文件(object file)
clang -fmodules -c main.s -o main.o
通过nm命令,可以查看main.o中的符号
xcrun nm -nm main.o
_printf 是一个undefined external的
undefined:表示在当前文件暂时找不到符号_printf
external:表示这个符号是外部可以访问的
- 生成可执行文件(链接)
链接器把编译生成的.o文件和(.dylib .a)文件链接之后,生成一个mach_o文件
- 终端输入
clang main.o -o main - 终端输入
xcrun nm -nm main - dyld_stub_binder 这是dyld的外部函数。链接后绑定
- 执行main 终端输入
./main
输出 6