1. 编译 C 程序
GCC (GNU Compiler Collection) 使用可移植性的C语言写成,能够对自身进行编译,因此很容易被移植到新系统上。编译是指把一个纯文本的源代码“程序”转变为机器码,即用于控制计算机的中央处理单元的 1 和 0 的序列。这种机器码被存放在称为“可执行文件”的文件中,有时也称为二进制文件。程序可以编译自单个源文件或多个源文件,还可以用到系统的库文件和头文件。
假定已经编写好“Hello World”的C语言程序
#include <stdio.h>
int main(void)
{
printf("Hello, world!");
return 0;
}
那么在linux系统 hello.c 程序文件路径下输入如下命令,即可对“Hello World”的纯文本源码进行编译。
$ gcc -Wall hello.c -o hello
这样就把“hello.c”中的源代码编译成机器码并存储在可执行文件“hello”中。用“-o”选项可以指定存储机器码的输出文件,该选项通常是命令上的最后一个参数。如果省略,输出将被保存到默认文件“a.out”中。“-Wall”选项打开所有最常用的编译警告,推荐总是使用该选项。
一个程序可以被分成多个源文件以便于编辑和理解,由其是在大型程序中。我们可以将 hello.c 文件分为 “main.c” “hello_func.c” 和 “hello.h”,其中“hello.h”文件中仅仅只有一行 void hello(const char *name) ,是对hello函数的原型进行声明。“main.c”为主程序
#include "hello.h"
int main(void)
{
hello("world");
return 0;
}
hello函数自身的定义包含在“hello_func.c”文件中
#include <stdio.h>
#include "hello.h"
void hello(const char *name)
{
print("Hello, %s !", name)
}
其中 # include "FILE.h" 和 # include <FILE.h> 这两种 include 声明形式的含义是有差异的,前者是先在当前目录搜索"FILE.h",然后再查看包含系统头文件的目录,而后者则是直接搜索系统目录的头文件,默认情况下不会再当前目录下去查找头文件。使用如下命令即可对多个源码文件进行编译
$ gcc -Wall main.c hello_func.c -o newhello
注意,头文件“hello.h”不需要在命令行上的源文件名列表中指定。“hello.h”源码文件中的# include指示符会指导编译器在合适的时候自动地包含它。程序中的所有部分已经被组合成单个的可执行文件,其象前面由单个源文件生成的可执行文件一样输出同样的结果。
2. 独立地编译文件
如果整个程序代码被存储在单个源文件中,那么对某个函数的任何改变都需要将整个源码文件重新编译以生成一个新的可执行文件,而重新编译大型源码文件可能需要花费大量的时间。当程序被存储在一个个单独的源码文件中时,只有那些被修改过的源码文件才需要重新编译。通过将源文件分开一个个编译,然后再链接在一起,对大型程序进行修改时可以节约大量时间。
在第一阶段,文件被编译但不生成可执行文件,编译的结果被称为对象文件(obj文件),用GCC时有 .o 的后缀名。在第二个阶段,各个对象文件由一个被称为连接器的单独程序合成在一起。连接器把所有的对象文件组合在一起生成单个的可执行文件。
对象文件包含的是机器码,其中任何对在其他文件中的函数(或变量)的内存地址的引用都留着没有被解析。这样就允许在互相之间不直接引用的情况下编译各个源代码文件。连接器在生成可执行文件时会填写这些还缺少的地址。
2.1 从源文件生成对象文件
命令行选项“-c”用于把源码文件编译成对象文件。例如,下面的命令将把源文件“main.c”编译成一个对象文件
$ gcc -Wall -c main.c
该命令会生成一个包含main函数机器码的对象文件“main.o”。它包含一个队外部函数hello的引用,但在这个阶段该对象文件中的对应的内存地址留着没有被解析(它将在后面链接时被填写)。编译源文件“hello_func.c” 的相应命令为:
$ gcc -Wall -c hello_func.c
在这里不需要用 -o 选项来指定输出文件的文件名。当用 -c 来编译时,编译器会自动生成与源文件同名,但用 .o 来代替原来的扩展名的对象文件。由于 “main.c” 和 “hello_func.c”中的 # include 声明,“hello.h”会自动被包括进来,所以在命令行上不需要指定该头文件。
2.2 从对象文件生成可执行文件
生成可执行文件的最后步骤是用gcc把各个对象文件链接在一起并补充缺失的外部函数的地址。要把对象文件链接在一起,只需要把他们简单的列在命令行上即可:
$ gcc main.o hello_func.o -o hello
这是几个很少需要用到 “-Wall” 警告选项的场合之一,因为每个源文件已经被成功的被编译成对象文件了。一旦源文件被编译,链接是一个要么成功要么失败的明确过程(只有在有引用不能解析的情况下才会链接失败)。
2.3 对象文件的链接次序
在类Unix系统上,传统上编译器和链接器搜索外部函数的次序是在命令行上指定的对象文件中从左到右的查找,这意味着包含函数定义的对象文件应当出现在调用这些函数的任何文件之后。
例如 main.o 调用 hello_func.o 函数,因此包含hello函数的文件“hello_func.o”应该被放在“main.o”之后。
$ gcc main.o hello_func.o -o hello
如果次序搞反了,有的编译器或链接器会报错。虽然当前绝大部分编译器和链接器会不管次序搜索所有的对象文件,但由于不是所有的编译器都这么做,最好遵守从左到右排序对象文件的惯例。如果命令行上已经包括了所有必须的对象文件,但你还是碰到意料之外的未定义引用这种问题,那就应该想想这个问题。
2.4 与外部库文件链接
2.4.1 静态库
库是已经编译好并能被链接入程序的对象文件的集合。库通常被存储在扩展名为 .a 的特殊归档文件中,被称为静态库。标准的系统库通常能在 /usr/lib 和 /lib 目录下找到。例如在类Unix系统中, C的数学库常被放在文件 /usr/lib/math.a 中,而该库中的相应的函数的原型声明在头文件 /usr/include/math.h 中。
下面是调用数学库 libm.a 中外部函数 sqrt 的一个例子,假定文件名为“calc.c”:
#include <math.h>
#include <stdio.h>
int main(void)
{
double x = sart(2.0);
printf("The square root of 2.0 is %f", x);
return 0;
}
试图只用该源文件就生成可执行文件会导致在链接阶段编译器报错
$ gcc -Wall calc.c -o calc
由于在没有外部数学库 libm.a 的情况下,对函数 sqrt 的引用不能解决。函数 sqrt 并不定义在源程序中或默认的C库 libc.a 中,而且除非 libm.a 被显示指定,否则编译器不会链接该库文件。为了使得编译器能够把 sqrt 函数链接到主程序 “calc.c”, 需要在命令行上显示地指定该库文件:
$ gcc -Wall calc.c /usr/lib/libm.a -o calc
为了避免在命令行上指定长路径名,编译器提供了短选项 “-l” 用于链接库文件。例如去路径指定的苦命可用下述命令代替:
$ gcc -Wall calc.c -lm -o calc
通常,编译器选项“-lName” 试图链接标准库目录下的文件名为“libName.a” 中的对象文件。另外可以通过命令行和环境变量指定的目录链接,在大型程序中通常会用到很多 -l 选项,来链接像数学库、图像库和网络等。
2.4.2 共享库
虽然上面的例子程序可以被成功编译和链接,但生成的可执行文件要能被载入并运行,还缺少最后一步。如果你试图直接启动该可执行文件,在绝大部分系统上将报错:libgdbm.so.*: cannot open shared object file: No such file or directory。因为 GDBM 软件包提供的共享库在可执行文件运行以前必须先从磁盘上被载入。
外部库通常用两种形式提供:静态库和共享库。静态库就是前面看到过的 .a 文件,当程序与一个静态库链接时改程序用到的外部函数(在静态库包含的对象文件中)的机器码被从库中复制到最终生成的可执行文件中。
处理共享库(动态链接库)用的是一种更高级的链接形式,它会使得可执行文件比较小。共享库使用 .so 后缀名,表示 共享对象 (shared object)。一个与共享库链接的可执行文件仅仅包含它用到的函数相关的一个表格,而不是外部函数所在的对象文件的整个机器码。在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该共享库中复制到内存中,这个过程被称为动态链接(dynamic linking)。
因为一份库可以在多个程序间共享,所以动态链接使得可执行文件更小,也节省了磁盘空间。绝大部分操作系统提供了虚拟内存机制,该机制允许物理内存中的一份共享库被要用到该库的所有运行的程序共用,节省了内存和磁盘空间。此外,共享库使得升级库时不需要重新编译用到它的程序(只要库提供的接口不变就行)。由于上述优点,在绝大部分系统上gcc编译程序时默认链接到共享库。
2.4.3 链接库搜索路径
在命令行上的库的次序遵照像对象文件中的同样的惯例——从左到右搜索,即包含函数定义的库应该出现在任何使用到该函数的源文件和对象文件之后,否则有的编译器会报错。使用库文件,为了得到函数参数和返回值正确类型的声明,必须包括相应的头文件。如果没有函数声明,可能传递错误类型的函数参数,从而导致错误的结果。
在编译用到库的程序时,常碰到的一个问题是include的头文件头错误:FILE.h: No such file or directory。如果头文件不在GCC用到的标准库目录中,就会出现这样的错误。搜索文件的目录列表被称为 include路径,而搜索库的目录列表被称为 搜索路径 或 链接路径。在这些路径中的目录是按次序搜索的,例如 /usr/local/include 中找到的头文件优先于 /usr/include 中的同名文件。类似的, /usr/local/lib 中找到的库优先于 /usr/lib 中的同名库。当有其他库被安装到另外的目录中,为了能够按序找到这些库,需要扩展搜索路径。编译器选项 -I 和 -L 用于把新目录添加到各自的include路径和库搜索路径的头上。
通过shell中的环境可以控制头问价和库的搜索路径。除了可以在每次开始shell会话的相应登录文件“.bash_profile”中自动设置,还可以使用环境变量 C_INCLUDE_PATH(针对C的头文件)和 CPP_INCLUDE_PATH(针对C++的头文件)把其他目录添加到 include 路径中。例如,当编译C程序时,下面的命令会把 /opt/gdbm/include 添加到include路径中。
$ C_INCLUDE_PATH=/opt/gdbm/include
$ export C_INCLUDE_PATH
该目录将在命令行上用选项 -I 指定的任何目录之后,但在标准默认目录 /usr/local/include 和 /usr/include 之前被搜索。 Shell命令 export 是必要的,以便shell以外的程序也能获得该环境变量。
类似的,使用环境变量 LIBRARY_PATH 可以把另外的目录添加到链接路径中去。例如下面的命令会把 /opt/gdbm/lib 添加到链接路径中。该目录将在命令行上用选项 -L 指定的任何目录之后,但在标准默认目录 /usr/local/lib 和 /usr/lib 之前被搜索。
$ LIBRARY_PATH=/opt/gdbm/lib
$ export LIBRARY_PATH
环境变量设置好之后,默认路径就包含了环境便令 C_INCLUDE_PATH 和 LIBRARY_PATH 中指定的目录。 遵循标准Unix搜索路径的规范,搜索目录可以在环境变量中用冒号分割的列表形式一起指定:DIR1:DIR2:DIR:3:...,这些目录被依次从左到右搜索。单个点 . 可以用来指示当前目录。在命令行上可以重复使用 -I 和 -L 选项来指定多个搜索路径的目录。在日常的使用情况中,通常用 -I 和 -L 选项把目录添加到搜索路径。当环境变量和命令行选项被同时使用时,编译器按照下面的次序搜索目录:
- 从左到右搜索由命令行
-I和-L指定的目录- 由环境变量指定的目录
- 默认的系统目录