代码改变世界

iOS开发系列-LLVM、Clang

2018-05-03 09:48  iCoderHong  阅读(1571)  评论(0编辑  收藏  举报

LLVM

LLVM计划启动于2000年,最初由University of Illinois at Urbana-Champaign的Chris Lattner主持开展。
我们可以认为LLVM是一个完整的编译器架构,也可以认为它是一个用于开发编译器、解释器相关的库
在理解LLVM时,我们可以认为它包括了一个狭义的LLVM和一个广义的LLVM。广义的LLVM其实就是指整个LLVM编译器架构,包括了前端、后端、优化器、众多的库函数以及很多的模块;而狭义的LLVM其实就是聚焦于编译器后端功能(代码生成、代码优化、JIT等)的一系列模块和库。

对应到这个图中,我们就可以非常明确的找出它们的对应关系。Clang其实大致上可以对应到编译器的前端,主要处理一些和具体机器无关的针对语言的分析操作;
编译器的优化器部分和后端部分其实就是我们之前谈到的LLVM后端(狭义的LLVM);而整体的Compiler架构就是LLVM架构。

编译流程

目前iOS 开发中 Objective-C 和 Swift 都用的是 Clang / LLVM 来编译的。Clang 是 LLVM 的子项目,是 C,C++ 和 Objective-C 编译器,目的是提供惊人的快速编译,比 GCC 快3倍。
其中的 clang static analyzer 主要是进行语法分析,语义分析和生成中间代码,当然这个过程会对代码进行检查,出错的和需要警告的会标注出来。

LLVM 核心库提供一个优化器,对流行的 CPU 做代码生成支持。lld 是 Clang / LLVM 的内置链接器,clang 必须调用链接器来产生可执行文件。

编译细节

源文件从编译到生成可执行文件流程大致如下图

在列出详细的编译步骤之前先看我们编写的源文件是如何完成一次性编译的。新建一个main.m文件,代码如下

#include <stdio.h>
#define VALUE 6
int main(){
    
    int a = VALUE;
    
    printf("Hello Clang\n");
    
    return 0;
}

在命令行编译、链接

clang -c main.m -o main.o // 编译
clang main.o -o main // 链接

这样还没发看清clang的全部过程,下面开始说下在编译前段clang编译的细节。

预处理

clang -E main.m -o main.e

执行完后打开main.e

extern int __vsprintf_chk (char * restrict, int, size_t,
      const char * restrict, va_list);
extern int __vsnprintf_chk (char * restrict, size_t, int, size_t,
       const char * restrict, va_list);

int main(){
    
    int a = 6;
    
    printf("Hello Clang\n");
    
    return 0;
}

预处理流程内部处理包括宏的替换、头文件导入,以及类似的#if的处理。

语法分析

预处理完成后就会进行词法分析,这里会把代码切成一个个 Token,比如大小括号,等于号还有字符串等。

clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m

执行完毕可以看到文件

annot_module_include '#include <st'		Loc=<main.m:1:2>
int 'int'	 [StartOfLine]	Loc=<main.m:3:1>
identifier 'main'	 [LeadingSpace]	Loc=<main.m:3:5>
l_paren '('		Loc=<main.m:3:9>
r_paren ')'		Loc=<main.m:3:10>
l_brace '{'		Loc=<main.m:3:11>
int 'int'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:5:5>
identifier 'a'	 [LeadingSpace]	Loc=<main.m:5:9>
equal '='	 [LeadingSpace]	Loc=<main.m:5:11>
numeric_constant '6'	 [LeadingSpace]	Loc=<main.m:5:13 <Spelling=main.m:2:15>>
semi ';'		Loc=<main.m:5:18>
identifier 'printf'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:7:5>
l_paren '('		Loc=<main.m:7:11>
string_literal '"Hello Clang\n"'		Loc=<main.m:7:12>
r_paren ')'		Loc=<main.m:7:27>
semi ';'		Loc=<main.m:7:28>
return 'return'	 [StartOfLine] [LeadingSpace]	Loc=<main.m:9:5>
numeric_constant '0'	 [LeadingSpace]	Loc=<main.m:9:12>
semi ';'		Loc=<main.m:9:13>
r_brace '}'	 [StartOfLine]	Loc=<main.m:10:1>
eof ''		Loc=<main.m:10:2>

然后就是语法分析,验证语法是否正确,然后将所有的节点组成抽象语法树AST。

clang -fmodules -fsyntax-only -Xclang -ast-dump main.m

截取生成的抽象语法树一部分

完成语法的分析后就可以开始中间IR代码的生成了,CodeGen 会负责将语法树自顶向下遍历逐步翻译成 LLVM IR,IR 是编译过程的前端的输出后端的输入。

IR中间代码的生成

clang -S -fobjc-arc -emit-llvm main.m -o main.ll

打开查看man.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.13.0"

@.str = private unnamed_addr constant [13 x i8] c"Hello Clang\0A\00", align 1

; Function Attrs: noinline optnone ssp uwtable
define i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 6, i32* %2, align 4
  %3 = call i32 (i8*, ...) @printf(i8* getelementptr inbounds ([13 x i8], [13 x i8]* @.str, i32 0, i32 0))
  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,+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,+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}
