编译原理:LLVM初步介绍

6,141

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/

参考资料

一些原始链接