LLVM

214 阅读12分钟

LLVM学习

解释型语言

  • vim 创建一个py文件 image.png
  • 写完一个简单的print("hello word")后保存退出 image.png
  • 打印内容结果就出来的 image.png

image.png

编译型语言

  • 终端vi hello.c 后写入一些简单的c语句打印。

image.png

  • 现在出现了这两个文件 image.png

  • 执行这个hello.c image.png

从上面的例子中我们发现解释型语言可以直接编译,编译型语言需要先编译成机器可执行的执行文件后才能执行。

  • 这个就是我们编译c文件的编译器。 image.png

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.传统的编译器设计

image.png

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。

image.png

3.LLVM的设计

  • LLVM编译器不同与传统的编译器。他的优点在于。

image.pngLLVM也分三个阶段,但是设计上略微的有些区别, 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架构中的编译器前端。

image.png

练习流程

4.LLVM编译流程

LLVM编译一个源文件的过程:预处理 -> 词法分析 -> Token -> 语法分析 -> AST -> 代码生成 -> LLVM IR -> 优化 -> 生成汇编代码 -> Link -> 目标文件 我们可以通过命令行工具打印出源码的编译主要流程

  • 创建一个简单的main.m 文件

image.png

  • 终端输入指令 clang -ccc-print-phases main.m 后打印 image.png

  • 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文件 查看这个文件。 image.png

  • 这个是另一个测试文件,测试 typedef int HK_INT_64;取别名并不是宏定义的一部分。 image.png 结论: 这个过程处理宏定义的替换、头文件的导入

编译阶段(compiler)

  • 词法分析
  • 终端输入clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m image.png

总结 :这里就是词法分析,预处理之后就会进行词法分析,这里会把代码切成一个个Token,比如大小括号,等于号和字符串等。


  • 语法分析
  • 终端输入 clang -fmodules -fsyntax-only -Xclang -ast-dump main.m image.png

image.png 这一段就是main函数的语法分析,我们可以清晰的看到 FunctionDecl 是方法的意思,main方法 ParmVarDecl 是传入的参数的意思。可以看到传入了第一个参数argc和第二个参数argv. -CompoundStmt 0x7f8ca0861028 <col:41, line:22:1> 是复合语句的意思,从当前行第41开始到 22行第1个结束。 image.png

  • DeclStmt 复合语句中的第一个局部变量a,和局部变量b。地址完全是一样的。

image.png 这个是函数的调用调用了printf函数。说明函数的指针,输入类型以及返回类型。

image.png 函数的第一个参数

image.png 这里是加法运算,a+b的结果作为第一个参数,在+30.

image.png 这里是return 返回一个0

总结:它的任务就是验证程序的语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽象语法树(AST),语法分析程序判断源程序在结构上是否正确


  • 生成中间代码IR(intermediate representation )

  • 这里修改了一下源文件,加了一个函数的调用。 image.png

  • 终端输入 clang -S -fobjc-arc -emit-llvm main.m

image.png
生成一个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

image.png

image.png

  • -O3:最快,但是包最大

  • -Os:是xcode默认的优化级别,它平衡了包体积大小与速度

  • -Oz:加载慢,但是包体积小,它内部是将多个函数汇编代码相同的指令放到一个新的函数 我们可以通过以下命令行去查看优化之后的IR

  • 可以使用优化终端输入 clang Os -S -fobjc-arc -emit-llvm main.m -o main.ll

  • 这是优化后的发现这个test函数是相当的精简同时main函数中的调用test函数直接拿到了结构6 image.png

; 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的优化是一致的 image.png

  • 这是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

image.png 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

image.png _printf 是一个undefined external的
undefined:表示在当前文件暂时找不到符号_printf
external:表示这个符号是外部可以访问的


  • 生成可执行文件(链接)

链接器把编译生成的.o文件和(.dylib .a)文件链接之后,生成一个mach_o文件

  • 终端输入 clang main.o -o main image.png
  • 终端输入 xcrun nm -nm main image.png
  • dyld_stub_binder 这是dyld的外部函数。链接后绑定

  • 执行main 终端输入
./main

输出 6