暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

iOS 编译原理探索

轻易科技技术中心 2019-05-28
646

前言:

iOS开发使用Objective-C和Swift编程语言,两者都是编译语言,也就是说,都需要通过编译才能执行;两者都需要通过编译器(Clang + LLVM)把代码编译生成机器码,机器码就可以直接在CPU上执行了。

 

1、Xcode编译器发展史

Xcode3 以前: GCC;

Xcode3: 增加LLVM,GCC(前端) + LLVM(后端);

Xcode4.2: 出现Clang - LLVM 3.0成为默认编译器;

Xcode4.6: LLVM 升级到4.2版本;

Xcode5: GCC被废弃,新的编译器是LLVM 5.0,从GCC过渡到Clang-LLVM的时代正式完成。


2、LLVM编译器设计简介(实现三相设计)

在基于LLVM的编译器中,前端负责解析,验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR(通常情况。但是也有例外,通过构建AST然后将AST转换为LLVM IR)。该IR可通过一系列改进代码的分析和优化过程提供,然后被发送到代码生成器以生成本机机器代码,如图下图所示:



3、使用三相设计的好处

首先解决了一个很大的问题:假如有N种语言(C、OC、C++、Swift...)的前端,同时也有M个架构(模拟器、arm64、x86...)的Target,是否就需要 N × M 个编译器?这时候三相架构的价值就体现出来了,通过共享优化器的中转,很好的解决了这个问题。假如你需要增加一种语言,只需要增加一种前端;假如你需要增加一种处理器架构,也只需要增加一种后端,而其他的地方都不需要改动。


 4、编译原理

将编译过程分为预处理、编译前端和编译后端三部分。

以下图片为编译的前后端流程图:


4.1、预处理顾名思义是预先处理,那预处理都做了哪些事情呢?内容如下:

4.1.1、import 头文件替换:

代码中会有很多 #import 宏,预处理的第一步就是将 #import 引入的文件代码放入对应文件。

4.1.2、 macro 宏展开:

带参数宏和不带参数宏。

4.1.3、处理其他的预编译指令(其实预编译过程也是处理预编译指令的过程):

条件编译语句也是在预处理阶段完成,并且条件编译只允许编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率,如以下代码就只会保留一个return语句,简单来说,“#”这个符号是编译器预处理的标志

#if DEBUG        
     return YES;
#else
     return NO;
#endif


4.2、Clang(编译器前端):GCC的替代品,Clang的编译速度比GCC快,负责解析、验证和诊断输入代码中的错误,然后将解析的代码转换为LLVM IR。

4.2.1、Lexer:

词法分析,读入源文件,并将其转化成字符流;只需要将源代码以字符文本的形式转化成Token流的形式,不涉及校验语义,不需要递归。

词法分析其实是编译器开始工作真正意义上的第一步,其所做的工作主要为:将输入的代码转换为一系列符合特定语言的词法单元,这些词法单元类型包括了关键字,操作符,变量等等。

可以通过下面两张图片将编译前后的内容一一对应起来。



4.2.2、Parser:

将字符流转换成AST(抽象语法树)。

4.2.3、SemanticAnalysis:

对输入的AST进行语法检查,对代码进行标记并不是Clang最终的目的;而是Clang的一个过程;其实标记代码为了让代码更便于转化成机器语言,标记代码转化成抽象语法树(abstract syntax tree – AST)是一个必经之路。

通过语法树进行代码静态分析,找出非语法性错误;模拟代码执行路径,分析出control-flow graph(CFG);预置常用Checker(检查器)。 

4.2.4、 Code Generation:

中间代码生成,将AST转换成低层次的IR指令;当通过Clang语法解析,代码没有出现报错,Clang前端就将进入最后一步:生成LLVM IR中间代码。

例如:使用命令 clang -S -emit-llvm main3.m -o main.ll生成LLVM 中间代码LLVM IR,如下图:

4.2.5、以下为生成的IR指令:


4.2.6、在这里简单介绍一些 LLVM IR 的指令:

%:局部变量

@:全局变量

alloca:分配内存堆栈

i32:32 位的整数

i32**:一个指向 32 位 int 值的指针的指针

align 4:向 4 个字节对齐,即便数据没有占用 4 个字节,也要为其分配四个字节

call:调用

4.2.7、LLVM IR 既是LLVM 前端的输出,也是LLVM 后端的输入,相当于前后端的桥接语言。

4.2.8、LLVM IR 有三种标识形式,但本质上是等价的,好比水有固、液、气三种状态:

4.2.8.1、text,便于阅读的文本格式,类似汇编语言,扩展名是.ll。

4.2.8.2、内存格式。

4.2.8.3、二进制格式,扩展名为.bc。


4.3、LLVM(编译器后端):

底层虚拟机(Low Level Virtual Machine),会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。

4.3.1、Optimization:

分析IR指令,将其中潜在会拖慢运行速度的指令干掉;

这一步的优化是非常重要的,很多直接转换来的代码是不合适且消耗内存的;因为是直接转换,所以必然会有这样的问题,而优化放在这一步的好处在于前端不需要考虑任何优化过程,减少了前端的开发工作。

4.3.2、 AsmPrinter:

通过IR(中间码)生成特定CPU架构的汇编代码。

4.3.3、Assemble :

将汇编代码转化成机器码。

4.3.4、 Linker:

将机器码链接成可执行程序或动态码。


4.4、BitCode

当我们提交程序到App Store上时,Xcode会将程序编译为一个中间表现形式( BitCode );然后App store 会再将这个BitCode编译为可执行的64位或32位程序。

BitCode类似于一个中间码,被上传到App Store之后,苹果会根据下载应用的用户的手机指令集类型生成只有该指令集的二进制进行下发,从而达到精简安装包体积的目的。



4.5、指令集

iOS模拟器没有arm指令集,编译运行的是x86/i386指令集。

4.5.1、模拟器的指令集:

4S、5:i386

5S以上:X64_86

真机(iOS设备)的默认指令集:

armv6设备:iPhone,iPhone2,iPhone3G

armv7设备:iPhone3GS,iPhone4,iPhone4S

armv7s设备:iPhone5,iPhone5C

arm64设备:iPhone5S以上

但是我们在编译的时候偶尔会报出i386,x86_64的错误?


4.5.2、解决办法:

查看静态库是否支持i386/x86_64的方法:

1、如果是.a文件:

打开终端,使用lipo -info 静态包地址.a查看

2、如果是framework文件:

cd到framework内部,之后在使用lipo -info xxxFramework查看


5、小结

通过对编译原理的认识,我们会更深层次地了解和认识编译过程。有利于我们去了解底层相关的知识,为程序的性能提升和优化打下基础。


文章转载自轻易科技技术中心,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论