源码要运行,必须先转成二进制的机器码。这是编译器的任务。php
好比,下面这段源码(假定文件名叫作test.c)。html
#include <stdio.h> int main(void) { fputs("Hello, world!\n", stdout); return 0; }
要先用编译器处理一下,才能运行。mysql
$ gcc test.c $ ./a.out Hello, world!
对于复杂的项目,编译过程还必须分红三步。sql
$ ./configure $ make $ make install
这些命令到底在干什么?大多数的书籍和资料,都语焉不详,只说这样就能够编译了,没有进一步的解释。bash
本文将介绍编译器的工做过程,也就是上面这三个命令各自的任务。我主要参考了Alex Smith的文章《Building C Projects》。须要声明的是,本文主要针对gcc编译器,也就是针对C和C++,不必定适用于其余语言的编译。app
编译器在开始工做以前,须要知道当前的系统环境,好比标准库在哪里、软件的安装位置在哪里、须要安装哪些组件等等。这是由于不一样计算机的系统环境不 同样,经过指定编译参数,编译器就能够灵活适应环境,编译出各类环境都能运行的机器码。这个肯定编译参数的步骤,就叫作"配置"(configure)。ide
这些配置信息保存在一个配置文件之中,约定俗成是一个叫作configure的脚本文件。一般它是由autoconf工具生成的。编译器经过运行这个脚本,获知编译参数。函数
configure脚本已经尽可能考虑到不一样系统的差别,而且对各类编译参数给出了默认值。若是用户的系统环境比较特别,或者有一些特定的需求,就须要手动向configure脚本提供编译参数。工具
$ ./configure --prefix=/www --with-mysql
上面代码是php源码的一种编译配置,用户指定安装后的文件保存在www目录,而且编译时加入mysql模块的支持。ui
源码确定会用到标准库函数(standard library)和头文件(header)。它们能够存放在系统的任意目录中,编译器实际上没办法自动检测它们的位置,只有经过配置文件才能知道。
编译的第二步,就是从配置文件中知道标准库和头文件的位置。通常来讲,配置文件会给出一个清单,列出几个具体的目录。等到编译时,编译器就按顺序到这几个目录中,寻找目标。
对于大型项目来讲,源码文件之间每每存在依赖关系,编译器须要肯定编译的前后顺序。假定A文件依赖于B文件,编译器应该保证作到下面两点。
(1)只有在B文件编译完成后,才开始编译A文件。
(2)当B文件发生变化时,A文件会被从新编译。
编译顺序保存在一个叫作makefile的文件中,里面列出哪一个文件先编译,哪一个文件后编译。而makefile文件由configure脚本运行生成,这就是为何编译时configure必须首先运行的缘由。
在肯定依赖关系的同时,编译器也肯定了,编译时会用到哪些头文件。
不一样的源码文件,可能引用同一个头文件(好比stdio.h)。编译的时候,头文件也必须一块儿编译。为了节省时间,编译器会在编译源码以前,先编译头文件。这保证了头文件只需编译一次,没必要每次用到的时候,都从新编译了。
不过,并非头文件的全部内容,都会被预编译。用来声明宏的#define命令,就不会被预编译。
预编译完成后,编译器就开始替换掉源码中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的其余部分(由于它们很是长)。另外,上面代码的头文件没有通过预编译,而实际上,插入源码的是预编译后的结果。编译器在这一步还会移除注释。
这一步称为"预处理"(Preprocessing),由于完成以后,就要开始真正的处理了。
预处理以后,编译器就开始生成机器码。对于某些编译器来讲,还存在一个中间步骤,会先把源码转为汇编码(assembly),而后再把汇编码转为机器码。
下面是本文开头的那段源码转成的汇编码。
.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
这种转码后的文件称为对象文件(object file)。
对象文件还不能运行,必须进一步转成可执行文件。若是你仔细看上一步的转码结果,会发现其中引用了stdout函数和fwrite函数。也就是说,程序要正常运行,除了上面的代码之外,还必须有stdout和fwrite这两个函数的代码,它们是由C语言的标准库提供的。
编译器的下一步工做,就是把外部函数的代码(一般是后缀名为.lib和.a的文件),添加到可执行文件中。这就叫作链接(linking)。这种经过拷贝,将外部函数库添加到可执行文件的方式,叫作静态链接(static linking),后文会提到还有动态链接(dynamic linking)。
make命令的做用,就是从第四步头文件预编译开始,一直到作完这一步。
上一步的链接是在内存中进行的,即编译器在内存中生成了可执行文件。下一步,必须将可执行文件保存到用户事先指定的安装目录。
表面上,这一步很简单,就是将可执行文件(连带相关的数据文件)拷贝过去就好了。可是实际上,这一步还必须完成建立目录、保存文件、设置权限等步骤。这整个的保存过程就称为"安装"(Installation)。
可执行文件安装后,必须以某种方式通知操做系统,让其知道可使用这个程序了。好比,咱们安装了一个文本阅读程序,每每但愿双击txt文件,该程序就会自动运行。
这就要求在操做系统中,登记这个程序的元数据:文件名、文件描述、关联后缀名等等。Linux系统中,这些信息一般保存在/usr/share /applications目录下的.desktop文件中。另外,在Windows操做系统中,还须要在Start启动菜单中,创建一个快捷方式。
这些事情就叫作"操做系统链接"。make install命令,就用来完成"安装"和"操做系统链接"这两步。
写到这里,源码编译的整个过程就基本完成了。可是只有不多一部分用户,愿意耐着性子,从头至尾作一遍这个过程。事实上,若是你只有源码能够交给用 户,他们会认定你是一个不友好的家伙。大部分用户要的是一个二进制的可执行程序,马上就能运行。这就要求开发者,将上一步生成的可执行文件,作成能够分发 的安装包。
因此,编译器还必须有生成安装包的功能。一般是将可执行文件(连带相关的数据文件),以某种目录结构,保存成压缩文件包,交给用户。
正常状况下,到这一步,程序已经能够运行了。至于运行期间(runtime)发生的事情,与编译器一律无关。可是,开发者能够在编译阶段选择可执行 文件链接外部函数库的方式,究竟是静态链接(编译时链接),仍是动态链接(运行时链接)。因此,最后还要提一下,什么叫作动态链接。
前面已经说过,静态链接就是把外部函数库,拷贝到可执行文件中。这样作的好处是,适用范围比较广,不用担忧用户机器缺乏某个库文件;缺点是安装包会 比较大,并且多个应用程序之间,没法共享库文件。动态链接的作法正好相反,外部函数库不进入安装包,只在运行时动态引用。好处是安装包会比较小,多个应用 程序能够共享库文件;缺点是用户必须事先安装好库文件,并且版本和安装位置都必须符合要求,不然就不能正常运行。
现实中,大部分软件采用动态链接,共享库文件。这种动态共享的库文件,Linux平台是后缀名为.so的文件,Windows平台是.dll文件,Mac平台是.dylib文件。
(文章完)