编译器(compiler)是将编程语言的代码转换为其他形式的软件,这种转换操作称为编译(compile)。
实际编译器有C语言的GCC(GNU Compiler Collection)、Java语言的编译器javac等。
编译示例
# 创建C语言程序
$ vim hello.c
#include <stdio.h>
int
main(int args, char **argv)
{
printf("hello, world!\n");
return 0;
}
# 生成名为hello的可执行文件(executable file)
$ gcc hello.c -o hello
# 运行生成的hello命令
$ ./hello
hello world!
疑问:
- 可执行文件是这样的文件呢?
- gcc命令是如何生成可执行文件的呢?
- 可执行文件hello是经过哪些步骤运行起来的呢?
可执行文件
首先从GCC生成的可执行文件说起
现代的Linux上的可执行文件,通常指符合 ELF(Executable and Linking Format) 这种特定形式的文件。例如ls
、cp
这些命令(command)对应的实体文件都是可执行文件。
# Windows上查看可执行文件
$ file hello
hello: PE32 executable (console) Intel 80386, for MS Windows
# Linux上查看可执行文件
$ file hello
ELF文件包含了程序代码以及如何运行该程序的相关信息(元数据)。程序代码就是机器语言(machine language)的列表。机器语言是唯一一种CPU能够直接执行的语言,不同种类的CPU使用不同的机器语言。
GCC将C语言的程序转化为用机器语言描述的程序,将机器语言的程序按照ELF这种特定的文件格式注入文件,得到的就是可执行文件。
编译
gcc
命令式如何将hello.c
转换为可执行文件的呢?
由 hello.c
这样的单个文件来生成可执行文件时,虽然只需要执行一次 gcc
命令,但实际上其内部经历了如下 4 个阶段的处理。
- 预处理
- 狭义的编译
- 汇编
- 链接
上述处理统称为编译,严谨地说“狭义的编译”才是真正意义上的编译,这4个阶段的处理统称为 build
。
预处理
C语言的代码首先由预处理器(preprocessor)对#include
和#define
进行处理。
#include <stdio.h>
具体说来就是读入头文件,将所有的宏展开,这就是预处理(preprocess)。preprocess
是前处理的意思,其实就是在编译前进行处理。
狭义的编译
接着,编译器(compiler)对预处理器的输出进行编译,生成汇编语言(assemble language)的代码。一般来说,汇编语言的代码的文件扩展名为.s
。
汇编语言是由机器语言转换过来的人类较易阅读的文本形式的语言,机器语言是以CPU执行效率为第一要素设计的,用二进制代码表示,每个bit
都有自己的含义,人类很难理解。因此,一般要使用与机器语言直接对应的汇编语言,以方便人类理解。
汇编
然后,汇编语言的代码由汇编器(assembler)转换为机器语言,这个处理过程称为汇编(assemble)。
汇编器的输出称为目标文件(object file),一般来说,目标文件的扩展名为 .o
。
Linux中,目标文件也是ELF文件,既然都是ELF文件,那么究竟是目标文件还是可执行文件呢?这不是区分不了吗?这个不用担心。ELF文件中有用于提示文件种类的标志。
链接
目标文件本身还不能直接使用,无论是直接运行还是作为程序库(library)文件调用都不可以。将目标文件转换为最终可以使用的形式的处理称为链接(link)。使用程序库的情况下,会在这个阶段处理程序库的加载。
总结
build
过程总结:C语言的代码经过预处理、编译、汇编、链接这4个阶段的处理,最终生成可执行文件。
![4933701-eaea83cc285d9c7b.png](https://i-blog.csdnimg.cn/blog_migrate/c1da51b72406fc1395bbc4aebd40fdd6.png)
程序运行环境
![4933701-f114ba06753b4f98.png](https://i-blog.csdnimg.cn/blog_migrate/c9f892e51b92b4f5359fbaee0dcaaf27.png)
现代编程语言的运行过程中,运行环境所起的作用越来越大。
首先,链接的话题并非仅仅出现在build
的过程中。如果使用共享库,那么在开始运行程序时,链接才会发生。最近广泛使用的动态加载(dynamic load),就是一种将所有链接处理放到程序运行时进行的手法。
其次,像Java和C#这种语言的运行环境中都有垃圾回收(GC, Garbage Collection)这一强大的功能,该功能对程序的运行有着很大的影响。
再次,在Java VM等具有代表性的Java的运行环境中,为了提高运行速度,采用了JIT编译器(Just In Time Compiler)。JIT编译器时在程序运行时进行处理,将程序转换为机器语言的编译器。也就是说,Java语言是在运行时进行编译的。
编程语言的运行方式
编译器会对程序进行编译,将其转换为可执行的形式。另外也有不进行编译,直接运行编译语言的方法。解释器(interpreter)就是这样的一个例子。解释器不将程序转换为别的语言,而是直接运行。例如Ruby和Perl的语言处理器就是用解释器来实现的。
运行语言的手段不止一种,例如C语言也可以用解释器来解释执行,Ruby也可以编译成机器语言或Java的二进制码。也就是说,编程语言与其运行方式可以自由搭配。因此,编译器也好,解释器也罢,都是处理并运行编程语言的手段之一,统称为编程语言处理器(programming language processor)。
但是,根据语言的特点,其运行方式有适合、不适合该语言之说。一般来说,有静态类型检查(static type checking)、要求较高可靠性的情况下使用编译的方式;相反,没有静态类型检查、对灵活性的要求高于严密性的情况,则使用解释的方式。
静态类型检查是指在程序开始运行之前,对函数的返回值以及参数的类型进行检查的功能。与之相反,在程序运行过程中随时进行类型检查的方式称为动态类型检查(dynamic type checking)。