美文网首页
编译器原理

编译器原理

作者: 康小曹 | 来源:发表于2020-06-29 10:35 被阅读0次

一、编译原理

1. 编译的概念

编译器是将源代码转化成机器码的软件;所以编译的过程则是将源代码转化成机器码的过程,也就是 cpu 可执行的二进制代码。

不要以为 clang -rewrite-objc 是编译,这只是 clang 提供的一个 OC 转 C/C++ 的一个工具;严格来说,因为平台、版本、环境等原因,这个指令生成的代码并不可靠,最可靠的代码还是源码;

编译的过程大概可以分为三个阶段,这也正是当前主流的编译器架构,即:编译前端(frontEnd)、中间代码优化(optimizer)、编译后端(backEnd)。

  • 编译前端:将源代码转化成中间代码。其详细过程包括:预处理、词法分析、语法分析、生成中间代码;

  • 中间代码优化:对编译器生成的中间代码进行一些优化,最终提供给编译后端;

  • 编译后端:根据不同的 cpu 架构,将中间代码汇编,产生汇编代码,最后解析汇编指令,生成目标代码,也就是机器码;

至此,编译器的工作结束。但是,机器码可以被 cpu 识别,却不能直接执行的。要生成可执行文件,还需要进行链接操作。

汇编只是编译中的一个过程,不要单独对待出来;

3. 链接

编译完成之后生成的是很多个二进制文件,文件中有 import 其他文件,此时单个的文件互相之间无法串联起来,需要进行链接。

链接主要是指将各个文件、静态库、动态库进行导入整合。其中最重要的部分就是链接器依靠重定位表对需要重定位的地址进行重新赋值;

因为在链接之前,.o 文件不知道自己 import 的文件的位置,也就不知道自己所调用的函数的地址,所以在生成 .o 文件时,这些函数地址会使用 0 代替,并使用重定位表进行记录。在链接时,依靠重定位表,将具体的函数地址,对每个需要重定位的地址进行重新赋值。

另外,需要说明的是,dyld 链接动态库,进行动态库的符号绑定是在程序运行之前,和编译过程相互独立,只不过编译链接生成的mach-o可执行文件中,对于动态库的函数地址会使用符号表的地址代替。dyld在程序运行之前将对应方法的地址复制到符号表中,最终使得程序运行时可以找到真实的动态库函数地址;

总结

对于 C 、C++ 、OC 这几门语言来讲,从源码到运行需要经历编译前端处理、中间代码优化、编译后端汇编和链接三个步骤。编译器接收源代码,输出由机器码组成的目标文件(二进制格式,.o 后缀),最后链接器将各个目标文件链接起来,执行重定位,最终生成可执行文件。

链接和编译是两个过程,分别由编译器和链接器来完成,只不过 LLVM 都实现了,所以我们再 Xcode 中只需要 build 就能生成可执行文件。准确而言,LLVM 已经不是单纯的编译器了,而是一个工具集合或者说是一个系统性的框架;

完整的可执行文件生成流程:

源码 -> 编译前端(词法分析等) -> 中间代码优化(中间代码) -> 生成汇编(汇编代码) -> 汇编解析 -> 单个的目标代码文件(机器码) -> 链接 -> 可执行文件(机器码)

  1. 编写源码;
  2. 编译前端处理,生成中间代码;
  3. 中间代码优化,将最终的中间代码传递给编译后端;
  4. 编译后端根据不同架构生成和架构对应的汇编代码;
  5. 编译后端对汇编代码进行解析,生成单个的目标文件,文件内部为机器码;
  6. 链接器对所有文件进行链接,生成可执行文件,也是机器码;

二、编译器的架构

传统编译器比如 gcc 会将编译的整个过程揉在一块,但是 LLVM 采用全新的架构,分为 frontEnd 、optimizer、backend三层架构:


LLVM架构

这样做的好处就是模块化的天然优势:
解耦:三个模块之间没有联系,相互独立;
可扩展性强:新出一个 CPU 架构时,只需要针对中间代码,新增一个对应的 Backend,其他模块基本不需要改变即可;

而 gcc 很可能一个模块的改动需要更新其他所有模块;

三、编译 VS 解释

编译型语言和解释型语言可以从三个角度进行区分:

1. 如何区分解释和编译语言

C 语言等编译型语言会最终生成包含机器码的可执行文件,程序运行时,所有代码统一执行。

javaScript 、python 等解释型语言会逐条翻译,逐条执行。

java 生成 .class 的字节码,类似于 C 语言中的中间代码。然后, .class 字节码会被加载进入 java 的虚拟机,逐条进行解释执行。如果单从这个角度就对语言进行区分,那么 java 是不是也可以称之为解释型语言呢?哈哈哈~~~

另外,因为 x86 架构比较复杂,即使是机器码,cpu 也会将其转化成更底层的代码。如此而言,硬件就相当于一个虚拟机的作用,那么 C 语言也有可能是运行时进行解释执行的,只不过是 cpu 在进行解释,那么如此而言, C 语言也可能是解释型语言??

其实,关键有两点:ATS(抽象语法树)是否已经生成、是否还需要转化成中间代码。

但是更关键的是。。。

2. 看清楚本质

其实区分解释型语言和编译型语言是后话。语言知识一种工具,适用场景决定了使用哪种工具。你难道要带个厨子去炒菜,用牛刀来杀鸡?

C 语言的优点是生成机器码,直接和硬件交互,速度基本可以达到极限值。但是其缺点也很明显,需要安装各种依赖库、导入各种系统库。

javaScript 速度慢,但是其有点也很明显。找个 ide 就能写,甚至是一个 text 都能写。写完放到不同的环境中,比如 node、比如浏览器,一份代码跨平台执行。

所以,C 语言的适用场景是那些对速度要求很高的场景,javaScript、python更多的使用在一些小工具、小脚本、Web开发等等。当然,javascript 的性能已经得到了较大的提升,但是即使如此,做大型游戏时,涉及到的计算量简直不要不要,这个时候你选择用 C++ 开发还是 javascript?

所以,使用需求决定使用场景,最终决定语言的选择。语言被选择之后又会反哺需求,会有和需求匹配的更多的系统库、三方库、三方代码等的出现。再之后,才会讨论变易语言和解释语言的区别,总感觉有点马后炮的意思。

相关文章

网友评论

      本文标题:编译器原理

      本文链接:https://www.haomeiwen.com/subject/dygnfktx.html