前言
我们会经常听到编译器这个词语,我们就会想什么是编译器,它的功能是什么,跟我们的开发又有什么关系,这篇文章就带大家走入LLVM编译器架构,揭开编译器的神秘面纱。
1 什么是编译器
我们用Python(解释型)和C(编译型)来先对比下
Python代码如下
print("hello world\n")
我们通过python py1.py命令执行下,看下效果,如图
python是python的解释器,这个就是解释型语言的效果。
我们再来看C,代码如下
#include<stdio.h>
int main(int argc,char * argv[]){
printf("hello world\n");
return 0;
}
我们通过命令clang hello.c,效果如下
我们看到并没有执行,而在我们的文件中多了一个a.out文件,在unix下,这是个可执行文件,我们再通过./a.out执行下,效果如图
3
我们看到了执行效果。
从这两个小小的案例可以看出,解释型语言和编译型语言的区别,
解释型语言读取代码就会执行,而编译型语言要先翻译成cpu可以读的二进制代码。
我们刚才的用的clang命令就是C,C++和Objective-C的编译器。
python就是python的解释器。
我们今天就从clang这个编译器开始说起。
2 LLVM介绍
LLVM概述
LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开话,并兼容已有脚本。
LLVM计划启动于2000年,最初由由美国UIUC大学的Chris Lattner博士主持开展。2006年Chris Lattner加盟Apple Inc.并致力LLVM在Apple开发体系中的应用。Apple也是LLVM计划的主要资助者。
目前LLVM已经被苹果iOS开发工具、Xilinx Vivado、Facebook、Google等各大公司采用。
传统编译器设计
编译器前端(Frontend)
编译器前端的任务是解析源代码。它会进行:词法分析、语法分析、检查源代码是否存在错误,然后构建抽象语法树(Abstract Syntax Tree AST),LLVM的前端还会生成中间代码(intermediate representation,IR)
优化器(Optimizer)
优化器负责进行各种优化。改善代码的运行时间,例始消除冗余计算ac等。
后端(Backend)/代码生成器(CodeGenerator)
将代码映财到目标指令集。生成机器语言,并且进行机器相关的代码优化。
iOS的编译器架构
Objcective C/C/C++使用的编译器前端是Clang,Swift是Swift,后端都是LLVM。
5
LLVM的设计
当编译器决定支持多种源语言或多种硬架构时,LLVM的最重要的地方就来了。
其它的编对器如GCC,它方法非常成功,但由于它是作为整体应用程序设计的,因此它的用途受到了很大的限制。
LLVM设计的最重要方便是,使用通用的代码表示形式(IR ),它是用来在编译器中表示代码的形式。所以LLVM可以为任何编译语言独立编写前端,并且可以为任意硬件架构独立编写后端。
6
Clang是LLVM项目的中的一个子项目。它是基于LLVM架构的轻量编译器,诞生之初是为了替代GCC,提供更快的编译速度。它是负责编译C、C++、Objective-C语言的编译器,它属于整个LLVM架构中的,编译器前端。对于开发者来说,研究Clang可以给我们带来很多好处。
3 编译流程分析
我们先看下一段代码,如下
#import <stdio.h>
int main(int argc, const char * argv[]) {
return 0;
}
我们通过命令clang -ccc-print-phases main.m执行
我们看编译的流程是什么样的。
- +- 0: input, "main.m", objective-c 读取代码。
- +- 1: preprocessor, {0}, objective-c-cpp-output 预处理价段,把宏替换,.h的导入进去。
- +- 2: compiler, {1}, ir 编译价段,前端编译器的任务。
- +- 3: backend, {2}, assembler 编译器后端,pass(环节,节点)优化,生成汇编代码。
- +- 4: assembler, {3}, object 生成目标文件。
- +- 5: linker, {4}, image 链接外部函数,静态库,动态库,生成镜像文件即可执行文件
- bind-arch, "x86_64", {5}, image 根据不同的架构生成不同的镜像文件。
编译流程的分析
1. 读取代码
读取我们编写的源代码。
2. 预处理
我们改下源码,如
#import <stdio.h>
#define C 30
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b +C);
return 0;
}
接着执行clang -E main.m >> main1.m,我们看下main1.m文件,
# 1 "main.m"
# 1 "<built-in>" 1
# 1 "<built-in>" 3
# 379 "<built-in>" 3
# 1 "<command line>" 1
# 1 "<built-in>" 2
# 1 "main.m" 2
这里是宏展开,我们看下main函数
int main(int argc, const char * argv[]) {
int a = 10;
int b = 20;
printf("%d",a + b +30);
return 0;
}
直接把我们的C这个宏展开直接替换成30。
我们还用过typedef,我们改下代码
#import <stdio.h>
typedef int RO_INT_64
int main(int argc, const char * argv[]) {
RO_INT_64 a = 10;
RO_INT_64 b = 20;
printf("%d",a + b);
return 0;
}
执行clang -E main.m >> main1.m,如
typedef int RO_INT_64
int main(int argc, const char * argv[]) {
RO_INT_64 a = 10;
RO_INT_64 b = 20;
printf("%d",a + b);
return 0;
}
没有展开,typedef只是取别名,增强可读性,不是预处理指令。
3.编译价段
3.1词法分析
我们再执行命令clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m,词法分析,会把代码切成token,如下所示
annot_module_include '#import <stdio.h>
#d' Loc=<main.m:2:1>
int 'int' [StartOfLine] Loc=<main.m:4:1>
identifier 'main' [LeadingSpace] Loc=<main.m:4:5>
l_paren '(' Loc=<main.m:4:9>
int 'int' Loc=<main.m:4:10>
identifier 'argc' [LeadingSpace] Loc=<main.m:4:14>
comma ',' Loc=<main.m:4:18>
const 'const' [LeadingSpace] Loc=<main.m:4:20>
char 'char' [LeadingSpace] Loc=<main.m:4:26>
star '*' [LeadingSpace] Loc=<main.m:4:31>
identifier 'argv' [LeadingSpace] Loc=<main.m:4:33>
l_square '[' Loc=<main.m:4:37>
r_square ']' Loc=<main.m:4:38>
r_paren ')' Loc=<main.m:4:39>
l_brace '{' [LeadingSpace] Loc=<main.m:4:41>
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 '10' [LeadingSpace] Loc=<main.m:5:13>
semi ';' Loc=<main.m:5:15>
int 'int' [StartOfLine] [LeadingSpace] Loc=<main.m:6:5>
identifier 'b' [LeadingSpace] Loc=<main.m:6:9>
equal '=' [LeadingSpace] Loc=<main.m:6:11>
numeric_constant '20' [LeadingSpace] Loc=<main.m:6:13>
semi ';' Loc=<main.m:6:15>
identifier 'printf' [StartOfLine] [LeadingSpace] Loc=<main.m:7:5>
l_paren '(' Loc=<main.m:7:11>
string_literal '"%d"' Loc=<main.m:7:12>
comma ',' Loc=<main.m:7:16>
identifier 'a' Loc=<main.m:7:17>
plus '+' [LeadingSpace] Loc=<main.m:7:19>
identifier 'b' [LeadingSpace] Loc=<main.m:7:21>
plus '+' [LeadingSpace] Loc=<main.m:7:23>
numeric_constant '30' Loc=<main.m:7:24 <Spelling=main.m:3:11>>
r_paren ')' Loc=<main.m:7:25>
semi ';' Loc=<main.m:7:26>
return 'return' [StartOfLine] [LeadingSpace] Loc=<main.m:8:5>
numeric_constant '0' [LeadingSpace] Loc=<main.m:8:12>
semi ';' Loc=<main.m:8:13>
r_brace '}' [StartOfLine] Loc=<main.m:9:1>
eof '' Loc=<main.m:9:2>
会把代码切成token,比如大小括号,等于号还有字符串等。
3.2语法分析
检查语法是否正确,在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等,然后将所有节点组成抽像语法树(Abstract Syntax Tree,AST)。语法分析程序判断源程序在结构上是否正确。
我们执行clang -fmodules -fsyntax-only -Xclang -ast-dump main.m,
我们把代码改错,看下效果
9
这里有错误提示。
我分析下语法树
-FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char **)'
|-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char **':'const char **'
`-CompoundStmt 0x7f9aed0bf7d0 <col:41, line:10:1>
|-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
| `-VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit
| `-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
|-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
| `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' cinit
| `-IntegerLiteral 0x7f9aed0bf0a0 <col:19> 'int' 20
|-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
| |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'
| |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
| | `-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay>
| | `-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"
| `-BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'
| |-BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
| | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
| | | `-DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int'
| | `-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
| | `-DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int'
| `-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30
`-ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12>
`-IntegerLiteral 0x7f9aed0bf7a0 <col:12> 'int' 0
- FunctionDecl 0x7f9aed0bee00 <line:5:1, line:10:1> line:5:5 main 'int (int, const char )'
|-ParmVarDecl 0x7f9aed01e140 <col:10, col:14> col:14 argc 'int'
|-ParmVarDecl 0x7f9aed01e288 <col:20, col:38> col:33 argv 'const char ':'const char ** 这里就是main函数,返回值int,参数int和char,参数名称arc,int类型,参数argv const char类型 - |-CallExpr 0x7f9aed0bf740 <line:8:5, col:25> 'int'
| |-ImplicitCastExpr 0x7f9aed0bf728 <col:5> 'int (*)(const char *, ...)' <FunctionToPointerDecay>
| | `-DeclRefExpr 0x7f9aed0bf550 <col:5> 'int (const char *, ...)' Function 0x7f9aed0bf0c8 'printf' 'int (const char *, ...)'这里有一个函数的调用printf,返回int类型。 - |-ImplicitCastExpr 0x7f9aed0bf788 <col:12> 'const char *' <NoOp>
| |-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |
-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d" 这是第一个参数 - |-DeclStmt 0x7f9aed0bf010 <line:6:5, col:21>
|-VarDecl 0x7f9aed0bef88 <col:5, col:19> col:15 used a 'RO_INT_64':'int' cinit |
-IntegerLiteral 0x7f9aed0beff0 <col:19> 'int' 10
|-DeclStmt 0x7f9aed0bf538 <line:7:5, col:21>
| `-VarDecl 0x7f9aed0bf038 <col:5, col:19> col:15 used b 'RO_INT_64':'int' 这里是a,b - | |
-ImplicitCastExpr 0x7f9aed0bf770 <col:12> 'char *' <ArrayToPointerDecay> | |
-StringLiteral 0x7f9aed0bf5b0 <col:12> 'char [3]' lvalue "%d"这是第一个参数 - BinaryOperator 0x7f9aed0bf6b0 <col:17, line:3:11> 'int' '+'是+运算结果,
- BinaryOperator 0x7f9aed0bf670 <line:8:17, col:21> 'int' '+'
| | |-ImplicitCastExpr 0x7f9aed0bf640 <col:17> 'RO_INT_64':'int' <LValueToRValue>
| | |-DeclRefExpr 0x7f9aed0bf5d0 <col:17> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bef88 'a' 'RO_INT_64':'int' | |
-ImplicitCastExpr 0x7f9aed0bf658 <col:21> 'RO_INT_64':'int' <LValueToRValue>
| |-DeclRefExpr 0x7f9aed0bf608 <col:21> 'RO_INT_64':'int' lvalue Var 0x7f9aed0bf038 'b' 'RO_INT_64':'int' |
-IntegerLiteral 0x7f9aed0bf690 <line:3:11> 'int' 30 第一个加法运算的结果+30 - ReturnStmt 0x7f9aed0bf7c0 <line:9:5, col:12> 这里是返回
- 返回int类型值为0
3.4 生成中间代码(intermediate representation )
代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM IR。
IR基本语法
@全局标识
%局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit
store写入内存
load读取数据
call调用函数
ret返回
我们改下代码
#import <stdio.h>
#define C 30
typedef int RO_INT_64;
int test(int a, int b) {
return a+ b +3;
}
int main(int argc, const char * argv[]) {
int a = test(1, 2);
printf("%d", a);
return 0;
}
我们执行命令clang -S -fobjc-arc -emit-llvm main.m,会生成main.ll文件,我们看下main.ll文件内容
define i32 @test(i32 %0, i32 %1) #0 { #test(int a, int b )
%3 = alloca i32, align 4 #开辟空间 4字节对齐 int a3;
%4 = alloca i32, align 4 #开辟空间 4字节对齐 int a4;
store i32 %0, i32* %3, align 4 # a3=a;
store i32 %1, i32* %4, align 4 # a4=b;
%5 = load i32, i32* %3, align 4 # int a5=a3;
%6 = load i32, i32* %4, align 4 # int a6=a4;
%7 = add nsw i32 %5, %6 # int a7 = a5+a6;
%8 = add nsw i32 %7, 3 # int a8= a7+ 3;
ret i32 %8 # return a8;
}
这就是test函数IR代码,这是没有经过优化的。
IR的优化
clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
经过优化会简洁很多,这里不再赘述。
xcode中的Optimization Level可以设置。
bitCode
clang -emit-llvm -c main.ll -o main.bc
4 生成汇编代码
我们通过最终的.bc或者.ll代码生成汇编代码
命令
clang -S -fobjc-arc main.bc -o main.s
clang -S -fobjc-arc main.ll -o main.s
生成汇编代码也可以进行优化
clang -Os -S -fobjc-arc main.m -o main.s
执行命令
clang -S -fobjc-arc main.ll -o main.s
_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
这是x86的汇编指令集。
我们再执行这个clang -Os -S -fobjc-arc main.m -o main.s优化的命令
_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
## kill: def $esi killed $esi def $rsi
## kill: def $edi killed $edi def $rdi
leal 3(%rdi,%rsi), %eax
popq %rbp
retq
.cfi_endproc
## -- End function
.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 $6, %esi
这是经过优化过的,main的函数调用的test直接优化成了6。
5 生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输了目标文件(object file)。这里属于后端的作务。
执行命令clang -fmodules -c main.s -o main.o,生成的main.o就是目标文件。通过xcrun nm -nm main.o查看符号,如下所示
(undefined) external _printf
0000000000000000 (__TEXT,__text) external _test
000000000000000a (__TEXT,__text) external _main
_printf是一个undefined external的符号。
undefined表示当前文件暂时找不到符号。
external表示这个符号是外部可以访问的。
5 生成可执行文件(链接)
连接器把编译产生的.o文件和(.dylib.a)文件,生成一个macho-o文件。
我们执行命令clang main.o -o main生成了可执行文件main。
我们再通过命令xcrun nm -nm main,如下
(undefined) external _printf (from libSystem)
(undefined) external dyld_stub_binder (from libSystem)
0000000100000000 (__TEXT,__text) [referenced dynamically] external __mh_execute_header
0000000100003f6d (__TEXT,__text) external _test
0000000100003f77 (__TEXT,__text) external _main
0000000100008008 (__DATA,__data) non-external __dyld_private
这里有两个外部符号_printf(可以找到)和dyld_stub_binder。
当我们的程序进入内存的的时候,外部函数会立即跟dyld_stub_binder绑定,这个dyld是强制执行,链接是打个标记,符号在哪个库中(编译期),绑定是在执行的时候把外部函数地址和符号进行绑定(运行期),一定会有dyld_stub_binder这个符号,先绑定这个符号,其它函数的绑定由dyld_stub_binder执行。
总结编译器的流程:
- 前端:读取代码,词法分析,语法分析,语义分析,生成AST(生成IR)
- 优化器:根据一个个的pass进行优化,
- 后端:生成汇编,根据不同的架构生成可执行文件
LLVM最大的好处:前后端分离。
pass的解释:就是“遍历一遍IR,可以同时对它做一些操作”的意思。翻译成中文应该叫“趟”。 在实现上,LLVM的核心库中会给你一些 Pass类 去继承。你需要实现它的一些方法。 最后使用LLVM的编译器会把它翻译得到的IR传入Pass里,给你遍历和修改
总结
这篇文章带大家初步了解了编译器的原理,LLVM的架构。分析了编译的流程,希望这篇文章可以让大家学习到新的知识。
网友评论