C/C++ 从0到1系统精讲 项目开发综合基础K

  二、说到编译器当然少不了Apple

  关于Apple的编译器,就不得不说一下GCC与LLVM的相爱相杀了。由于编译器触及到从高级开发言语到低级言语的转换处置,复杂度自然不用多说。我们都晓得Apple产品软件的开发言语是Objective-C,能够以为是对C言语的扩展。而C言语所运用的编译器则是大名鼎鼎的GCC,此时的GCC肯定是妥妥的大哥了,所以早些年为了不用要的资源投入,关于自家OC(Objective-C简称OC)编译器的开发索性直接拿大哥的代码GCC停止二次开发了,没错,从主干版本中拉个独立分支搞起。这么看的话,Apple早期就曾经开端了降本增效了?

  随着OC言语的不时迭代开展,言语特性也就愈来愈多,那编译器的新特性才能支持当然也得跟得上啊?但是C也在不时的迭代开展,GCC编译器的主干功用当然也越来越多,OMG!单独维护的OC编译器版本对GCC主干的新功用并没有很好的同步,关键在兼并功用的时分不可防止的呈现种种抵触。为此,Apple曾屡次申请与GCC主干功用兼并同步,GCC乍一看都是OC 特性feature,跟C有毛线关系?所以关于兼并的优先级总是排到最低,Apple也是没有方法,结果只能是差别化的东西越来越多,编译器的维护本钱也变得异常之高。

  除了以上的问题之外,GCC整体的架构设计也是非模块化的,那什么是模块化呢?比方我们通常在系统设计的时分,会将各个系统的功用停止模块化分割设计,不同的模块可以单独为系统内部提供不同的功用。同时呢,我们还能把这些模块单独抽离出来提供应外部运用,这就增大了系统的底层的灵敏度,简单说就是可以直接运用模块化的接口才能。

  所以Apple深知定制化的GCC编译器将是后续言语迭代晋级的绊脚石,内部也在不时的探究可以替代GCC的替代品。在编译器的探究路上,这里不得不说一下Apple的一位神级工程师 Chris Lattner(克里斯·拉特纳),可能光说名字的话可能没有太多人晓得他,那假如要说Swift言语的开创人是不是就有所耳闻了?由于克里斯在大学期间对编译器的细致的研讨,发起了LLVM(Low Level Virtual Machine)项目对编译的源代码停止了整体的优化。Apple将眼光放在了克里斯团队身上,同时直接顾用了他们团队,当然克里斯也没有孤负众望,在 Xcode从 3.1完成了llvm-gcc compiler,到 3.2完成了Clang 1.0, 再到4.0完成了Clang 2.0 ,后来在Mac OS X 10.6 开端运用LLVM的编译技术,到如今曾经将LLVM开展成为了Apple的中心编译器。

  三、LLVM编译器的编译过程与特性

  关于传统的编译器,主要分为前端、优化器和后端,援用一张通用的简约的编译过程图,如下:

  简单来说,针关于源代码翻译成计算机底层代码的过程中呢要阅历三个阶段:前端编译、优化器优化、后端编译。经过前端编译之后,针对编译的产物停止优化处置,最后经过后端完成机器码的生成。而关于LLVM编译器来说,这里我们以OC的前端编译器Clang为例,它担任LLVM的前端的整体编译流程(预处置、词法剖析、语法剖析和语义剖析),生成中间产物LLVMIR,最后由后端停止架构处置生成目的代码,如下图:

  能够看出LLVM将编译的前后端独立分开了,前端担任不同言语的编译操作,假如增加一个言语的编译支持,只需求扩展支持当前言语的前端编译支持(Clang担任OC前端编译、SwiftC担任Swift前端编译)即可,优化器与后端编译器整体均不用修正即可完成新增言语的支持。同理,关于后端,假如需求新增新的架构设备的支持,只需求扩展后端架构对应编译器的支持即可完成新架构设备的支持,这也是LLVM编译器的优点之一。

  3.1、编译器前端

  在XCode中针关于OC与Swift的编译有着不同的前端编译器,OC采用Clang停止编译,而Swift则采用SwiftC编译器,两种不同的编译器前端在编译之后,生成的中间产物都是LLVMIR。这也就解释了关于高级言语Swift或者OC开发,哪怕是混编,在经过各自的编译器前端编译之后,最终的编译产物都是一样的,所以选用哪种开发言语关于最终生成的中间代码IR都是通用的。关于Clang的整体编译过程,如下图所示:

  预处置

  经过对源代码中以“#”号开头如包含#include,宏定义制定#define等扫描。然后停止源代码定义交换,停止头文件内容的展开。经过预处置器把源文件处置成.i文件。

  词法剖析

  在词法剖析完成之后会生成 token 产物,它是做什么的?这里不贴官方的解释了,简单点说就是对源代码的原子切分,切分红可以底层描绘的单个原子,就是所谓的token,至于token长什么样子?能够经过 clang 的命令执行编译查看生成的原子内容:

  clang -fmodules -E -Xclang -dump-tokens xxx.m

  #import <UIKit/UIKit.h>

  #import "AppDelegate.h"

  int main(int argc, char * argv[]) {

  NSString * appDelegateClassName;

  @autoreleasepool {

  // Setup code that might create autoreleased objects goes here.

  appDelegateClassName = NSStringFromClass([AppDelegate class]);

  int a = 0;

  }

  return UIApplicationMain(argc, argv, nil, appDelegateClassName);

  }

  我们拿工程的main.m 做个测试,编译生成的内容如下:

  注:假如遇到 main.m:8:9: fatal error: 'UIKit/UIKit.h' file not found 错误,能够加上系统根底库途径如下:

  clang

  -fmodules

  -E

  -Xclang

  -dump-tokens

  -isysroot

  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

  main.m

  能够发现,计算机在停止源码处置的时分,并不能像人一样可以了解整个源码内容的含义。所以为了停止转换,在停止源码剖析的时分,将整体的内容停止单词切分,构成原子为后续的语义剖析做准备,整体的切分过程大致采用的是状态机原理。

  语法剖析

  在完成词法剖析之后,编译器大致了解了每个源码中的单词的意义,但是关于单词组合起来的语句内容并不能了解。所以接下来需求对单词组合起来的内容停止辨认,也就是我们所说的**语法剖析**。 语法剖析的原理有点模板匹配的意义,怎样了解呢?就是我们常说的语法规则,在编译器中预置了相关言语的语法规则模板,假如匹配了相关的规则,则依照相关语法规则停止解析。举个例子,比方我们在OC中写一个这样的语句:

  int a = 100;

  这是一种通用的赋值语法格式,所以在编译器停止语法剖析的时分,将其依照赋值语法的规则停止解析,如下:

  经过对原子token的组合解析,最终会生成了一个笼统语法树(AST),AST笼统语法树将源代码转换成树状的数据构造,它描绘了源代码的内容含义以及内容构造,它的生成可以让计算机更好的了解和处置中间产物。以XCode生成的默许项目的main.m内容为例,在 clang 中我们照旧能够查看详细的笼统生成树(AST)的样子,能够对源码停止如下的编译:

  clang

  -isysroot

  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

  -fmodules

  -fsyntax-only

  -Xclang

  -ast-dump

  main.m

  编译后的结果如下:

  简单转换一下树形视图,大致长这样:

  能够发现,阅历过语法剖析之后,源代码转换成了详细的数据构造,而数据构造的整体生成是后续停止语义剖析生成中间代码的根底前提。

  语义剖析

  在阅历过语法剖析之后,编译器会对语法剖析之后生成的笼统语法树(AST)再次停止处置,需求留意的是编译器并不会直接经过AST编译成目的代码,主要缘由是由于编译器将编译过程拆分了前后端,而前后端的通讯的媒介就是IR,没错就是之前提到过的LLVMIR这样一个中间产物。该中间产物与言语无关,同时与cpu的架构也无关,那么为什么要加上中间产物这个环节,直接生成目的代码难道不是更好吗?我们都晓得cpu的不同架构直接影响cpu的指令集,不同的指令集对应不同的汇编指令,所以针关于不同的cpu架构要对应生成不同适配的汇编指令才干正常的运转到不同的cpu架构的机器上。假如将前后端的编译过程绑定死,那么就会招致每增加一个新的编译前端,同时增加对一切cpu架构的后端的支持(1对n的关系),同理,假如增加新的一个cpu架构支持,编译前端也需求统统再完成一遍,这个工作量是很反复以及繁琐的。所以为了防止这样的问题,Apple对编译器的前后端停止了拆分,用中间产物来停止前后端的逻辑适配。

  关于语义剖析生成中间产物的过程,也能够经过 Clang 的编译命令查看,详细如下:

  # 生成扩展为.ll的便于阅读的文本格式

  clang

  -isysroot

  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

  -S

  -emit-llvm

  main.m

  -o

  main.ll

  # 生成二进制格式,扩展为.bc

  clang

  -isysroot

  /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk

  -emit-llvm

  -c

  main.m

  -o

  main.bc

  从编译的产物来看,其中也包含了常见的内存分配、所用到的标识定义等内容,能够明显的发现生成的中间产物曾经没有任何源代码言语的影子了。同时我们会发现针关于中间代码,存放器(%+数字)的运用仿佛没有个数限制,为什么呢?由于中间代码只是将源代码停止了中间代码的描绘转义,此时并没有相关的目的架构信息可供参考运用,所以针关于变量的援用也仅仅是中间层的标识。在后端编译的过程中会将中间的这些存放器的援用再次停止指令的转换,最终会生成对应CPU架构指令集的汇编代码。

  还记得XCode中的BitCode开关选项吗?它决议了编译生成的中间产物IR能否需求保管,假如保管的话,会把当前的中间产物插入到可执行文件的数据段中,保存这些中间产物内容又有什么作用呢?我们晓得在没有保存中间产物之前,为了确保一切cpu架构的机型可以正常装置打出的装置包,在打包的时分会把可以支持的一切cpu架构的汇合停止兼并打包,生成一个Fat Binary,确保装置包可以适配一切的机型,这样会有一个问题,比方ARM64架构的机器在装置的时分只需求ARM64的架构二进制文件即可,但是由于装置包里兼容了一切的cpu架构,其他的架构代码实践上基本没有用到,这也就间接的招致了装置包的体积变大。而苹果在应用分发的时分,是晓得目的机器的cpu架构的,所以假如可以将中间的编译产物交给AppStore后台,由Appstore后台经过编译后端优化生成目的机器的二进制可执行文件,去除无用的兼容架构代码,进而缩减装置包的体积大小。这也即是BitCode的呈现目的,为理解决编译架构冗余的问题,同时也为APP的瘦身提供参考。

  编译器在停止语义剖析期间还有一个重要的过程叫做静态剖析(Static Analysis),llvm官方文档是这样引见静态剖析的:

  The term "static analysis" is conflated, but here we use it to mean a collection of algorithms and techniques used to analyze source code in order to automatically find bugs. The idea is similar in spirit to compiler warnings (which can be useful for finding coding errors) but to take that idea a step further and find bugs that are traditionally found using run-time debugging techniques such as testing.?

  Static analysis bug-finding tools have evolved over the last several decades from basic syntactic checkers to those that find deep bugs by reasoning about the semantics of code. The goal of the Clang Static Analyzer is to provide a industrial-quality static analysis framework for analyzing C, C++, and Objective-C programs that is freely available, extensible, and has a high quality of implementation.

  静态剖析它可以协助我们在编译期间自动查找错误,比起运转时的时分去找出错误要更早一步,能够用于剖析 C、C++ 和 Objective-C 程序。编译器经过静态剖析根据AST中节点与节点之间的关系,找出有问题的节点并抛出正告错误,到达修正提示的目的。比方官方文档中引见的内存泄露的静态剖析的案例:

  除了官方的静态剖析,我们常用的OCLint也是在编译器生成AST笼统语法树之后,对笼统语法树停止遍历剖析,到达校验标准的目的,总结一下编译前端的所阅历的流程:经过源码输入,对源码停止词法剖析将源码停止内容切割生成原子token。经过语法剖析对原子token的组合停止语法模板匹配,生成笼统语法树(AST)。经过语义剖析,对笼统语法树停止遍历生成中间代码IR与符号表信息内容。

  3.2、编译器后端

  编译器后端主要做了两件重要的事情: 1、优化中间层代码LLVMIR(阅历屡次的Pass操作) 2、生成汇编代码,最终链接生成机器码

  编译器前端完成编译后,生成了相关的编译产物LLVMIR,LLVMIR会经过优化器停止优化,优化的过程会阅历一个又一个的Pass操作,什么是Pass呢?援用官方的解释:

  The LLVM Pass Framework is an important part of the LLVM system, because LLVM passes are where most of the interesting parts of the compiler exist. Passes perform the transformations and optimizations that make up the compiler, they build the analysis results that are used by these transformations, and they are, above all, a structuring technique for compiler code.

  我们能够了解为一个个的中间过程的优化,比方指令选择、指令调度、存放器的分配等,输入输出也都是IR,如下图: