什么是LLVM
LLVM是架构编译器(compiler
)的框架系统,以C++编写而成,用于优化任何编程语言编写的程序的编译时间(compile-time
),链接时间(link-time
),运行时间 (run-time
)以及空闲时间(idle-time
),对开发者保持开放,并兼容已有脚本。
目前,LLVM已被苹果的开发工具(Xcode),Xilinx Vivado,Facebook,Google等公司采用。
传统编译器的设计
-
Frontend(编译器前端)
编译器前端主要任务是解析源代码。它会进行词法分析,语法分析,语义分析,检测源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree, AST),LLVM前端还会生成中间代码(intermediate representation ,IR)。 -
Optimizer(优化器)
负责进行各种优化。改善代码运行时间,如消除代码冗余计算等。 -
Backend(编译器后端)/CodeGenerator(代码生成器)
将代码映射到目标指令集。生成机器语言,并进行机器相关的代码优化。
iOS的编译器架构
ObjectiveC/C/C++使用的编译器前端是Clang,Swift的编译前端是Swift,而它们的后端都是LLVM。
LLVM的设计
当编译器要支持多种源语言或多种硬件架构时,LLVM的优势就显现出来了。它有别于如GCC这样的编译器,由于GCC是作为整体应用程序而设计的,因此他的用途就受到了局限性和灵活性;而LLVM设计最重要的部分是,使用通用的代码表现形式(IR),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编程语言独立编写前端,同样可以为任何硬件架构独立编写后端。
Clang
Clang是LLVM项目中的一个子项目。它是基于LLVM的一个轻量级编译器,诞生之初是为了替代GCC, 提供更快的编译速度。它是负责编译C,C++,Obj-C的编译器,它属于真个LLVM架构中的,编译器前端。
通过Clang 感受编译流程
首先通过Xcode创建一个简单的工程, 这里只包含一个main.m
文件
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!");
}
return 0;
}
- 通过命令行打印
main.m
的编译阶段:
clang -ccc-print-phases main.m
执行结果:
0: input, "main.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
注释
0: 输入文件:找到源文件
1: 预处理:替换宏,头文件导入等
2: 编译:进行词法分析,语法分析,检测语法错误,最终生成ir文件
3: 后端:通过一个个的Pass优化,最终生成汇编代码
4: 汇编:生成目标文件
5: 链接:链接需要的动态库和静态库,生成可执行文件
6: 通过不同的架构,生成对应的可执行文件
-
执行预处理命令
这个步骤执行的东西比较多,这里#import <Foundation/Foundation.h>
注释掉,然后修改下代码:
#import <stdio.h>
#define A 10
typedef int ZZ_INT;
int main(int argc, const char * argv[]) {
ZZ_INT B = 15;
printf("result = %d",A + B);
return 0;
}
clang -E main.m
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 375 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2
# 11 "main.m"
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 1 3 4
# 64 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/stdio.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 1 3 4
# 68 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/_stdio.h" 3 4
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 1 3 4
# 630 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include/sys/cdefs.h" 3 4
//省略.......
typedef union {
char __mbstate8[128];
long long _mbstateL;
} __mbstate_t;
typedef __mbstate_t __darwin_mbstate_t;
typedef long int __darwin_ptrdiff_t;
//省略......
typedef int ZZ_INT;
int main(int argc, const char * argv[]) {
ZZ_INT B = 15;
printf("result = %d",10 + B);
return 0;
}
省略的部分大致为#import <stdio.h>
头文件导入的内容,main
函数中printf
中,原来#define A 10
也被直接替换成了10
。这里需要注意的是typedef int ZZ_INT;
并没有替换。
-
编译阶段
-词法分析
预处理之后就是词法分析。这里会把代码切成一个个的Token
clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
执行结果:
annot_module_include '#import <stdio.h>
#define A 10
typedef int ZZ_INT;
int main(int argc, const char * argv[]) {
ZZ_INT B = 15;
printf("result = %d",A + B);
return 0;
}
?' Loc=<main.m:11:1>
typedef 'typedef' [StartOfLine] Loc=<main.m:14:1>
int 'int' [LeadingSpace] Loc=<main.m:14:9>
identifier 'ZZ_INT' [LeadingSpace] Loc=<main.m:14:13>
semi ';' Loc=<main.m:14:19>
int 'int' [StartOfLine] Loc=<main.m:16:1>
identifier 'main' [LeadingSpace] Loc=<main.m:16:5>
l_paren '(' Loc=<main.m:16:9>
int 'int' Loc=<main.m:16:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:16:14>
comma ',' Loc=<main.m:16:18>
const 'const' [LeadingSpace] Loc=<main.m:16:20>
char 'char' [LeadingSpace] Loc=<main.m:16:26>
star '*' [LeadingSpace] Loc=<main.m:16:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:16:33>
l_square '[' Loc=<main.m:16:37>
r_square ']' Loc=<main.m:16:38>
r_paren ')' Loc=<main.m:16:39>
l_brace '{' [LeadingSpace] Loc=<main.m:16:41>
identifier 'ZZ_INT' [StartOfLine] [LeadingSpace] Loc=<main.m:17:5>
identifier 'B' [LeadingSpace] Loc=<main.m:17:12>
equal '=' [LeadingSpace] Loc=<main.m:17:14>
numeric_constant '15' [LeadingSpace] Loc=<main.m:17:16>
semi ';' Loc=<main.m:17:18>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:18:5>
l_paren '(' Loc=<main.m:18:11>
string_literal '"result = %d"' Loc=<main.m:18:12>
comma ',' Loc=<main.m:18:25>
numeric_constant '10' Loc=<main.m:18:26 <Spelling=main.m:13:11>>
plus '+' [LeadingSpace] Loc=<main.m:18:28>
identifier 'B' [LeadingSpace] Loc=<main.m:18:30>
r_paren ')' Loc=<main.m:18:31>
semi ';' Loc=<main.m:18:32>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:19:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:19:12>
semi ';' Loc=<main.m:19:13>
r_brace '}' [StartOfLine] Loc=<main.m:20:1>
eof '' Loc=<main.m:20:2>
词法分析将代码按照单个词 或者 标点符号 分割出来,并标记出了文件名,行号,起始位置,比如这里的
identifier 'main' [LeadingSpace] Loc=<main.m:16:5>
main
在main.m
文件下的 第16行 从第5个字符开始。
-语法分析
词法分析完之后就是语法分析,主要是验证语法是否正确。在词法分析的基础上,将单词序列组合成各类语法短语,如'程序','语句','表达式'等,然后将所有的节点组成抽象语法树(Abstract Syntax Tree,AST)。语法分析程序判断源程序在结构上是否正确。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
执行结果:
TranslationUnitDecl 0x7feb9b01cc08 <<invalid sloc>> <invalid sloc> <undeserialized declarations>
|-TypedefDecl 0x7feb9b01d4a0 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7feb9b01d1a0 '__int128'
|-TypedefDecl 0x7feb9b01d510 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7feb9b01d1c0 'unsigned __int128'
|-TypedefDecl 0x7feb9b01d5b0 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7feb9b01d570 'SEL *'
| `-BuiltinType 0x7feb9b01d400 'SEL'
|-TypedefDecl 0x7feb9b01d698 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7feb9b01d640 'id'
| `-ObjCObjectType 0x7feb9b01d610 'id'
|-TypedefDecl 0x7feb9b01d778 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7feb9b01d720 'Class'
| `-ObjCObjectType 0x7feb9b01d6f0 'Class'
|-ObjCInterfaceDecl 0x7feb9b01d7d0 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7feb9b01db48 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7feb9b01d940 'struct __NSConstantString_tag'
| `-Record 0x7feb9b01d8a0 '__NSConstantString_tag'
|-TypedefDecl 0x7feb9a022a00 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7feb9b01dba0 'char *'
| `-BuiltinType 0x7feb9b01cca0 'char'
|-TypedefDecl 0x7feb9a022ce8 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7feb9a022c90 'struct __va_list_tag [1]' 1
| `-RecordType 0x7feb9a022af0 'struct __va_list_tag'
| `-Record 0x7feb9a022a58 '__va_list_tag'
|-ImportDecl 0x7feb9a023510 <main.m:11:1> col:1 implicit Darwin.C.stdio
|-TypedefDecl 0x7feb9a023568 <line:14:1, col:13> col:13 referenced ZZ_INT 'int'
| `-BuiltinType 0x7feb9b01cd00 'int'
`-FunctionDecl 0x7feb9a023840 <line:16:1, line:20:1> line:16:5 main 'int (int, const char **)'
|-ParmVarDecl 0x7feb9a0235d8 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7feb9a0236f0 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7feb9a0c3e60 <col:41, line:20:1>
|-DeclStmt 0x7feb9a0c3c68 <line:17:5, col:18>
| `-VarDecl 0x7feb9a0c3800 <col:5, col:16> col:12 used B 'ZZ_INT':'int' cinit
| `-IntegerLiteral 0x7feb9a0c3868 <col:16> 'int' 15
|-CallExpr 0x7feb9a0c3dd0 <line:18:5, col:31> 'int'
| |-ImplicitCastExpr 0x7feb9a0c3db8 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7feb9a0c3c80 <col:5> 'int (const char *, ...)' Function 0x7feb9a0c3890 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7feb9a0c3e18 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7feb9a0c3e00 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7feb9a0c3cd8 <col:12> 'char [12]' lvalue "result = %d"
| `-BinaryOperator 0x7feb9a0c3d70 <line:13:11, line:18:30> 'int' '+'
| |-IntegerLiteral 0x7feb9a0c3d00 <line:13:11> 'int' 10
| `-ImplicitCastExpr 0x7feb9a0c3d58 <line:18:30> 'ZZ_INT':'int' <LValueToRValue>
| `-DeclRefExpr 0x7feb9a0c3d20 <col:30> 'ZZ_INT':'int' lvalue Var 0x7feb9a0c3800 'B' 'ZZ_INT':'int'
`-ReturnStmt 0x7feb9a0c3e50 <line:19:5, col:12>
`-IntegerLiteral 0x7feb9a0c3e30 <col:12> 'int' 0
CompoundStmt
,DeclStmt
,CallExpr
等都是一些节点对应的命令语句。
-生成中间代码IR(intermediate representation)
完成上面步骤之后就要开始生成中间代码(IR)了,代码生成器会将语法树自上而下的遍历逐步翻译成LLVM-IR。通过如下命令生成.ll
文本文件来查看IR
代码:
clang -S -fobjc-arc -emit-llvm main.m
打开main.ll
文件:
; ModuleID = 'main.m'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.15.0"
@.str = private unnamed_addr constant [12 x i8] c"result = %d\00", align 1
; Function Attrs: noinline optnone ssp uwtable
define i32 @main(i32, i8**) #0 {
%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
store i32 15, i32* %6, align 4
%7 = load i32, i32* %6, align 4
%8 = add nsw i32 10, %7
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([12 x i8], [12 x i8]* @.str, i64 0, i64 0), i32 %8)
ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { noinline 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, !3, !4, !5, !6, !7}
!llvm.ident = !{!8}
!0 = !{i32 2, !"SDK Version", [3 x i32] [i32 10, i32 15, i32 4]}
!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 4, !"Objective-C Garbage Collection", i32 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 11.0.3 (clang-1103.0.32.59)"}
这里的代码看起来就有点汇编的意思了,先简单介绍下这些标识符:
@
全局标识
%
局部标识
alloca
开辟空间
align
内存对齐
i32
32Bit/4bytes
store
写入内存
load
读取数据
call
调用函数
ret
返回
IR的优化:LLVM将优化级别分为-O0
,-O1
,-O2
,-O3
,-Os
,
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
bitCode
Xcode7 以后开启bitCode
后,苹果会做进一步优化。生成.bc
的中间代码。
我们通过优化过的main.ll
来生成main.bc
代码:
clang -emit-llvm -c main.ll -o main.bc
执行结果:
dec0 170b 0000 0000 1400 0000 d80c 0000
0700 0001 4243 c0de 3514 0000 0700 0000
620c 3024 9696 a6a5 f7d7 7f5d d3b7 4ffb
b7ed e7fd 4f0b 5180 4c01 0000 210c 0000
e602 0000 0b02 2100 0200 0000 1600 0000
0781 2391 41c8 0449 0610 3239 9201 840c
2505 0819 1e04 8b62 8014 4502 4292 0b42
a410 3214 3808 184b 0a32 5288 4870 c421
2344 1287 8c10 4192 0264 c808 b114 2043
4688 20c9 0132 5284 182a 282a 9031 7cb0
5c91 20c5 c800 0000 8920 0000 1000 0000
3222 4809 2064 8504 9322 a484 0493 22e3
84a1 9014 124c 8a8c 0b84 a44c 1040 7304
4832 000a 7304 6040 8048 19c6 2864 aa30
08a1 8180 1c18 34a8 8c00 cc11 8002 0000
5118 0000 5901 0000 1bf8 27f8 ffff ffff
-
生成汇编代码
通过最终的main.bc
或main.ll
,来生成汇编代码:
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
执行结果:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 4
.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 $15, -20(%rbp)
movl -20(%rbp), %eax
addl $10, %eax
leaq L_.str(%rip), %rdi
movl %eax, %esi
movb $0, %al
callq _printf
xorl %ecx, %ecx
movl %eax, -24(%rbp) ## 4-byte Spill
movl %ecx, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "result = %d"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
生成的汇编代码也可以进行优化:
clang -Os -S -fobjc-arc main.m -o main.s
优化之后的文件变小了一些,最明显的变化就是汇编指令变少了:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 10, 15 sdk_version 10, 15, 4
.globl _main ## -- Begin function main
_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
leaq L_.str(%rip), %rdi
movl $25, %esi
xorl %eax, %eax
callq _printf
xorl %eax, %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "result = %d"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
-
生成目标文件(汇编器)
目标文件的生成,是汇编以汇编代码作为输入,将汇编代码转换为机器代码,最终输出目标文件(object file)。
clang -fmodules -c main.s -o main.o
通过nm
命令,查看main.o
中的符号信息:
nm -nm main.o
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _main
这里的_printf
是一个undefined external
的。
undefined
表示在当前文件暂时找不到符号_printf
;
external
表示这个符号是外部可以访问的。
这里也就是说还没有链接动静态库。
-
生成可执行文件(链接)
链接器把编译产生的.o
文件和.dylib
/.a
文件链接,生成一个mach-o
文件。
clang main.o -o main
这部操作之后就生成了一个exec
的mach-o
的可执行文件:
再次查看
main
中的符号信息:
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100000f6f (__TEXT,__text) external _main
0000000100002008 (__DATA,__data) non-external __dyld_private
可以看到libSystem.dylib
就链接上去了。
Clang插件
待补充
总结
本文90%内容为摘抄记录,代码部分为实测。