编译 想必都知道,那么LLVM是什么?
LLVM是一种编译器!LLVM编译流程是怎么样的?
本篇就LLVM进行初探
首先让我们来了解编译器是神马~
一、编译器是什么?
1、以python
程序为例
新建一个python文件
:helloDemo.py(内部一行代码打印hello
)
print("hello\n")
进入python文件
,执行python helloDemo.py
2、
c
语言程序为例
新建
helloDemo.c
文件:
#include
int main(int a,char * argv[]){
printf("hello \n");
return 0;
}
执行clang helloDemo.c
编译,生成a.out
文件,file a.out
查看文件
发现最终生成文件是:.out文件:64位的Mach-O可执行文件,当前clang出来的是x86_64架构, 说明mac电脑可读。 所以可以./a.out直接执行:
以上可以看到,python和C输出流程是完全不同的,pthyon直接用python命令就可以输出结果,而C需要先生成.out文件。这是由于两种编译过程不一样
- python是
解释型
语言,一边翻译一边执行,机器可直接执行。 - C语言是
编译型
语言,不可以直接执行,需要编译器将其转换成机器识别语言。
注:
编译型语言:编译后输出的是指令(0、1组合),cpu可直接执行指令
解释性语言:生成的是数据,不是0、1组合,机器也能直接识别
- 编译器就是将高级语言转换成机器可识别的语言(
可执行文件
)
补:
汇编语言是否需要编译?
直接解释
1、早期科学家,就是使用0、1编程。 为了摒弃手敲0和1的繁琐。开发者们创造了中间解释器,再创建出call、bl等这种容易记的指令来代表0、1组合(例如call对应00001111)。有了对应关系后,就好办了。程序员只用输入call、bl这样的标记指令,经过解释器,变成0和1的组合,就可以直接交给机器去执行了。 这就是汇编的由来。
2、基于汇编以上,再映射和封装相关对应关系。于是生成了跨时代性的c语言,再往上层封装,就出现了高级语言oc、swift、JAVA等语言。之所以汇编执行快,是因为它直接转换为机器语。
3、既然汇编速度这么快,为什么不都用汇编开发?
因为汇编的指令集,是针对同一操作系统而言,不支持跨平台。机器指令是cpu的在识别。早期的计算机厂家非常多,虽然都用0和1的组合,但相同组合背后却是相应不同的指令。所以汇编无法跨平台,不同操作系统下,汇编指令是不同的,所以需要可以跨平台的高级语言来开发~
二、LLVM概述
1、上面我们知道了编译器是什么。那么LLVM
是什么呢?
llVM
就是编译器的一种:
-
架构编译器(compiler
)的框架系统,以c++
编写而成,用于优化
以任意程序语言编写的程序的编译时间(compile-time)
、链接时间(link-time)
、运行时间(run-time)
以及空闲时间(idle-time)
,对开发者保持开放
,并兼任已有脚本 - 2006年Chris Lattner加盟Apple Inc.并致力于LLVM在Apple开发体系中的应用。
Apple
也是LLVM计划
的主要资助者
。
目前LLVM
已经被苹果iOS开发工具
、Xilinx Vivado、Facebook、Google等各大公司采用
2、传统编译器
- 编译器前端(Frontend):
编译器的前端任务是解析源代码。 会进行词法分析
、语法分析
、语义分析
。检查
源代码是否存在错误
,然后构建抽象语法树
(Abstract Syntax Tree AST),LLVM前端
还会生成中间代码(intermediate representation, IR
)
- 优化器(Optimizer)
优化器负责各种优化
。改善
代码的运行时间
,如消除冗余计算
等
源代码中有很多函数调用,就会需要需要申请很多函数调用栈空间,调用函数栈时,需要压栈输入数据,调用完毕后,出栈。其中过程逻辑可能非常复杂,那么就会无形中就会占用很多内存,优化器就是用来优化一些逻辑,以节省时间和空间。
- 后端(Backkend)/ 代码生成器(CodeGenerator)
将代码映射
到目标指令集
,生成机器语言
,并进行机器相关的代码优化
(目标指不同操作系统)
3、iOS
的编译器架构
Objective-C、C、C++
编译器的前端用的都是Clang
,Swift
使用的是swift
,后端使用的是llvm
三、LLVM的设计
1、llvm:支持多种语言
或多种硬件架构
。使用通用代码表示形式:IR
(用来在编译器中表示代码的形式)
⚠️GCC
也是一个非常成功的编译器
,但由于它作为整体
应用程序设计的,用途
受到了限制
因为llvm
使用IR
作为中间文件
,所以
LLVM
可以为任何
编程语言
独立编写前端
,也可以为任何
硬件架构
独立编写后端
.-
LLVM
不
是一个简单
的编译器
,而是架构编译器
,可以兼容
所有前端
和后端
。
2、Clang
Clang
是LLVM
项目的一个子项目
。基于LLVM架构
的轻量级编辑器
,诞生之初就是为了替代GCC
,提供更快
的编译速度
。 他是负责编译C、C++、Objecte-C语言
的编译器,它属于整个LLVM
架构中的编译器前端
对于开发者而言,研究Clang
可以给我们带来很出益处
下面就Clang
分析一下OC编译
流程
3、编译流程
-
新建一个.m文件
-
进入文件
Test2
执行命令clang -ccc-print-phases main.m
此步骤为7步骤
0: 输入文件:找到源文件
1: 预处理:宏的展开,头文件的导入
2: 编译:词法、语法、语义分析,最终生成IR
3: 汇编: LLVM通过一个个的Pass去优化,每个Pass做一些事,最后生成汇编代码
4: 生成 目标文件
5: 链接: 链接需要的动态库和静态库,生成可执行文件
6:架构可执行文件:通过不同架构,生成对应的可执行文件
这里并没有出现optimizer优化器,因为它是独立触发,与流程阶段无关
-
预处理
在.文件里定义一个宏:C
使用命令:clang -E main.m >> main2.m
,生成main2.m
文件
查看main2.m:大部分是stdio库的代码,定位到main函数里,发现
C变成了50了
得知,
预处理:完成2个步骤,
- 导入头文件 2.替换宏
还有一个定义类型的叫做typedef
,这里我们把 int
替换成wl_INT64
,试试是否
会还原
预处理结束:
发现tydef并没有被还原
由此我们可以根据预处理结果,做一些安全管理方面的混淆措施
使用define,将重要方法名称或者类进行替换。比如用#define BUY XXXDemo
代码中使用宏BUY,被hank时,实际代码是XXXDemo,不易被发现。
且#define的真实内容,不应该写成乱码,会让人有此地无银三百两的感觉,最好弄成系统类似名称或其他不经意的名称。才会被忽视,安全级别会更高 。
- 编译
【1】词法分析
使用clang -fmodules -fsyntax-only -Xclang -dump-tokens main.m
这些将代码拆分成一个个Token。标注了位置是第几行的第几个字符开始的。
【2】语法分析
是验证语法是否正确:
- 在词法分析的基础上,将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等,然后将所有节点组成抽象语法树(Abstract Syntax Tree,AST)。 语法分析程序判断源程序在结构上是否正确。
使用clang -fmodules -fsyntax-only -Xclang -ast-dump main.m
- 可以看到语法树。每一个操作都生成一个变量,作用域、数据类型、运算方式都在语法树体现出来。
- 语法树一次只能处理一次计算两次运算,就得多分一层。
- 语法分析,就是在生成语法树时完成检测的
如果头文件找不到,可以指定SDK
clang -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator12.2.sdk(自己SDK路径) -fmodules -fsyntax-only -Xclang -ast-dump main.m
-
生成中间代码IR(Intermediate representation)
完成以上步骤后,就开始生成中间代码IR,代码生成器(Code Generation)会将语法树自顶向下遍历逐步翻译成LLVM的IR。通过下面命令生成.ll文本文件,查看IR代码:
clang -S -fobjc-arc -emit-llvm main.m
代码在这一阶段进行runtime的桥接:property的合成,ARC处理等
IR的基本语法
@ 全局标识
% 局部标识
alloca 开辟空间
align 内存对齐
i32 32个bit,4个字节
store 写入内存
load 读取数据
call 调用数据
ret 返回
- IR的优化
llvm的优化级别分别是 -O0 -O1 -O2 -O3 -Os(O)
使用命令clang -Os -S -fobjc-arc -emit-llvm main.m -o main.ll
bitCode
Xcode7之后,开启bitCode苹果会做进一步的优化,生成.bc的中间代码,我们通过优化后的IR代码生成.bc文件
clang -emit-llvm -c main.ll -o main.bc
补:日常开发中,我们引入第三方库,有时会提示不支持bitcode,可以通过一、将源码编译一下,二、工程设置不支持.bitcode即可
-
生成汇编代码
我们通过最终生成的.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
-
生成目标文件(汇编器)
目标文件的生成,是汇编器以汇编代码作为输入,将汇编代码转换为机器代码,最后输出目标文件(object file)
clang -fmodules -c main.s -o main.o
通过nm命令,查看下main.o中的符号:nm -nm main.o
_printf 是一个是undefined external的
undefined表示在当前文件暂时找不到符号_printf
external表示这个符号是外部可以访问的。
以上打印结果表示是因为printf、PoolPop、PoolPush函数是来自于其他库,所以找不到,需要链接其他目标文件
- 生成可执行文件(链接)
连接器把编译产生的.o文件和(.dylib.a)文件,生成一个mach-o文件
clang main.o -o main
查看链接后的符号
nm -nm main
四、重新编辑Clang插件
由于国内网络限制,我们需要借助镜像下载LLVM源码
https://mirror.tuna.tsinghua.edu.cn/help/llvm/
- 在llvm的tools目录下下载Clang
cd llvm/tools
git clone https://mirrors.tuna.tsinghua.edu.cn/git/llvm/clang.git
- 在LLVM的projects目录下下载compiler-rtlibcxx、libcxxabi
- 在clang的tools下安装extra工具
- llvm编译,最新的只支持cmake来编译,所以需要安装cmake
- 通过brew安装cmake
brew install cmake
通过Xcode编译llvm
- cmake编译成Xcode项目
一顿猛如虎的操作后,电脑没烧坏的情况下,就编译成功了,然后创建插件,请听下回分解~