编译器的工作过程 源码要运行必须先转成二进制的机器码。这是编译器的任务。比如下面这段源码假定文件名叫做test.c。#include int main(void) { fputs(Hello, world!\n, stdout); return 0; }要先用编译器处理一下才能运行。$ gcc test.c $ ./a.out Hello, world!对于复杂的项目编译过程还必须分成三步。$ ./configure $ make $ make install这些命令究竟是在做什么呢, 大部分的书籍以及资料, 对于此都是说得含糊不清, 仅仅表示如此这般便能够进行编译了, 却没有给出更进一步的阐释。通过本文, 将会对编译器的工作过程予以介绍, 此工作过程即为上面那三个命令各自所承担的任务。本人在参考时, 主要依据的是Alex Smith所撰写的名为《C》的文章。在此需要进行声明的是, 本文主要是针对gcc编译器而言的, 具体来说是针对C以及C的情况, 并不一定能够适用于其他语言的编译工作, 是这样的情况。第一步 配置想要编译器开始工作, 就得先知晓当下的系统环境, 标准库所处位置得清楚, 软件的安装位置也得知道, 还有哪些组件需要安装等情况。只因不同计算机的系统环境存在差异, 借助指定编译参数, 编译器便可灵活适应环境, 去编译出能在各种环境顺利运行的机器码。而确定编译参数的这个步骤, 就被称作“配置”。有一些配置信息, 它们被保存于一个配置文件里, 按照约定俗成的情况, 那是一个被叫做脚本文件的东西。一般而言, 它是借助工具生成的。编译器依靠运行这个脚本, 从而知晓编译参数。被编写的脚本已尽可能地将不同系统之间存在的那些差异作了考虑, 而且针对于各种各样的编译相关参数给出了默认状态下相应的值。倘若使用者的系统所处的环境是比较独特特别的, 又或者是有着一些特定的需求, 那么就需要以手动操作向该脚本提供与编译有关的参数。$ ./configure --prefix/www --with-mysql上边的代码属于php源码的一种编译配置, 用户指定了安装之后的文件, 要保存在www目录, 而且在编译的时候, 加入了mysql模块的支持。第二步 确定标准库和头文件的位置源码必然会用到标准库函数、头文件, 它们能够放置在系统的任意目录里, 编译器事实上没办法自动检测其位置, 唯有借助配置文件方可明白。完成编译的第二步操作, 也就是要从配置文件里弄清楚标准库以及头文件分别置于何处, 通常来讲, 配置文件会给出一连串的目录清单, 逐个列出几个明确的具体目录, 等到进入编译阶段时, 编译器就会依照顺序前往这几个目录当中, 去寻觅目标。第三步 确定依赖关系将大型项目而言, 源码文件相互之间常常存有依赖关系, 编译器得去确定编译的先后次序。假设A文件对B文件存有依赖, 编译器理应确保达成下面这两点。1只有在B文件编译完成后才开始编译A文件。2当B文件发生变化时A文件会被重新编译。在一个被称作的文件里保存着编译顺序, 其中列出了哪个文件要先进行编译, 哪个文件要后进行编译。并且文件借由脚本运行得以生成, 这便是编译的时候须得首先运行的缘由所在。除了确定依赖关系之外, 编译器还确定了, 编译的时候会用到哪些头文件。第四步 头文件的预编译存在不同的源码文件, 其有可能引用同一个头文件, 像stdio.h这样的, 在进行编译之际, 头文件也是必须要一同编译的, 为了达成节省时间这一目的, 编译器会于编译源码之前, 率先对头文件予以编译, 如此便确保了头文件仅需被编译一回, 而并非在每次使用到它的时候, 都要再次进行编译。不过, 并非头文件里的全部内容, 都会遭受预编译之处理。专门用以声明宏的#命令那儿, 定然不会被予以预编译。第五步 预处理做完预编译事情之后, 编译器便着手起头去替换掉源码中所说bash的头文件以及那些宏, 举例来说呀, 就以本文最顶端开头呈现开始的那段源码当作例子, 它是包含着头文件stdio.h的, 而替换之后所呈现出出现的样子就如同下面这样。extern int fputs(const char *, FILE *); extern FILE *stdout; int main(void) { fputs(Hello, world!\n, stdout); return 0; }为了让阅读更便利, 上面那段代码仅仅截取了头文件里跟源码有关联的那一部分, 也就是fputs以及FILE的声明, 把stdio.h的其他部分给省略掉了鉴于它们特别长。此外, 上面代码所涉及的头文件未曾经过预编译, 然而实际上, 插入源码的是预编译过后的成果。编译器在这一环节还会将注释给移除掉。“预处理”指的就是这一步, 因为一旦完成这一步之后, 便要着手开始真正的处理了。第六步 编译在进行预处理以后, 编译器便开始着手生成机器码。对于一些编译器而言, 存在这样一个中间步骤, 先是要把源码转变为汇编码, 之后还要将汇编码转化成机器码。下面是本文开头的那段源码转成的汇编码。.file test.c .section .rodata .LC0: .string Hello, world!\n .text .globl main .type main, function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movq stdout(%rip), %rax movq %rax, %rcx movl $14, %edx movl $1, %esi movl $.LC0, %edi call fwrite movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident GCC: (Debian 4.9.1-19) 4.9.1 .section .note.GNU-stack,,progbits这种转码后的文件称为对象文件 file。第七步 连接对象文件此刻仍无法运行, 得进一步转化为可执行文件才行。要是你认真去瞧上一步转化的结果, 就会发觉其中有函数的引用状况。也就是说, 要让程序正常运作起来, 除了上面提及的代码之外, 还必定得有这两个函数的代码, 它们是由C语言的标准库予以提供的。编译器接下来要做的工作, 是将外部函数的代码, 也就是通常后缀名为.lib和.a的文件, 添加进可执行文件中。这被称作连接。这种借助拷贝, 把外部函数库添加到可执行文件的方式, 叫做静态连接, 后文会提及还有动态连接。关于 make 命令所起到的作用, 其起始点是从第四步头文件预编译开始发动, 持续推进, 直至完成这一步骤, 最终达成相应结果。第八步 安装在上一步的时候, 连接是于内存里开展的, 也就是说, 编译器于内存之中生成了能够执行的文件。到了下一步, 就得把能够执行的文件保存至用户预先指定好的安装目录那里。看样子, 这一步挺简易, 只需把可执行文件连着相关的数据文件移至那边就搞定啦。但事实上, 这一步还得去做创建目录、留存文件、设定权限诸如此类的步骤。将这一整套的保存进程称作“安装”。第九步 操作系统连接安装可执行文件之后, 得按照某种途径去通知操作系统, 要让操作系统晓得能够去使用这个程序啦。比如说, 当我们安装了一款文本阅读程序时, 常常是期望双击txt文件后, 这个程序就会自行运行起来的。在操作系统里, 这便要求登记此程序的元数据, 诸如文件名、文件描述以及关联后缀名等。在Linux系统情况下, 这些信息常常被保存在位于/usr/share/目录下的.文件之中。此外, 在操作系统方面, 还得于Start启动菜单内, 构建起一个快捷方式。被叫做“操作系统连接”的是这些事情。make命令 , 用于去完成“安装”这一步 , 以及“操作系统连接”这一步。第十步 生成安装包写到这儿, 源码编译的全面进程就大体完成了。然而仅有极少量用户, 乐意忍着性子, 自始至终做一回这个进程。实际上, 要是你仅拥有源码能交付给用户, 他们会判定你是个不友善的家伙。多数用户所需的是一个二进制的可执行程序, 马上便能运行。这便要求开发者, 把上一步所生成的可执行文件, 制作为能够分发的安装包。因此, 编译器必然得具备生成安装包的能力方可。一般而言呀, 是把可执行文件连着相关的数据文件, 通过某种目录结构的形式, 存储为压缩文件包, 而后交付给用户。第十一步 动态连接 常规状况下, 进展至这一阶段时, 程序已然能够运行。至于在运行时间段当中所出现的各类状况, 和编译器是全然没有关联的。然而, 开发者能够于编译时期抉择可执行文件去连接外部函数库的具体方式, 究竟是采取静态连接也就是编译之时进行连接, 还是采用动态连接即运行之际进行连接。所以, 最后依旧需要提及一下, 究竟什么才被称作动态连接。先前所讲的, 静态连接乃是将外部函数库, 直接拷贝至可执行文件里。如此操作能够带来的益处是应用适配范畴较为广泛, 无需担忧用户主机缺乏相应库文件然而其不足之处却在于, 安装包裹规模会比较庞大, 而且针对诸多应用程序, 其间是没办法实现共用相同库文件的。动态连接所采取的方式乃完全截然相反, 即外部函数库不会被收纳进安装包内, 仅仅是在程序运行期间进行动态引用。它所具备的优势突出表现为, 安装包的体积能够保持较小, 多个应用程序能够共同使用相同的库文件但不利之处在于, 用户必须预先完成库文件的安装, 并且所用蓝本以及安装所在位置必须契合要求, 不然的话便无法正常运转。确切来讲, 于实际情形之中, 绝大多数的软件运用的是动态连接共享库文件, 这般的动态共享库文件, 针对Linux平台而言, 乃是后缀名称为.so的文件, 对于某一特定平台而言, 是.dll文件, 而针对于Mac平台来讲, 则是.dylib文件。原文出处