编译系统
一个hello.c程序:1
2
3
4
5
6
7
int main()
{
printf("hello, world\n");
return 0;
}
在Unix系统上,由编译器把源文件转换为目标文件。
1 | gcc -o hello hello.c |
对于上述gcc命令,不跟任何的选项的话,会默认执行预处理、编译、汇编、链接这整个过程;如果程序没有错,就会得到一个可执行文件,默认为a.out。
- -E选项:提示编译器执行完预处理就停下来,后边的编译、汇编、链接就先不执行了。
- -S选项:提示编译器执行完编译就停下来,不去执行汇编和链接了。
- -c选项:提示编译器执行完汇编就停下来。
以上三种选项限定了编译器执行操作的停止时间,而不是单独的将某一步拎出来执行。
-o选项:输出名字的参数,指定输出的文件名;不用-o则一般会在当前文件夹下生成默认的a.out文件作为可执行程序
-O选项:对程序进行优化编译、链接,采用这个选项,整个源代码会在编译、链接过程中进行优化处理,这样产生的可执行文件的执行效率可以提高,但是,编译、链接的速度就相应地要慢一些。比如氧气优化-O2。(吸口氧,呼呼)
编译的整个过程如下:
- 预处理阶段(hello.c -> hello.i):预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。处理包括:#include,#define,条件编译指令(#if,#ifdef…),删除注释等内容;添加行号和文件标识名;保留所有的#pragma编译指令(留待编译器使用)
- 编译阶段(hello.i -> hello.s):翻译成汇编文件;
- 汇编阶段(hello.s -> hello.o):将汇编文件翻译成可重定位目标文件;汇编器将 hello.s 翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件 hello.o 中。 hello.o 文件是一个二进制文件,它的字节编码是机器语言指令而不是字符。
- 链接阶段(hello.o + 其他.o文件 -> hello):在本例中调用了 printf() 函数,因此需要使用链接器将重定位目标文件 hello.o 和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件(简称可执行文件,可以被加载到内存中,由系统执行)。
库
- 库是写好的,现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库。
- 本质上来讲,库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。
静态链接
静态链接器以一组可重定位目标文件为输入,生成一个完全链接的可执行目标文件作为输出;在链接阶段,要把所有需要的函数的二进制代码都包含到可执行文件中去。
优点:
- 在可执行程序中具备了执行程序所需要的所有东西,执行时运行速度快。
缺点:
- 浪费空间:每个可执行程序对需要的目标文件都有一份副本,目标文件可能会在内存中有多个副本。
- 更新困难:每当库函数的代码被修改,就需要重新进行编译链接形成可执行程序。
链接器主要完成以下两个任务:
- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
- 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

动态链接
为了解决静态链接中:空间浪费、更新困难两大问题。动态链接把程序按照模块拆分成各个相对独立部分,在程序运行时才将它们链接在一起形成一个完整的程序,而不是像静态链接一样把所有程序模块都链接成一个单独的可执行文件。
特点:
- 在给定的文件系统中一个动态库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
- 在内存中,一个共享库的已编译程序的机器代码的一个副本可以被不同的正在运行的进程共享。
- Linux 系统:.so 后缀;
- Windows 系统:.DLL 后缀;
优点:
- 节省空间:因为即使所有程序都依赖都一个库,但是该库在内存中只存在一个副本。
- 更新方便:因为更新时只需要替换原来的目标文件,而无需将所有的程序再重新链接一遍。当程序下一次运行时,新版本的目标文件会自动加载到内存中并且链接起来。
缺点:
- 由于是运行时加载,可能会影响程序的前期执行性能。

目标文件
- 可执行目标文件:可以直接在内存中执行;
- 可重定位目标文件:可与其它可重定位目标文件在链接阶段合并,创建一个可执行目标文件;
- 共享目标文件:这是一种特殊的可重定位目标文件,可以在运行时被动态加载进内存并链接;