1. 什么是编译器
从本质来看,平时提到的“编程语言”其实都是一些助记符,用于向其他人或机器描述我们想要完成的逻辑运算。这些易于人类理解的语言想要被计算机理解并正确执行,就必须被转换成机器码,而完成这一转换过程的软件系统就是编译器。
简言之,编译器其实也是一个计算机程序,它可以读取用一种编程语言(我们称之为source language)编写的代码并将其转换为用另一种语言(称之为target language)实现的程序,转换过程需保证前后两种语言描述的逻辑运算是等价的。这一过程如下图所示。
如果转换后的目标程序是可执行的机器语言程序,则我们可以直接调用之来输出预期结果,如下图所示。
根据上述描述,我们可以把编译器看作一个语言处理器(language processor)。另一种常见的语言处理器是解释器(interpreter),与编译器先生成目标程序然后执行不同,解释器是直接对输入执行源码中指定的操作,如下图所示。
由编译器生成的目标程序的运行效率通常要比解释执行的程序快的多且通常占用更少的资源,不过解释执行的程序通常更易于调试,其开发效率要高的多。这也是在处理器速度越来越快和存储器成本越来越低的今天,解释执行语言越来越流行的一大原因。
值得一提的是Java的语言处理器,它融合了编译和解释执行两种行为:Java源码先被编译为字节码(bytecodes),然后由JVM解释执行。这种折衷处理方式使得Java程序与纯解释执行的语言相比具有一定性能优势;与纯编译型的语言相比又具有开发效率高且跨平台的能力(跨平台是因为JVM屏蔽了底层系统的差异)。这种具有混合行为的编译器如下图所示。
2. 由源码构建可执行程序的过程
从严谨的角度看,由源码生成可执行程序的过程中,除编译器程序外,还需其它几个辅助工具程序,整个构建过程可用下图来说明。
由上图可知,由源码生成可执行程序的过程可以分解为4个步骤,分别是预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。
本文以编译C/C++程序为例来进行说明。
2.1 预处理(预编译)
这个过程主要处理源码中以"#"开头的预编译命令(如"include <xxx.h>"或#define xxx yyy等),主要规则如下:
1) 将所有#define定义的宏做展开以替换原"#define"语句
2) 处理所有的条件预编译指令,如"#if"/"#ifdef"/"#elif"/"#else"/"#endif"
3) 处理"#include"预编译指令,将被包含的文件插入该预编译指令的位置。该过程是递归的,也即若#include指定的文件中include了其它文件,则这些文件都会被插入。
4) 删除所有注释"//"和"/* */"
5) 添加行号和文件名标识,如#2 "hello.c" 2,以便编译时产生用于调试的行号信息或编译过程输出Fatal或Warning时显示行号。
6) 保留所有"#pragma"编译器指令,因为编译器会用到它们
2.2 编译
这个过程就是对预处理完的文件做词法分析(Lexical Analysis)、语法分析(Snytax Analysis)、语义分析(Semantic Analysis)及优化,其输出是相应的汇编代码文件。这个过程是整个程序构建的核心部分,也是最复杂的部分。
2.3 汇编
汇编器将编译器生成的汇编代码转换成机器可执行的指令。汇编过程相对于编译过程来说比较简单,因为每个汇编语句几乎都对应一条机器指令,所以它没有复杂的语法,也没有语义,亦无需做指令优化,汇编器根据汇编指令和机器指令的映射表做翻译即可。
2.4 链接
链接就是把汇编器生成的*.o文件“组装”成最终的可执行程序。从原理上说,链接器的工作是把一些指令对其它符号地址的引用加以修正,也即把各个目标文件间相互引用的部分处理好,使得各个模块间能正确衔接。
从链接时机来看,链接可分为静态编译(或称静态链接)和动态链接两大类。
静态链接主要包括地址和空间分配(Address and Storage Allocation)、符合决议(Symbol Resolution)和重定位(Relocation)等步骤。
动态链接是在节省内存和磁盘空间(每个静态编译的程序都包含一份基础库)及方便程序开发和部署(对于静态编译的应用程序,底层库的改动需要重新编译所有依赖了该库的可执行程序)等需求中应用而生的。其基本思想就是把链接过程推迟到程序运行时再进行,具体的行为与静态编译类似,也包括地址解析、地址重定位等。
#include <stdio.h> int main() { printf("Hello World\n"); return 0; }3.1 gcc的预编译过程
$ gcc -E hello.c -o hello.i上例中,-E表示"Stop after the preprocessing stage; do not run the compiler proper",预编译的输出文件名后缀是.i,这是gcc约定的后缀,表示源码已被预处理,hello.i具体内容本文不赘述,感兴趣的话可以查看下。
$ gcc -S hello.i -o hello.s上例中,-S表示"Stop after the stage of compilation proper; do not assemble",其输出文件名后缀为.s,也是gcc的约定。如果查看hello.s文件,会发现其确实是汇编指令文件。
$ /usr/lib/gcc/i486-linux-gnu/4.9/cc1 hello.c它等价于下面的gcc命令:
$ gcc -S hello.c -o hello.s3.3 gcc的汇编过程
$ gcc -c hello.s -o hello.o也可以直接从源码开始产生目标文件:
$ gcc -c hello.c -o hello.o其中-c选项表示"Compile or assemble the source files, but do not link"。
$ as hello.s -o hello.o3.4 gcc的链接过程
$ ld -static crt1.o crti.o crtbeginT.o hello.o -start-group -lgcc -lgcc_eh -lc -end-group crtend.o crtn.o其中,-static表示ld将以静态链接方式生成可执行文件(再次注意,gcc默认的链接方式是动态链接),该选项会影响其后紧跟的由-l指定的库的搜索方式,即紧跟在-static后的由-l选项指定的库的静态库版本会被搜索并参与链接过程。
【参考资料】
1. <Compilers Principles, Techniques, & Tools>即龙书第1.1节
2. 《程序员的自我修养》第2.1节和第11.2.3节
3.
gcc官网文档
4.
ld man文档
5.
ld.so man文档
6.
Understanding ld-linux.so.2
======================== EOF ==========================
原文地址:http://blog.csdn.net/slvher/article/details/44279523