LLVM介绍
The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
模块化,可重用的编译器及工具链技术的集合。 2012年ACM的软件系统奖项。Java, Apache, WWW, UNIX等。
LLVM的作者:Chris Lattner
他于2000年毕业于俄勒冈州波特兰大学计算机科学专业,同年前往UIUC(伊利诺伊大学厄巴纳香槟分校),攻读计算机科学硕士和博士学位。在UIUC期间,他的GPA是4.0(满分),并不断地研究探索关于编译器的未知领域,发表了多篇论文。在硕士毕业论文中,他提出了一套完整的在编译时、链接时、运行时甚至是在闲置时优化程序的编译思想,奠定了LLVM的基础。LLVM在Chris Lattner念博士时更加的成熟。首先,LLVM使用GCC作为前端来对用户程序进行语义分析产生 IF(Intermidiate Format),然后,LLVM使用分析结果完成代码优化和生成。这项研究让Chris Lattner在2005年毕业的时候,成为了小有名气的编译器专家。他也因此早早地被Apple相中,成为其编译器项目的骨干。进入Apple之后,Chris Lattner首先在OpenGL小组做代码优化,把LLVM运行时的编译架在OpenGL栈上,这样OpenGL栈能够产出更高效率的图形代码。这个强大的OpenGL实现被用在了后来发布的Mac OS X 10.5上。同时,LLVM 的链接优化被直接加入到Apple的代码链接器上。Chris Lattner于2010年7月开始着手Swift编程语言的设计工作,他实现了该语言的大部分基础架构。2011年末,又有几位大神加入了该项目。2013年7月,Swift项目成为了苹果开发者工具部门的主要焦点。在2014年6月2日,Swift语言正式发布。www.kanxue.com/book-37-410…
编译器架构
Source Code --- 前端 --- 优化器 --- 后端 --- Machine Code
LLVM是编译器工具链技术的一个集合,包括:
- 编译器前端:词法分析,语法分析,语义分析,生成中间代码。
- 优化器:中间代码(Intermediate Representation)优化。
- 编译器后端:生成对应架构平台的机器码。如x86,ARM等。
一些工具:
- 编译器llc:对每个文件进行编译,生成Mach-O可执行文件。
- 链接器lld:将每个Mach-O文件合并成一个,将符号绑定到地址上。lld即LLVM的内置链接器。
LLVM特点(VS GCC)
-
模块化
-
统一的中间代码IR,而前端、后端可以不一样。而GCC的前端、后端耦合在了一起,所以支持一门新语言或者新的平台,非常困难。
-
功能强大的Pass系统,根据依赖性自动对Pass(包括分析、转换和代码生成Pass)进行排序,管道化以提高效率。
-
License
编译器 | License
-|-
GCC | GPL
LLVM | [LLVM LICENSE](http://releases.llvm.org/7.0.0/LICENSE.TXT)
LLVM的主要子项目
项目名称 | 描述 |
---|---|
LLVM Core | 包含一个源代码和目标架构无关的独立配置器,一个针对很多主流(甚至于一些非主流)的CPU的汇编代码生成支持。这些核心库围绕IR来构建。 |
Clang | 一个C/C++/Objective-C编译器,提供高效快速的编译效率,风格良好、极其有用的错误和警告信息。 |
LLDB | 基于LLVM提供的库和Clang构建的优秀的本地调试器。原生支持调试多线程程序。 |
LLD | clang/llvm内置的链接器 |
dragonegg | gcc插件,可将GCC的优化和代码生成器替换为LLVM的相应工具。 |
libc++, libc++ ABI | 符合标准的,高性能的C++标准库实现,以及对C++11的完整支持。 |
compiler-rt | 为动态测试工具(如AddressSanitizer,ThreadSanitizer,MemorySanitizer和DataFlowSanitizer)提供了运行时库的实现。为像“__fixunsdfdi”这样的低级代码生成器支持进程提供高层面的调整实现,也提供当目标没有用于实现核心IR操作的短序列本机指令时生成的其他调用。 |
OpenMP | 提供一个OpenMP运行时,用于Clang中的OpenMP实现。 |
vmkit | 基于LLVM的Java和.NET虚拟机实现。 |
polly | 支持高级别的循环和数据本地化优化支持的LLVM框架,使用多面体模型实现一组缓存局部优化以及自动并行和矢量化。 |
libclc | OpenCL(开放运算语言)标准库的实现. |
klee | 基于LLVM编译基础设施的符号化虚拟机。它使用一个定理证明器来尝试评估程序中的所有动态路径,以发现错误并证明函数的属性。 klee的一个主要特性是它可以在检测到错误时生成测试用例。 |
SAFECode | 用于C / C ++程序的内存安全编译器。 它通过运行时检查来检测代码,以便在运行时检测内存安全错误(例如,缓冲区溢出)。 它可用于保护软件免受安全攻击,也可用作Valgrind等内存安全错误调试工具。 |
其他领域的编译器
- GCC
- Android NDK(Native Development Kit)。如Dart虚拟机,会在设备中生成JIT编译优化的本地代码。iOS上,Dart代码则由LLVM编译来成为本地可执行文件(AOT)。
iOS相关
clang
clang只是LLVM的一种编译器前端而已,支持C/C++/OC。它的出现也是因为GCC的各种原因。
C/C++/OC --- clang --- 优化器 --- 后端 --- Machine Code
Swift --- Swiftc --- 优化器 --- 后端 --- Machine Code
clang的优点(相对于GCC)
- 编译速度快
- 生成的AST占用内存小
- 基于库的模块化设计,结构清晰,易于IDE集成和扩展
- 诊断信息可读性强,利于调试。在编译过程中,clang创建并保留了大量详细的元数据(metadata),有利于调试和错误报告。
clang static analyzer
静态分析工具,通过自动分析程序的逻辑,在编译时就找出程序可能的bug。
Swift
前端:swift/LLVM swift前端会多出SIL optimizer,把.swift生成的中间代码.sil属于High-Level IR。 方法调用不再是像OC那样的消息发送,这样编译就可以获得更多信息用于后面的后端优化。
编译步骤
OC源文件
main.m
#import <Foundation/Foundation.h>
#define MY_NUMBER 3
int main(int argc, char *argv[]) {
printf("这是一个main方法");
int a = 1;
int b = 2;
int c = a + b + MY_NUMBER;
// NSLog(@"%d + %d = %d", a, b, c);
// NSLog(@"这是一个main方法");
return 0;
}
先看生成结果
直接生成可执行文件
clang main.m -o main
./main
结果
这是一个main方法
查看文件:
file main
main: Mach-O 64-bit executable x86_64
可以指定为.o后缀
clang main.m -o main.o
./main.o
默认即生成本机可执行文件a.out。 大多数UNIX C编译器都默认生成a.out可执行文件。
clang main.m
./a.out
a.out与main是一样的。
而使用GCC:
gcc main.m
./a.out
gcc main.m -o main.native
./main.native
先,得到LLVM IR文件
clang命令的参数:
-emit-llvm
-S或-c
-O3 优化级别
生成两种文件,都可以使用LLVM的工具lli来执行,效果与执行最终编译出来的二进制文件一样。 lli是LLVM解释器,可以直接执行LLVM IR,而不用将代码最终编译成二进制。对于支持它的架构,默认情况下,lli将作为JIT编译器(如果编译了功能),将比解释器更快地执行代码。 lli - directly execute programs from LLVM bitcode
ll后缀
-S参数
clang -emit-llvm -S main.m -o main.ll
/xxx/llvm_build/bin/lli main.ll
可读文本
bc后缀
-c参数
clang -emit-llvm -c main.m -o main.bc
/xxx/llvm_build/bin/lli main.bc
不可读的二进制格式。
相互转换
llvm-dis工具可以反汇编bitcode文件,生成ll文件。 llvm-dis - LLVM disassembler
/xxx/llvm_build/bin/llvm-dis < main.bc > main.ll
llvm-as工具可以汇编ll文件,生成bc文件。 llvm-as - LLVM assembler
/xxx/llvm_build/bin/llvm-as -f main.ll
可参考的命令:
再,生成当前CPU架构的汇编指令
llc是LLVM后端编译器,将LLVM IR转换为当前CPU的机器码,即汇编代码。
/xxx/llvm_build/bin/llc main.ll -o main.s
这里使用bc文件也可以。
结果:
.section __TEXT,__text,regular,pure_instructions
.macosx_version_min 10, 14 sdk_version 10, 14
.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, -20(%rbp)
movl %edi, -16(%rbp)
movq %rsi, -32(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
movl $1, -8(%rbp)
movl $2, -4(%rbp)
movl -8(%rbp), %eax
addl -4(%rbp), %eax
addl $3, %eax
movl %eax, -12(%rbp)
xorl %eax, %eax
addq $32, %rsp
popq %rbp
retq
.cfi_endproc
## -- End function
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "\350\277\231\346\230\257\344\270\200\344\270\252main\346\226\271\346\263\225"
.section __DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
.long 0
.long 64
.subsections_via_symbols
最终,由汇编指令得到可执行文件
有了main.s汇编指令,则即可得到可执行文件了。
gcc main.s -o main.native
与之前
clang main.m -o main
gcc main.m -o main.native
的结果一样。
交叉编译
macOS平台(x86)上,编译iOS上运行的二进制文件(ARM架构)。 在编译clang插件中:
cmake -G Xcode -DCMAKE_BUILD_TYPE=Release -DCMAKE_OSX_ARCHITECTURES:STRING=x86_64 -DLLVM_TARGETS_TO_BUILD=host -DLLVM_INCLUDE_TESTS=OFF -DCLANG_INCLUDE_TESTS=OFF -DLLVM_INCLUDE_UTILS=OFF -DLLVM_INCLUDE_DOCS=OFF -DLLVM_INCLUDE_EXAMPLES=OFF -DLLVM_BUILD_EXTERNAL_COMPILER_RT=ON -DLIBCXX_INCLUDE_TESTS=OFF -DCOMPILER_RT_INCLUDE_TESTS=OFF -DCOMPILER_RT_ENABLE_IOS=OFF ../llvm
生成llvm_xcode/Debug/lib/MyPlugin.dylib文件。
这里,-DCMAKE_OSX_ARCHITECTURES指定架构,例如:
cmake -G "Ninja" -DCMAKE_OSX_ARCHITECTURES="armv7;armv7s;arm64"
-DCMAKE_TOOLCHAIN_FILE=<PATH_TO_LLVM>/cmake/platforms/iOS.cmake
-DCMAKE_BUILD_TYPE=Release -DLLVM_BUILD_RUNTIME=Off -DLLVM_INCLUDE_TESTS=Off
-DLLVM_INCLUDE_EXAMPLES=Off -DLLVM_ENABLE_BACKTRACES=Off [options]
<PATH_TO_LLVM>
cmake使用 -DCMAKE_TOOLCHAIN_FILE 定义编译期间的flag。
使用MachOView来查看Mach-O文件
iOS, macOS平台上可执行文件的格式。有以下几种类型:
- 二进制文件
- 静态链接库 .a文件
- 动态链接库 .dylib文件
- Bundle,不能被链接的dylib,只能在运行时使用dlopen()来加载
- 可重定向文件类型
通过MachOView可以看出: a.out, main.native, main都为Mach-O文件,第一种。
参考:
再看看编译阶段
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 // 生成汇编程序 .S文件
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
查看预处理(preprocessor)过程
clang -E main.m
结果:
......
......
......
__attribute__((visibility("default"))) __attribute__((availability(macosx,introduced=10_8)))
@interface NSUserAutomatorTask : NSUserScriptTask {
@private
NSDictionary *_variables;
}
@property (nullable, copy) NSDictionary<NSString *, id> *variables;
typedef void (^NSUserAutomatorTaskCompletionHandler)(id _Nullable result, NSError * _Nullable error);
- (void)executeWithInput:(nullable id <NSSecureCoding>)input completionHandler:(nullable NSUserAutomatorTaskCompletionHandler)handler;
@end
#pragma clang assume_nonnull end
# 181 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 1 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/FoundationLegacySwiftCompatibility.h" 1 3
# 185 "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk/System/Library/Frameworks/Foundation.framework/Headers/Foundation.h" 2 3
# 2 "main.m" 2
int main(int argc, char *argv[]) {
int a = 1;
int b = 2;
int c = a + b + 3;
return 0;
}
将#include, @import,#import引入,引入头文件内容,宏定义的代码替换,条件编译(#ifdef),删除注释等。
词法分析,生成Token
将代码分解,生成一个个 Token。Token是代码的最小单元。
clang -fmodules -E -Xclang -dump-tokens main.m
Token类型包括:关键字,标识符,字面量,特殊符号。
结果:
annot_module_include '#import <F' Loc=<main.m:1:1>
int 'int' [StartOfLine] Loc=<main.m:5:1>
identifier 'main' [LeadingSpace] Loc=<main.m:5:5>
l_paren '(' Loc=<main.m:5:9>
int 'int' Loc=<main.m:5:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:5:14>
comma ',' Loc=<main.m:5:18>
char 'char' [LeadingSpace] Loc=<main.m:5:20>
star '*' [LeadingSpace] Loc=<main.m:5:25>
identifier 'argv' Loc=<main.m:5:26>
l_square '[' Loc=<main.m:5:30>
r_square ']' Loc=<main.m:5:31>
r_paren ')' Loc=<main.m:5:32>
l_brace '{' [LeadingSpace] Loc=<main.m:5:34>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:6:5>
l_paren '(' Loc=<main.m:6:11>
string_literal '"这是一个main方法"' Loc=<main.m:6:12>
r_paren ')' Loc=<main.m:6:36>
semi ';' Loc=<main.m:6:37>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
identifier 'a' [LeadingSpace] Loc=<main.m:7:9>
equal '=' [LeadingSpace] Loc=<main.m:7:11>
numeric_constant '1' [LeadingSpace] Loc=<main.m:7:13>
semi ';' Loc=<main.m:7:14>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:8:5>
identifier 'b' [LeadingSpace] Loc=<main.m:8:9>
equal '=' [LeadingSpace] Loc=<main.m:8:11>
numeric_constant '2' [LeadingSpace] Loc=<main.m:8:13>
semi ';' Loc=<main.m:8:14>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:9:5>
identifier 'c' [LeadingSpace] Loc=<main.m:9:9>
equal '=' [LeadingSpace] Loc=<main.m:9:11>
identifier 'a' [LeadingSpace] Loc=<main.m:9:13>
plus '+' [LeadingSpace] Loc=<main.m:9:15>
identifier 'b' [LeadingSpace] Loc=<main.m:9:17>
plus '+' [LeadingSpace] Loc=<main.m:9:19>
numeric_constant '3' [LeadingSpace] Loc=<main.m:9:21 <Spelling=main.m:3:19>>
semi ';' Loc=<main.m:9:30>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:12:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:12:12>
semi ';' Loc=<main.m:12:13>
r_brace '}' [StartOfLine] Loc=<main.m:13:1>
eof '' Loc=<main.m:13:2>
语法分析,生成语法树AST
验证语法正确性,将所有Token组成AST抽象语法树。
clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
根据上边的Token,生成的AST格式如下:
TranslationUnitDecl 0x7fe4d90052e8 <<invalid sloc>> <invalid sloc>
|-TypedefDecl 0x7fe4d9005b80 <<invalid sloc>> <invalid sloc> implicit __int128_t '__int128'
| `-BuiltinType 0x7fe4d9005880 '__int128'
|-TypedefDecl 0x7fe4d9005be8 <<invalid sloc>> <invalid sloc> implicit __uint128_t 'unsigned __int128'
| `-BuiltinType 0x7fe4d90058a0 'unsigned __int128'
|-TypedefDecl 0x7fe4d9005c80 <<invalid sloc>> <invalid sloc> implicit SEL 'SEL *'
| `-PointerType 0x7fe4d9005c40 'SEL *' imported
| `-BuiltinType 0x7fe4d9005ae0 'SEL'
|-TypedefDecl 0x7fe4d9005d58 <<invalid sloc>> <invalid sloc> implicit id 'id'
| `-ObjCObjectPointerType 0x7fe4d9005d00 'id' imported
| `-ObjCObjectType 0x7fe4d9005cd0 'id' imported
|-TypedefDecl 0x7fe4d9005e38 <<invalid sloc>> <invalid sloc> implicit Class 'Class'
| `-ObjCObjectPointerType 0x7fe4d9005de0 'Class' imported
| `-ObjCObjectType 0x7fe4d9005db0 'Class' imported
|-ObjCInterfaceDecl 0x7fe4d9005e88 <<invalid sloc>> <invalid sloc> implicit Protocol
|-TypedefDecl 0x7fe4d98555e8 <<invalid sloc>> <invalid sloc> implicit __NSConstantString 'struct __NSConstantString_tag'
| `-RecordType 0x7fe4d9855400 'struct __NSConstantString_tag'
| `-Record 0x7fe4d9005f50 '__NSConstantString_tag'
|-TypedefDecl 0x7fe4d9855680 <<invalid sloc>> <invalid sloc> implicit __builtin_ms_va_list 'char *'
| `-PointerType 0x7fe4d9855640 'char *'
| `-BuiltinType 0x7fe4d9005380 'char'
|-TypedefDecl 0x7fe4d9855948 <<invalid sloc>> <invalid sloc> implicit __builtin_va_list 'struct __va_list_tag [1]'
| `-ConstantArrayType 0x7fe4d98558f0 'struct __va_list_tag [1]' 1
| `-RecordType 0x7fe4d9855770 'struct __va_list_tag'
| `-Record 0x7fe4d98556d0 '__va_list_tag'
|-ImportDecl 0x7fe4d99008f8 <main.m:1:1> col:1 implicit Foundation
这里才是main函数
|-FunctionDecl 0x7fe4d9900ba8 <line:5:1, line:13:1> line:5:5 main 'int (int, char **)'
| |-ParmVarDecl 0x7fe4d9900948 <col:10, col:14> col:14 argc 'int'
| |-ParmVarDecl 0x7fe4d9900a60 <col:20, col:31> col:26 argv 'char **':'char **'
| `-CompoundStmt 0x7fe4d9901518 <col:34, line:13:1>
| |-CallExpr 0x7fe4d9901170 <line:6:5, col:36> 'int'
| | |-ImplicitCastExpr 0x7fe4d9901158 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | | `-DeclRefExpr 0x7fe4d9901088 <col:5> 'int (const char *, ...)' Function 0x7fe4d9900cb8 'printf' 'int (const char *, ...)'
| | `-ImplicitCastExpr 0x7fe4d99011b8 <col:12> 'const char *' <BitCast>
| | `-ImplicitCastExpr 0x7fe4d99011a0 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7fe4d99010e8 <col:12> 'char [23]' lvalue "\350\277\231\346\230\257\344\270\200\344\270\252main\346\226\271\346\263\225"
| |-DeclStmt 0x7fe4d9901268 <line:7:5, col:14>
| | `-VarDecl 0x7fe4d99011e8 <col:5, col:13> col:9 used a 'int' cinit
| | `-IntegerLiteral 0x7fe4d9901248 <col:13> 'int' 1
| |-DeclStmt 0x7fe4d9901318 <line:8:5, col:14>
| | `-VarDecl 0x7fe4d9901298 <col:5, col:13> col:9 used b 'int' cinit
| | `-IntegerLiteral 0x7fe4d99012f8 <col:13> 'int' 2
| |-DeclStmt 0x7fe4d99014c8 <line:9:5, col:30>
| | `-VarDecl 0x7fe4d9901348 <col:5, line:3:19> line:9:9 c 'int' cinit
| | `-BinaryOperator 0x7fe4d99014a0 <col:13, line:3:19> 'int' '+'
| | |-BinaryOperator 0x7fe4d9901458 <line:9:13, col:17> 'int' '+'
| | | |-ImplicitCastExpr 0x7fe4d9901428 <col:13> 'int' <LValueToRValue>
| | | | `-DeclRefExpr 0x7fe4d99013a8 <col:13> 'int' lvalue Var 0x7fe4d99011e8 'a' 'int'
| | | `-ImplicitCastExpr 0x7fe4d9901440 <col:17> 'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7fe4d99013e8 <col:17> 'int' lvalue Var 0x7fe4d9901298 'b' 'int'
| | `-IntegerLiteral 0x7fe4d9901480 <line:3:19> 'int' 3
| `-ReturnStmt 0x7fe4d9901500 <line:12:5, col:12>
| `-IntegerLiteral 0x7fe4d99014e0 <col:12> 'int' 0
`-<undeserialized declarations>
TranslationUnitDecl 根节点,表示一个编译单元。 节点主要有三种:Type类型,Decl声明,Stmt陈述。
ObjCInterfaceDecl OC中Interface声明 FunctionDecl 函数声明 ParmVarDecl 参数声明 CompoundStmt 具体语句 DeclStmt 语句声明 VarDecl 变量声明 IntegerLiteral 整数字面量 BinaryOperator 操作符 ImplicitCastExpr 隐士转换 DeclRefExpr 引用类型声明 ReturnStmt 返回语句
使用clang的API可针对AST进行相应的分析及处理。
语义分析
在语法分析的基础上加一些检查等。 根据语法树,可以分析找出潜在的代码错误:如类型不匹配,OC向target发送一个未实现的消息等。
生成中间代码IR
CodeGen自顶向下,遍历AST,逐步翻译生成LLVM IR代码。 main.ll和main.bc即为IR的两种形式。
中间代码IR
中间代码有两种表示样式:
文本,ll后缀
便于阅读的文本格式,类似于汇编格式,后缀为.ll
; ModuleID = 'main.bc'
source_filename = "main.m"
target datalayout = "e-m:o-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-apple-macosx10.14.0"
@.str = private unnamed_addr constant [23 x i8] c"\E8\BF\99\E6\98\AF\E4\B8\80\E4\B8\AAmain\E6\96\B9\E6\B3\95\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
%7 = alloca i32, align 4
%8 = 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
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i32 0, i32 0))
store i32 1, i32* %6, align 4
store i32 2, i32* %7, align 4
%10 = load i32, i32* %6, align 4
%11 = load i32, i32* %7, align 4
%12 = add nsw i32 %10, %11
%13 = add nsw i32 %12, 3
store i32 %13, i32* %8, align 4
ret i32 0
}
declare i32 @printf(i8*, ...) #1
attributes #0 = { noinline optnone ssp uwtable "correctly-rounded-divide-sqrt-fp-math"="false" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-jump-tables"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+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" "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "no-signed-zeros-fp-math"="false" "no-trapping-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="penryn" "target-features"="+cx16,+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", [2 x i32] [i32 10, i32 14]}
!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 LLVM version 10.0.1 (clang-1001.0.46.4)"}
OC代码在这里进行runtime的桥接:property合成,ARC处理等。
二进制格式
不可读,后缀为.bc,使用llvm-dis工具可将其转化为.ll文件。
IR的语法
IR是基于寄存器的指令集,只能通过load和store指令来进行CPU和内存间的数据交换。
基本语法
关键字:
- ; 注释
- define 函数定义
- i32 所占bit位
- @ 全局变量
- % 局部变量
- ret 函数返回
- alloca 在当前执行的函数的栈空间分配内存,当函数返回时,自动释放内存
- align 内存对齐
- load 读取数据
- store 写入数据
- icmp 整数值比较,返回布尔值结果
- br 选择分支,根据cond来转向label
- label 代码标签
%0,%1分别为函数参数。
参考:
main.m
#import <Foundation/Foundation.h>
#define MY_NUMBER 3
int main(int argc, char *argv[]) {
printf("这是一个main方法");
int a = 1;
int b = 2;
int c = a + b + MY_NUMBER;
// NSLog(@"%d + %d = %d", a, b, c);
// NSLog(@"这是一个main方法");
return 0;
}
转换的部分IR为:
; 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
%7 = alloca i32, align 4
%8 = 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
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([23 x i8], [23 x i8]* @.str, i32 0, i32 0))
store i32 1, i32* %6, align 4
store i32 2, i32* %7, align 4
%10 = load i32, i32* %6, align 4
%11 = load i32, i32* %7, align 4
%12 = add nsw i32 %10, %11
%13 = add nsw i32 %12, 3
store i32 %13, i32* %8, align 4
ret i32 0
}
test_add.m
void test_add(int a, int b) {
int c = a + b - 3;
}
转换的部分IR为:
; Function Attrs: noinline nounwind optnone ssp uwtable
define void @test_add(i32, i32) #0 {
%3 = alloca i32, align 4 ; int A
%4 = alloca i32, align 4 ; int B
%5 = alloca i32, align 4 ; int c
store i32 %0, i32* %3, align 4 ; A = a
store i32 %1, i32* %4, align 4 ; B = b
%6 = load i32, i32* %3, align 4 ; a
%7 = load i32, i32* %4, align 4 ; b
%8 = add nsw i32 %6, %7 ; a + b
%9 = sub nsw i32 %8, 3 ; a + b - 3
store i32 %9, i32* %5, align 4 ; 将最终结果存到%5(即c)中
ret void ; 返回void
}
test_loop.m
#include <stdio.h>
void test_loop() {
int i = 0;
while (i < 10) {
i++;
printf("%d", i);
}
}
转换的部分IR为:
; Function Attrs: noinline optnone ssp uwtable
define void @test_loop() #0 {
%1 = alloca i32, align 4
store i32 0, i32* %1, align 4
br label %2
; <label>:2: ; preds = %5, %0
%3 = load i32, i32* %1, align 4
%4 = icmp slt i32 %3, 10
br i1 %4, label %5, label %10
; <label>:5: ; preds = %2
%6 = load i32, i32* %1, align 4
%7 = add nsw i32 %6, 1
store i32 %7, i32* %1, align 4
%8 = load i32, i32* %1, align 4
%9 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @.str, i32 0, i32 0), i32 %8)
br label %2
; <label>:10: ; preds = %2
ret void
}
目前项目已经接触过的
attribute
编译器指令,运行开发者指定更多的编译检查和优化。
//弃用API,用作API更新
#define __deprecated __attribute__((deprecated))
//带描述信息的弃用
#define __deprecated_msg(_msg) __attribute__((deprecated(_msg)))
//遇到__unavailable的变量/方法,编译器直接抛出Error
#define __unavailable __attribute__((unavailable))
//告诉编译器,即使这个变量/方法 没被使用,也不要抛出警告
#define __unused __attribute__((unused))
//和__unused相反
#define __used __attribute__((used))
//如果不使用方法的返回值,进行警告
#define __result_use_check __attribute__((__warn_unused_result__))
//OC方法在Swift中不可用
#define __swift_unavailable(_msg) __attribute__((__availability__(swift, unavailable, message=_msg)))
这是GNU风格的。
如:
// This installs a small guard that checks for the most common threading-errors in UIKit.
// This won't really slow down performance but still only is compiled in DEBUG versions of PSPDFKit.
// @note No private API is used here.
__attribute__((constructor)) static void PSPDFUIKitMainThreadGuard(void) {
@autoreleasepool {
for (NSString *selStr in @[PROPERTY(setNeedsLayout), PROPERTY(setNeedsDisplay), PROPERTY(setNeedsDisplayInRect:)]) {
SEL selector = NSSelectorFromString(selStr);
SEL newSelector = NSSelectorFromString([NSString stringWithFormat:@"pspdf_%@", selStr]);
if ([selStr hasSuffix:@":"]) {
PSPDFReplaceMethodWithBlock(UIView.class, selector, newSelector, ^(__unsafe_unretained UIView *_self, CGRect r) {
PSPDFAssertIfNotMainThread();
((void ( *)(id, SEL, CGRect))objc_msgSend)(_self, newSelector, r);
});
} else {
PSPDFReplaceMethodWithBlock(UIView.class, selector, newSelector, ^(__unsafe_unretained UIView *_self) {
PSPDFAssertIfNotMainThread();
((void ( *)(id, SEL))objc_msgSend)(_self, newSelector);
});
}
}
}
}
自定义attribute
Clang警告处理
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
///代码
#pragma clang diagnostic pop
- 对当前编译环境进行压栈,
- 忽略未声明的selector警告 -Wundeclared-selector
- 编译代码
- 对编译环境进行出栈
预处理
如 DEBUG,可以自定义,在 Build Settings 的Preprocessor Macros中添加
DEBUG=1 COCOAPODS=1 TEST_SERVER=1
使用:
#ifdef TEST_SERVER
// 测试服务器
#else
// 生产服务器
#endif
插入脚本
比较常见的是Run Script, 以及 CocoaPods中的sh脚本。
Reveal的原理
在UIApplicationMain添加Symbolic断点:
expr (Class)NSClassFromString(@"IBARevealLoader") == nil ? (void *)dlopen("/Applications/Reveal.app/Contents/SharedSupport/iOS-Libraries/libReveal.dylib", 0x2) : ((void*)0)
注意,这里使用到了dlopen,加载动态库。
另外,还有reveal的cocoapods添加方式。 比较二者的区别!!!
动态修改target的build版本号
勾选Run Script Only When installing
buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${PROJECT_DIR}/${INFOPLIST_FILE}")
buildNumber=$(($buildNumber + 1))
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "${PROJECT_DIR}/${INFOPLIST_FILE}"
脚本编译打包
持续集成CI
//编译成.app
xcodebuild -workspace $projectName.xcworkspace -scheme $projectName -configuration $buildConfig clean build SYMROOT=$buildAppToDir
//打包
xcrun -sdk iphoneos PackageApplication -v $appDir/$projectName.app -o $appDir/$ipaName.ipa
通过info命令,可以查看到详细的文档
info xcodebuild
如 Fastlane, auto_ipa.sh。
提高编译速度
Compile mm files
MBCARKernelFilter.mm 93.6s
FlareblurRender.mm 96.3s
FacialRemodelingTool.mm 96.8s
EyesEnlargeTool.mm 97.1s
DrawHairing.mm 97.3s
BeautyTeethViewController.mm 99.7s
......
Build target AFNetworking
Compile UIWebView+AFNetworking.m 13.1s
Compile AFURLRequestSerialization.m 19.2s
Compile AFNetworkReachabilityManager.m 18.6s
Compile AFNetworkActivityIndicatorManager.m 18.2s
Compile AFImageDownloader.m 18.0s
Compile AFHTTPSessionManager.m 17.9s
Compile AFAutoPurgingImageCahce.m 17.8s
Create static library xxx/Build/Products/Debug-iphoneos/AFNetworking/libAFNetworking.a
制作clang插件
命名规范,代码检查等。
LLVM其他实践
一些工具
libclang libTooling 应用:语法树AST分析,AST转换等。 如OC-Swift,JS->OC, OC-JS等。
LLVM优化
-
编译时优化:编译前端将源码编译成LLVM IR
-
链接时优化:跨文件的分析优化
-
运行时优化:收集运行时的信息 profiling information
-
闲时优化:获得运行时的profiling information,生成更高效的代码。
Pass开发
LLVM的优化即对中间代码IR优化,由多个Pass来完成,每个Pass完成特定的优化工作。可以分组。 clang命令的参数如-O2,-O3, -O4等。
Source Code --- 前端 --- IR - Pass - IR - Pass - IR --- 后端 --- Machine Code
Pass即为一层一层相互独立的IR优化器。可以做到代码优化,代码混淆等。
如开启bitcode,则苹果会做一个对应的Pass优化。
源码路径为llvm/lib/Transforms/,涉及FunctionPass等,类似clang插件。编译生成后也是dylib文件。
已有Pass的分类
使用Pass
Pass是作用于IR之上的,对IR文件执行Pass会得到另一个优化后的IR文件。
/xxx/llvm_build/bin/opt -load MyPass.dylib -MyPass < main.ll > another_main.ll
/xxx/llvm_build/bin/opt -load MyPass.dylib -MyPass < main.ll > another_main.bc
使用lli来执行ll或bc查看结果
/xxx/llvm_build/bin/lli another_main.ll
/xxx/llvm_build/bin/lli another_main.bc
AOT和JIT
- AOT:ahead-of-time 提前编译。执行前全部翻译成机器码.S文件,然后通过汇编程序来编译成指定架构的二进制文件。AOT,如C。
- JIT:just-in-time 即时编译。一句一句边翻译边运行,代码可在运行时直接生成。JIT,如js,python等。如js可动态下发并执行代码。一般带有自省函数eval。
同时支持AOT和JIT
- Python:可以直接翻译执行,也可以提前翻译成中间代码。
- Dart:JIT快速开发周期,避免每次代码改动都重新编译;AOT发布运行高效的机器码。
热更新方案: JSPatch,滴滴热更新,腾讯热更新
开发新的编程语言
https://llvm-tutorial-cn.readthedocs.io/en/latest/index.html
https://kaleidoscope-llvm-tutorial-zh-cn.readthedocs.io/zh_CN/latest/