!llvm.ident = !{!7}

!0 = !{i32 1, !"Objective-C Version", i32 2}
!1 = !{i32 1, !"Objective-C Image Info Version", i32 0}
!2 = !{i32 1, !"Objective-C Image Info Section", !"__DATA,__objc_imageinfo,regular,no_dead_strip"}
!3 = !{i32 4, !"Objective-C Garbage Collection", i32 0}
!4 = !{i32 1, !"Objective-C Class Properties", i32 64}
!5 = !{i32 1, !"wchar_size", i32 4}
!6 = !{i32 7, !"PIC Level", i32 2}
!7 = !{!"Apple LLVM version 9.1.0 (clang-902.0.39.1)"}

LLVM优化

在 Xcode 的编译设置里也可以设置优化级别-01,-03,-0s,还可以写些自己的 Pass。

Pass 是 LLVM 优化工作的一个节点,一个节点做些事,一起加起来就构成了 LLVM 完整的优化和转化。
如果开启了 bitcode 苹果会做进一步的优化,有新的后端架构还是可以用这份优化过的 bitcode 去生成。

clang -emit-llvm -c main.m -o main.bc

生成汇编

clang -S -fobjc-arc main.m -o main.s

打开main.s可以看到代码对应的汇编

	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 13
	.globl	_main                   ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Lcfi0:
	.cfi_def_cfa_offset 16
Lcfi1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Lcfi2:
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	leaq	L_.str(%rip), %rdi
	movl	$0, -4(%rbp)
	movl	$6, -8(%rbp)
	movb	$0, %al
	callq	_printf
	xorl	%ecx, %ecx
	movl	%eax, -12(%rbp)         ## 4-byte Spill
	movl	%ecx, %eax
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"Hello Clang\n"

	.section	__DATA,__objc_imageinfo,regular,no_dead_strip
L_OBJC_IMAGE_INFO:
	.long	0
	.long	64


.subsections_via_symbols

生成目标文件

clang -fmodules -c main.m -o main.o

目标文件就是对应cpu架构的二进制的机器指令了。

可以通过链接命令生成可执行文件,执行程序

clang main.o -o main
至执行
./main
输出
Hello Clang

Clang警告的处理

#pragma clang diagnostic push // 处理警告代码的其实位置
#pragma clang diagnostic ignored "-Wdeprecated-declarations" // -Wdeprecated-declarations需要处理警告的表示
        sizeLabel = [self sizeWithFont:font constrainedToSize:size lineBreakMode:NSLineBreakByWordWrapping]; // 需要处理警告的代码
#pragma clang diagnostic pop // 处理警告代码结束位置

对过警告的类型我们可以通过Xcode show the Report navigator查看,