unix下c编译系统

编译过程概述
了解一些编译知识的读者都知道,所谓编译,就是在编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件感谢格式的要求链接生成可执行程序。
UNIX环境下的C编译系统所遵循的也是这么一个一般的过程。值得注意的是这个过程并不是有某个单个程序完成的,而是有多个分别完成某一方面工作的程序组合完成的。这一设计思想同我们最初提到的UNIX系统软件功能专一的特点是相符的。
归纳起来,可以将UNIX环境下C编译系统的工作过程下图所示。
C源程序头文件-->预编译处理(cpp)-->编译程序本身-->优化程序-->汇编程序-->链接程序-->可执行文件
一般我们用cc命令来完成对源程序的编译工作。此cc命令并不是一个二进制的可执行程序,而是一个shell命令文件。它的工作就是依次调用我们上面所列出的各个完成某部分工作的具体程序,将指定的c源程序转换成可执行的代码。
1.编译预处理
在此阶段,预编译程序读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。C语言中的伪指令主要包括以下四个方面
(1)宏定义指令,如# define Name TokenString,#undef等。对于前一个伪指令,预编译所要作得的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。
(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉
(3)头文件包含指令,如#include "FileName"或者#include 等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。
包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。关于预编译程序如何搜索头文件,后面我们将要介绍。
(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。
从上述过程我们可以看到,预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。
2.编译阶段
经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。预编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
这种等价的中间代码表示或汇编代码由于是编译程序按照一种比较固定的、相对而言比较机械的方法转换得到的。自然我们不能指望它具有比较高的效率。但一般情况下,我们在这方面的要求也不是很高,这样得到的代码也就基本上可以了。但特殊情况下还要对此种程序进行优化,以期产生效率比较高的代码。
3.优化阶段
优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。
对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。
后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。
经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。
4.汇编过程
汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
目标文件由段组成。通常一个目标文件中至少有两个段:
代码段   该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。 
数据段   主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。 
UNIX环境下主要有三种类型的目标文件:
(1)可重定位文件   其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。
(2)共享的目标文件   这种文件存放了适合于在两种上下文里链接的代码和数据。第一种事链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。
(3)可执行文件   它包含了一个可以被操作系统创建一个进程来执行之的文件。
汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。
5.链接程序
由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。
根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:
(1)静态链接 在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。
(2)动态链接 在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。
对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。
经过上述五个过程,C源程序就最终被转换成可执行文件了。缺省情况下这个可执行文件的名字被命名为a.out。但并不是任何一个C源程序都要严格地按照上述过程走到底。某些编译过程因其特殊需要将停止在某个位置。例如,我们可能只想要得到某个C源程序的目标文件,这时就用不着进行链接。
对一个C源程序进行哪些阶段的处理,以及各阶段具体要经过什么处理是由在CC命令行中加入相应的选项而控制的。关于cc命令的一些用法,我们将在下一节进行详细的讨论。

UNIX系统开发-CC命令 
cc的基本用法
在UNIX系统中,实现C源程序到可执行文件的这一转换过程的工具是cc。在大多数系统中cc实际上是一个shell命令文件。有些系统中的C编译程序可能并不叫cc而是其它的一个什么名称,如Sun工作站上常用的gcc等等。但这些都无关紧要。大多数系统中C编译命令的用法基本上都是类似的。我们这里介绍的将以SVR4上的C编译系统为基础。
cc基本用法
一般我们只需要将C源程序的名字写在CC命令行中,cc即可对这些源文件(.c文件)进行编译。如果这些源文件中都没有main()函数的定义,那么cc将只能生成与各源文件相对应的目标文件(.o文件)。如果某个源文件中有关于main()函数的定义,则将把所有目标文件链接起来生成相应的可执行文件。缺省的情况下这个可执行文件的名字将是a.out。
例如,假定myprog.c是一个包含有main()函数定义的C语言程序文件,其中代码如下:
/*********************************************
* An example source code with errors   *
* Name:myprog.c    *
********************************************* /
#include
#include
# define TESTOK   1
int TestInput(char * ValuInput)
{while (* ValueInput)
if (! isdigit(* ValueInput )) return (! TESTOK);
else    ValueInput + +;
return ((100/atoi(ValueInput))? TESTOK:! TESTOK);
}
void
main(int argc,char * argv[])
{int i;
for (i=1;i if(TestInput (argv[i]) = =TESTOK)
printf("The %dth value '%s' \tis OK! \n",i,argv[i]);
else
printf("The %dth value '%s' \tis BAD! \n" ,i,argv[i]);
}
对于此程序中的错误(设计错误)我们暂不理会。下一章我们介绍程序调试时再回过头来看看如何排除这个错误。
我们看到。在这个源程序文件中,定义了两个函数:TestInput()和main(),定义了一个宏TESTOK,同时包含了两个标准的头文件。为了把这个C程序转换成可执行文件,在shell提示符下输入:
$cc myprog.c
在程序中没有任何语法错误的情况下,cc将在当前目录下生成一个名为a.out的可执行文件,如:
$ cc myproc.c
$ ls -l
-rwx------ 1 yxz users 5812 Aug 31 15:32 a.out
-rw------- 1 yxz users 716 Aug 31 15:27 myproc.c
$
还可以看到这里a.out是一个可执行文件。当然这个程序由于在设计上有些失误,我们现在还不能马上就带参数运行。但不带参数运行还是可以的。只不过此时该程序什么都没有干,如:
$ a.out
$
在程序中我们通过main函数的两个参数argc和argv而使程序能够引用shell命令行参数;这是UNIX环境下一种常用的编程技术。
在生成了a.out文件之后,我们自然可用mv命令将其修改为某个合适的名称。但更简单的方法是在cc命令行中加上-o选项,使cc直接将可执行文件写入到指定的文件中而不生成a.out文件,如:
$ cc -o myprog myprog.c
$ ls -l myprog
total 14
-rwx------ 1 yxz users   5812 Aug 31 15:34 myprog
-rw------- 1 yxz users    716 Aug 31 15:27 myprog.c
$
我们看到,myprog这个文件除了文件名及修改时间同a.out不一样外,其他属性同a.out都是一摸一样的。这也说明了两者的等价性。
在某个程序的源代码被存放到多个不同文件中的情况下,我们只需要在命令行中一一指定这多个C文件即可。例如,我们可以将上述myprog.c拆分为两个C文件和一个头文件(.h)如下:
myprog.h
# include
# inclued
# define TESTOK 1
myprog.c
#include "myprog.h"
void
main (int argc,char * argv[])
{int i;
for (i=1;i if (TestInput(argv[i])= = TESTOL)
printf("The %dth value '%s' \tis ok! \n",argv[1]);
else
printf("The %dth value ' %s' \tis BAD! \n",iargv[i]);
}
myfunc.c
#include "myprog.h"
int
TestInput(char * ValueInput)
{while (* ValueInput)
if (!isdigit(*ValueInput) return (! TESTOK);
else   ValueInput + +
return ((100/atoi(ValueInput))? TESTOK:! TESTOK);
这时要再编译此程序时可输入如下命令:
$ cc -o myprog myprog.c myfunc.c
在这个命令行中如果不指定myfunc.c,此时由于在myprog.c中所调用的TestInput()这个函数不是任何标准的库函数,在链接时链接程序将找不到此符号的定义,故链接过程将以失败而告终,此时cc将给出如下的错误信息:
Undefine   first referenced
symbol   in file
TestInput    myprog.o
id: myprog:fatal error: Symbol referencing errors.No output written to myprog
$
而可执行文件myprog也无法生成。但编译却会生成myprog.c的目标代码(在某个文件固有语法错误而无法正确被编译的情况下(此时为编译过程出错),cc将生成其他无语法错误的源文件的目标文件,但不进行链接)。如下:
$ ls -l
total 8
rw-r--r-- 1 yxz user 454 Sep 1 09:27 myfunc.c
rw-r--r-- 1 yxz user 479 Sep 1 09:28 myprog.c
rw-r--r-- 1 yxz user 298 Sep 1 09:27 myprog.h
rw-r--r-- 1 yxz user 924 Sep 1 09:28 myfunc.o
此时我们可以使用如下命令行得到可执行文件:
$ cc -o myprog myprog.o myfunc.c
这里我们看到,cc命令行中的文件参数可以不全是.c文件,目标文件(.o)文件以后编译过程中所得到的其他文件,如预编译后文件(.i文件),编译后的汇编程序(.s文件)等都可作为文件参数。在了解了UNIX C编译系统的工作过程之后,理解这一点是不困难的。因为编译系统只需要对各种不同类型的文件进行有关的处理就可以了。
关于cc命令最基本的用法我们就介绍这么多,其它更高级的用法可参考下面几节的讨论。
常用选项
cc命令还提供了其他许多有用的命令行选项。借助于这些选项我们可以对编译过程进行进一步的控制,如使cc只完成某些阶段的编译工作,指定对头文件的搜索目录,指定对代码进行优化,指定在代码中加入一些供调试程序所用的信息,等等。下面我们分别讨论这些问题。
1.仅进行编译预处理
在命令行中加上-P选项可以使cc仅完成对.c文件的预处理工作,而后面的编译,汇编,优化,链接则都不作,例如:
$ cc -P myprog.c
此时编译系统将在当前目录下生成一个名为myprog.i的文件。这个文件中包含有对myprog.c中的伪指令进行处理后的代码及myprog.c中原有的代码。
在某些情况下,.i文件可能对于程序排错有一定的用处。对于下面的代码段,编译程序可能会报告j无定义的错误:
for (i=0;i<10;i++)
{ /*declare avariable j:
int j;/*This is a temporary variable */
j=i*i;
.
.
.
}
预编译处理后,缺省情况下,预处理程序将把源程序中的注释删除,这样改段代码将变成:
for (i=0;i<10;i++)
{
j=i*i;
.
.
.
}
这样一来我们将能够比较快地发现程序中的错误。利用.i文件,我们还能够对条件编译和宏扩展后的结果进行检查。
在cc命令行中加上-C选项可以在预编译后的文件中保留源文件中的注释。
2.仅生成汇编语言代码
在cc命令行中加上-S 选项,可以使cc只调用预处理程序和编译程序以生成与源程序相应的汇编代码。与每一个C源文件相应的汇编程序被放到相应的.s文件中。例如:
$ cc -S myprog.c myfunc.c
$ ls *.s
myfunc.s myprog.s
$
这种汇编语言代码是同机器具体相关的。有些情况下我们可能需要用汇编语言进行编程,这时可以先用C语言编写此程序,再编译得到汇编程序,然后手工对此汇编程序修修改改,估计基本上就能满足要求。由于用汇编语言进行编程是一件效率比较低的工作,用此种方法可以预期将获得比较高的效率。
3.仅生成目标文件
如果只想生成源文件的目标代码而不对这些代码进行链接,可以在cc命令行中加上-C选项。此时编译系将只生成与各源文件相对应的.o文件(目标文件)。如:
$ cc -c myprog.c myfunc.s
将生成同myprog.c和myfunc.s相对应的目标文件myprog.o和myfunc.o。
4.头文件搜索路径
当用户在C源程序中用# include指令包含了某个头文件时,根据文件名指定方法的不同,C编译系统将在不同的目录下去寻找指定的头文件:
在用尖括号(<>)指定头文件名时,预处理程序将在系统中存放头文件的标准位置(通常是/usr/include目录)寻找指定的头文件。
在用双引号(“”)指定投文件名时,预处理程序将先在包含此头文件的C源程序所在的目录中(一般为当前目录),去查找该头文件。找不到时再到标准目录下去查找。
在对于那些头文件既不在标准位置,又不在与C源程序同一目录时的情况怎么办呢?为此,CC命令提供了-I(Include)选项,以供用户自己指定头文件所在地目录。例如,对于myfunc.c和myprog.c中所包含的头文件myprog.h,我们假定其后来被放在目录$HOME/include目录下,而这两个C文件则被放在$HOME/cfile目录下。此时在$HOME/cfile目录下对这两个C文件进行编译时,可使用如下命令:
$ cc -I #HOME/include myprog.c myfunc.c
此时对于这两个C文件中的#include "myprog.h",预处理程序将先在$HOME/cfile目录下,然后再$HOME/include目录下,最后再系统标准位置查找myprog.h。-I选项也能改变那些用尖括号(<>)指定的头文件的搜索顺序,此时预编译程序将首先在-I指定的目录下,然后才在标准位置搜索。
-I选项可以多次重复使用。这样我们将能够指定多个非标准的头文件目录。
5.在目标文件中加入调试用的信息
除非是那种特别简单的程序,一般大多数程序都会有这样或那样的问题。为了能够使用UNIX的符号调试程序(sdb,下一章回具体介绍)对程序进行调试,必须在目标代码中加入一些有关的程序变量和语句信息,以便sdb能够跟踪函数调用、显示变量的值以及设置断点,等等。
在cc命令行中加入-g选项将能够实现上述要求,如:
$ cc -g -o myprog myprog.c myfunc.c
这样生成的myprog就可以用sdb进行调试了。
6.优化处理
优化的含义前面我们已经讲过,这里不想再重复。我们要说明的是在程序的调试过程中用不着进行优化处理。优化只应对最终提交的可执行程序进行。
在CC命令行中加上-O选项可以使编译系统对代码进行优化:
$ cc -O -o myprog myprog.c myfunc.c
优化对于不同的程序效果可能是不同的。有些程序优化不优化都不会有什么区别。在有些系统上(如Sun OS),对程序的优化可以分成不同的级别(一般是1至4级)。第一级优化是仅在汇编级上优化,这是大多数系统都会做得。第二级优化是全局优化,如循环优化、公共子表达式的消除、复写传播及自动寄存器的分配。第三级上的优化再加上对外部变量的用法和定义的优化。第四级优化则在第三级基础上对指针赋值得效果进行跟踪。程序员可在-O后面加上一个数字(1,2,3,4)来表示所希望的优化级别。
在cc命令行中还可以使用其他的许多选项,下一节我们将介绍同链接有关的一些选项,其他选项的使用请参阅联机帮助。或者使用手册。

UNIX系统开发-链接处理(1) 
我们已经知道链接实际上是指将在一个模块中引用的符号与它在另一个模块中的定义相链接的过程。并且我们还知道链接分为动态链接和静态链接两种方式。不论是对哪一种方式。链接程序都将搜索程序中的每一个模块,包括所用到的每一个库文件,以在这些文件中寻找在某个模块中没有定义的外部符号的定义。如果没有找到某个被引用的符号的定义,链接程序将报告错误。此时可执行文件的创建将会失败。
对于静态链接和动态链接,其区别主要在于搜索到某个符号的定义后链接程序所做的不同工作:
对静态链接,链接程序将把静态链接库(档案库)中哪些被用户程序所引用的符号定义的目标代码,拷贝到最终生成的可执行文件中。这种情况下,程序中的外部符号引用同其定义的链接是在可执行文件被建立的时候完成的。 
对动态链接,共享对象(动态链接库)中的内容在运行时被映射到用户进程的虚地址空间。链接程序所作的仅仅是在最终生成的可执行文件中记录下到哪里去找外部符号定义的目标代码。这种情况下符号的外部引用与其定义的链接是在程序运行时完成的。 
在这一节中,我们将详细地讨论链接过程。如编译系统的一些缺省设置、用户如何生成自己的动态库或静态库、如何在程序中链接这些库文件,以及动态链接库是如何实现的,等等问题。在掌握了这些内容之后,读者将能够高效地组织自己的源文件,提高开发的效率和程序的可维护性。
缺省设置
前面一节中,我们使用:
$ cc -o myprg myprog.c myfunc.c
命令来生成可执行文件。此时cc将生成同每一个C源程序对应的目标文件,并把它们彼此链接,以生成一个可执行的程序文件。对于所生成的每一个目标文件,我们称之为可重定位的目标文件,因为这些目标文件中含有尚未同其定义相链接的符号引用,也即尚没有在内存中分配地址。
但我们可以注意到,myprog.c中所调用的printf()和myfunc.c中所调用的isdigit(),这两个函数是我们在自己的程序中所没有定义的。这两个函数是标准C库所提供的。缺省情况下链接程序将自动地道标准C库中去查找哪些被用户程序所调用的函数的定义。
标准的C函数库有动态和静态两个不同的版本,其文件名分别是libc.so和libc.a,分别用于动态链接和静态链接。缺省情况下,链接程序将对标准C库函数调用进行动态链接(使用libc.so),也即所调用的函数将在运行时与程序相链接。但有些标准函数由于设计上的一些遗漏,在libc.so中并没有其定义。对这些函数链接程序将使用libc.a进行静态链接,也即将这些函数的代码拷贝至可执行文件中。但对哪些既可以静态链接,又可以进行动态链接的符号引用,程序员可以自主选择到底使用哪种链接方式。后面我们将介绍如何完成这一点。
标准C函数库中所包含的函数有如下几类:
标准I/O函数,如fopen,fread,fwrite,fclose等; 
字符串操作函数,如strcat,strcmp,strcpy等; 
对八位字符编码的整数值分类的函数,如isalpha,isupper,islower,isdigit等; 
字符、整数或字符串转换函数,如atoi,atol,strtoul,atof等; 
用库函数形式实现的系统调用,如open,read,write,close等等。 
关于其他未列出的函数及据说明读者可参考有关的手册。
cc自动对标准C库的动态链接是下述约定为基础的:
根据约定,共享对象或动态链接库的名称,前缀是lib而后缀是.so;档案库或者静态链接库,前缀为lib而后缀是.a。因此标准C库的共享版本的名称将是libc.so;而静态版本的名称则是libc.a。
cc命令的-l选项能够识别上述约定。也就是说:
$ cc ... -lx
将使链接程序搜索动态库libx.so或静态库libx.a。缺省情况下,cc将自动地把-lc选项传给链接程序。因此,从实际看-l选项是一个链接程序选项。
缺省情况下,链接程序将优先查找同一目录下的动态库版本libx.so,不成功时才选择静态库libx.a。
缺省情况下,链接程序将在系统的标准位置(/usr/ccs/lib和/usr/lib目录下)按上述顺序搜索所需的库。由编译系统提供的标准库一般被放在/usr/ccs/lib目录下。 
在这些约定的基础上,我们可以更明确地讲,缺省的cc命令行将引导链接程序搜索/usr/ccs/lib/libc.so,而不是它所对应的静态库。后面我们将要讲到如何将用户的程序同某个库的静态版本相链接(如同libc.a相链接二不是同libc.so相链接),以及用户如何建立起自己的静态库或动态库,并使程序与这些库相链接。当然,如果缺省的cc命令行能够满足编译要求的话,那么就用不着进行多余的链接处理了。
标准库函数的链接
libc.so只是一个目标文件,其中包含有标准C库中每一个函数的代码。当某个程序调用该库中的某个函数并且动态地将自己的程序相链接时,libc.so的全部内容将被映射到运行时与该程序相对应的进程虚地址空间中。
对于静态库(档案库)则不是这么回事,每一个函数或者一组相关的函数代码被保存到它们自己的目标文件中。然后这些目标文件被收集在一个档案库中。但程序员在命令行中指定对标准C库进行静态链接的时候,链接程序将在档案库中搜索被调用函数的代码并将其拷贝到最终的可执行文件中。这里我们看到使用静态链接时最终可执行文件中只包含有哪些所需的代码。
那么如何使链接程序改变缺省的动态链接方式而进行静态链接呢?方法是在cc命令行中加上-dn选项,如下所示:
$ cc -o myprog -dn myprog.c myfunc.c
这样对于myprog.c和myfunc.c中的标准C库函数调用,链接程序将在libc.a中去搜索目标代码并且将其拷贝到最终可执行的文件中。
在程序比较复杂的情况下,一个程序可能就不是仅仅调用了标准C库中的函数了。例如,对于用到了数学运算sin(),cos()这类函数的程序,它就可能需要同数学函数库链接。由于编译系统只提供了libc和libdl(对动态链接进行控制的函数调用集合)的动态版本,因此,除非是用户自己在标准位置安装了数学函数库的动态版本libm.so,那么链接程序将在标准位置查找libm.a库函数。当然这需要在cc命令行中加上一个-l选项,如下所示:
$ cc file.c file2.c   -lm
注意在上述命令行中我们并没有指定-dn选项。这样链接程序对每一个库函数调用,将仍然试图去进行动态链接。例如,对于file1.c和file2.c中的标准C库函数调用,链接程序将在libc.so中搜索其定义。后面我们将看到对每一个库,如何指定是同静态版本相链还是同动态版本相链。
另外还要注意的是链接程序对于静态库的搜索仅仅是为了解决以前发现的、尚未定义的外部引用。因此,-l选项在命令行中的位置很重要。例如:
$ cc -dn file1.c -lm file2.c
这样链接程序对libm.a的搜索只是为了解决file1.c中对数学函数的调用。因此如果在file2.c中调用了某个数学函数,链接程序将无法找到该函数的定义,因而链接也将失败。这种情况下,除非是特别清楚每个C文件中都调用了哪些函数,否则还是将-l选项放在命令行的最后比较好,并且-l选项可以出现多次,以指定多个不同的库文件。
下一部分:静态库及动态库的建立 ......

UNIX系统开发-静态库和动态库的建立
UNIX系统及各种软件包为开发人员提供了大量的库文件。但一般情况下这些库文件还不能足以满足用户的所有需求。开发人员大多会根据他们自己的开发、研究要求编写出许多函数。对于这些函数,如果都用在命令行中指定源文件的方法同调用它们的的程序链接起来,虽然也是可以的,但也有一些缺点:
对每一个调用了这些函数的程序,在编译时都需要将这些函数的代码分别重新编译,这实际是对计算时间的大量浪费。 
一个文件中通常都不止包含有一个函数的定义。使用上述编译方法将使得大量无关函数的代码被拷贝到最终的可执行文件中,无端加大对存储资源的占用量,使运行时装载变慢。 
维护上的诸多不便。由于一个源文件供多个程序使用,当由于某个程序的需要面对此源文件进行了某种修改时将引起诸多意想不到的麻烦。等等。 
所有这些原因,使得我们想到能否将自己编写的函数也作成库文件供多个程序调用,就如同那些标准的库函数那样。事实上在UNIX系统中提供了这方面的工具。借助于这些工具我们不光是能将函数放到静态库,而且能够将其作成动态库。
下面来看看如何生成静态库。
我们知道静态库也称档案库,在此档案文件中实际上是收集了一系列的目标文件。这些目标文件就是由CC对函数的源代码编译生成的。因此,静态库的生成方法实际上可分成两步:
1.将各函数代码所在地源文件编译成目标文件。例如,对于前面的myfunc.c,可以用如下命令将其编译成目标文件:
$ cc -c myfunc.c
当然在有多个源文件时,只需在cc命令行中将其分别列上就可以了。
经此一步我们将能够得到各源文件的目标文件。对上例将得到myfunc.o。
2.将各目标文件收集起来放到一个静态库文件中。这主要借助于ar命令完成,如:
$ ar r $HOME/lib/libtest.a myfunc.o
ar:creating /home/yxz/libtest.a
$
这里-o $HOME/lib/libtest.a是生成的静态库的全路径名。其中我们假定$HOME/lib目录已经存在。注意对静态库的命名要遵循libx.a的原则,便于以后能够在cc命令行中用-l选项指定之。后面的myfunc.o则是待收集到档案库中的目标文件名。有多个目标文件时只需分别列上即可。
这生成了libtest.a档案库之后,再编译myprog.c时,便可使用下面的办法:
$ cc -L $HOME/lib -o myprog myprog.c -ltest
这里-L选项指示链接程序在$HOME/lib目录下去搜索有关的库文件(当然它还会自动搜索标准位置)。下一节我们对此将进行更详细的说明。最后的-ltest选项指示链接程序在libtest.so或libtest.a中取搜索myprog.c中对TestInput()的引用。当然由于我们并没有生成libtest.so文件,故链接程序将只能搜索libtest.a。另外,由于我们在命令行中并未指定-dn选项,故对于缺省的-lc选项,链接程序将搜索libc.so而不是libc.a。
静态链接库的生成虽然比较简单,但此种链接方式也正因为其简单而在某些情况下达不到比较高的效率。同动态链接方式相比,这种链接方式具有如下一些明显的不足:
由于在生成的可执行文件中包含有函数代码的单独拷贝,这些重复的代码会消耗掉大量的磁盘空间。 
运行时各进程单独在自己的地址空间中装入它所调用的每一个函数的代码,这样在有多个进程都调用了同一个函数时,内存中将会有此函数代码的多个拷贝,无端地占用比较多的内存。 
由于对符号引用的确定是在编译链接时完成的。故以后对函数的定义进行更新的时候,必须重新链接调用这些函数的程序。 
在虚拟存储管理方案的基础上,实现的动态链接方式克服了静态链接上述不足,而使整个系统能够获得比较高的效率。因此缺省情况下,链接程序只要有可能就要试图进行动态链接(找库函数的动态版本)。
进行动态链接的核心问题是要生成动态链接库(共享对象)。下面我们介绍如何生成动态链接库,然后讨论建立动态链接库的一些原则。
建立动态链接库并不需要用到其他的工具,借助于cc命令即可完成。此时需在命令行中加上-K PIC和-G这两个选项,如下我们可以建立libtest的动态版本:
$ cc -K PIC -G -o $ HOME/lib/libtest.so myfunc.c
这里-o $ HOME/lib/libtest.so指定待生成的动态链接库的全路径名。同静态库一样,动态库的命令应遵循libx.so约定。-G选项指示cc按动态链接库的格式将其各文件的目标代码组织起来。
-K PIC 选项是生成动态链接库所必须的。我们已经知道,动态链接时建立在页式虚拟管理方式的基础上的。在这种需存管理方式下,进程之间内存的共享是以页为单位的。只要运行时内存页不改变,它们就能够被共享。但是这些共享的页在不同的进程中。可能会具有不同的虚地址。因此这些代码的物理地址只是在运行时才能得到。(这个过程称为地址的重定位。)如果在重定位某个共享对象的引用时,某个进程写了一个共享页,此时操作系统将为该进程生成改页的一个专用拷贝。这种情况下,页面共享的好处就没有了。因此程序必须尽可能减少对页面的修改的次数,减少此修改次数的方法就是使用地址浮动的代码。
地址可浮动的代码将能够被装入到进程地址空间的任何地方。由于此种代码不依赖于绝对地址,故这些代码将在使用它的每一个进程中在不同的虚地址正确的运行,而且在运行过程中是没有页面修改的。-K PIC选项的作用,就是指示编译系统生成地址可浮动的目标文件。此时在目标文件中,可重定位的引用将从所在地正文段被移动到数据段的表中。
在生成了动态库之后,就可以在cc命令行中使用它了。如:
$ cc -L $ HOME/lib -o myprog myprog.c -l test
这时虽然在$HOME/lib目录下也具有test库的静态版本libtest.a,但链接程序将优先搜索libtest.so。
在搞清楚如何生成动态库之后,下面我们来看一看建立动态库的一些原则。所有这些原则都是为了性能的改善而提出来的。
性能的改善主要涉及两方面的问题。其一是尽量减少动态链接库的数据段。我们知道,所谓共享,共享的只是代码。而对于动态库的数据段却是无法供多个进程共享的。系统将为每个共享该库文件的进程都分配该库整个数据段的一份内存拷贝。因此,要想真正实现动态链接库少占用内存的目的,必须尽可能地减小共享对象的数据段大小。归纳起来,大致有以下四种方法:
(1)尽量使用自动(堆栈)变量。如果自动变量行的通,就不要使用全局变量或者静态变量。
(2)尽量使用函数接口而少使用全局变量进行参数的传递。这样还能够提高程序的可维护性。
(3)将那些大量使用全局变量的函数排除在动态链接库之外。对于此类函数,将其放到静态链接库中比较合适。
(4)动态链接库应是自包含的。也就是说在生成某个动态链接库时,对其他库的函数调用不要使用动态链接而应使用静态链接。因为在此种情况下使用动态链接时,调用该动态库中函数的进程将除了得到该动态库的数据段的拷贝以外,还将得到该数据库说链接的其他动态库的数据段拷贝。这实际上是得不偿失的了。
动态链接性能的改善所涉及到的第二个问题,尽量减少内存页面的交换动作。虽然使用共享库的进程并不会写共享页面,但它们仍然可能引起页面失效而导致动态链接的性能降低。对这个问题可以使用以下两种方法加以解决:
(1)改进对符号引用的定位性 这包括两方面的含义。其一在共享库中排除哪些很少用到的、库本身不依赖于它们的那些函数定义。如果共享库中装有许多不相关的函数,而且只是一些不相关的进程偶尔调用这些函数,那么定位性将降低,页面的交换将会变得频繁。其二是要尽可能把相关的函数组合在一起,放到同一个页面中以改进引用的定位性。
例如:假定func1()调用了func2()和func3(),并且这三个函数的代码被放到同一页中。那么在执行func1()的代码时,func2()和func3()地代码也将被同时装入内存。而假如func2()的代码与func1()的代码不在同一页上。那么在执行func1()的过程中可能还需要去调页,从而系统的效率将受到影响。
(2)调整页面安排 这主要是指要对共享库的目标文件进行整理,使那些频繁使用的函数的代码不要越过页边界被分到不同的页上去了。完成这个工作首先要搞清楚系统内存页面的大小。然后用nm命令显示出目标文件中各符号的偏移值。然后据此可对各函数的位置进行调整,使那些必须跨越页边界的是那些使用不太频繁的函数,以尽可能提高页面的命中率。
上述对动态链接性能改善的分析实际上也是动态链接的一些不足。因此并不是说在什么情况下都应使用动态链接而排除静态链接。理想情况下,对同一个库应提供其静态和动态两个不同的版本。这主要是由于一些用户可能找不到适合自己应用程序的动态库,另一方面有些UNIX系统并不支持共享对象。从上面我们对静态库和动态库建立方法的介绍,知道完成这一点并不是什么特别费力的事。

UNIX系统开发-链接程序搜索目录
上一节我们提到,当待于程序链接的库文件不在系统的标准位置时,需要在cc命令行中加上-L选项以指定非标准的库文件所在的目录。链接程序将首先在-L选项指定的各目录中搜索-l选项指定的库文件。在查找这些库文件时链接程序,首先看有没有指定库的动态版本,有的话则进行动态链接;否则它将用指定库的静态版本进行静态链接。
另外,前面还介绍过-dn选项,该选项使得链接程序取消缺省的动态链接方式而用静态链接。现在自然而然地产生一个问题;如何让链接程序对某些库进行静态链接而对另外一些库使用动态链接?
解决这个问题的第一种方法就是将静态库同动态库分别存放到不同的目录下。然后再cc命令行中用-L选项指定待链接的静态库所在地目录。例如,假定我们将上一节生成的静态库libtest.a放到目录$HOME/archive目录下,此时为了使myprog.c同此静态库相链接,可使用如下命令:
$ cc -L #HOME/archive -o myprog myprog.c -l test
这样,即使在命令行中因要链接$HOME/lib目录下的其他库而指定-L $HOME/lib目录,链接程序也将先在$HOME/archive目录下找到libtest.a库,此时它将进行静态链接。即使后来搜索$HOME/lib目录找到libtest.so,但也“为时已晚"。
当然这种方法要将动态库和静态库分开存放,显得有点乱。并且在某些情况下也不是总能解决问题。例如假定$HOME/archive目录下还有另外一个静态库libtest.a,并且在$HOME/lib目录下还有该库文件的一个动态版本。假设我们需对libtest.a进行静态链接而对libtest2.a进行动态链接。采用上述的方法显然就达不到目的了。如果就是要使用此种方法,那么就只好再建新目录存放libtest2.a,但这显然是比较麻烦的。
为了解决这个问题,在cc命令行中提供了-B static和-B dynamic这两个选项以取消或转向动态链接。例如,假定libtest.a仍在$HOME/lib目录下,下列命令:
$ cc -L $ HOME/lib -o myprog myprog.c -Bstatic -l test -Bdynamic
使得链接程序将myprog静态地与libtest.a相链而动态地与libc.so相链接(-lc最缺省的且位于最后)。
-Bdynamic将使链接程序不使用其后用-l选项指定的库的动态版本进行链接,而-Bdynamic选项则取消此种设定。在cc命令行中-Bstatic和-Bdynamic可以交替地使用,以获得所期望的链接效果。
增加链接程序搜索目录的另外一种方法是使用环境变量LD_LIBRARY_PATH。在此环境变量中所记录的是一系列路径的名称,如同PATH环境变量那样。如我们可以如下定义LD_LIBRARY_PATH 的值:
$ LD_LIBRARY_PATH=dir1:dir2:dir3;dir4:dir5
$ export LD_LIBRARY_PATH
我们看到LD_LIBRARY_PATH记录的值得形式同PATH变量类似:由冒号分隔的一系列路径名。所不同的只是其中可以用一个分号(;)将各路径名分成前后两个部分。分号之前的各个目录将被链接程序在-L指定的路径之前被搜索;而分号之后的各路径则在-L指定的路径之后被搜索。当然用分号将路径两部分并不是必须的。
在使用了LD_LIBRARY_PATH环境变量的值之后,链接程序将搜索的库文件目录就极大地扩展了。在链接时,首先链接程序将在LD_LIBRARY_PATH中分号之前的各目录中;然后在命令行中由-L选项指定的各目录中;再在LD_LIBRARY_PATH中分号之后的各目录中;最后再系统的标准位置搜索由-l选项指定的各个库文件。应该主要的是,在设置LD_LIBRARY_PATH的值时,必须使用绝对路径。
LD_LIBRARY_PATH还有其他的用处。下一节我们将介绍这一点。

UNIX系统开发-动态链接程序搜索目录
在采用动态链接方式对程序进行编译,链接时候。链接程序只是在最终的可执行文件中记录下关于所引用的共享库中的符号的一些登记信息,以便在程序被执行时,动态链接程序能够根据这些登记信息找到相应的代码。因此登记动态链接方式而言,除了在链接阶段涉及到对库文件的搜索路径外,还有一个在程序运行阶段对库文件的搜索问题。
前一个问题我们知道借助于LD_LIBRARY_PATH环境变量及cc命令行中的-L选项已经得到了比较好的解决。对于第二个问题,我们必须提供一种机制,使得动态链接程序能够找到相应的动态库,然后才能将其代码映射到其自己的地址空间中。
UNIX系统中对这些问题的解决实际上采取了类似的方法。程序员可以使用LD_RUN_PATH来解决上面的第二个问题。同PATH变量相同,LD_RUN_PATH的值也是一系列由冒号分隔的绝对路径名组成的。在此环境变量中,我们可以列出所用到的动态库所在的那些目录,一般动态链接程序在程序运行时对这些目录进行搜索。例如,我们可以定义LD_RUN_PATH的值如下:
$ LD_RUN_PATH=$HOME/lib;export LD_RUN_PATH
这里我们在LD_RUN_PATH中只指定了一个目录名。在使用如下命令:
$ cc -L $HOME/lib -o myprog myprog.c -l test
建立可执行文件myprog之后,在shell提示符下输入:
$ myprog ...
在执行此程序时,动态链接程序将根据LD_RUN_PATH中所记录的路径($HOME/lib),在其中依次搜索有关的动态库libtest.so。
在搜索完LD_RUN_PATH指定的各个目录之后,动态链接程序将根据缺省设置搜索系统标准位置。对于动态链接程序而言,库的标准位置只有/usr/lib。编译系统所提供的库的每一个可执行版本都保留在/usr/lib目录中。
上一节我们介绍过的环境变量LD_LIBRARY_PATH实际上也能起到同样的作用。并且使用此环境变量还有一个好处就是在链接生成了可执行文件之后,可以把此可执行文件用到的共享库移至另一个目录中,不必重新对程序进行链接。只需恰当地设置LD_LIBRARY_PATH的值,仍然可以让动态链接程序找到相应的动态库。
仍以上例来说明这个问题。假定在生成了myprog文件之后,我们将libtest.so移至另外一个目录$HOME/sharedlib下。此时仅仅将LD_RUN_PATH的值设成是$HOME/sharedlib是不行的,因为编译产生的可执行程序将无法使用$HOME/sharedlib目录下的libtest.so。但可以在LD_LIBRARY_PATH中指定新的目录。
$LD_LIBRARY_PATH= $HOME/sharedlib;export LD_LIBRARY_PATH
这样再执行myprog时,动态链接程序将首先在$HOME/lib中搜索libtest.so。当然此时它是找不到该文件的。于是根据LD_LIBRARY_PATH的值,动态链接程序将搜索$HOME/sharedlib目录,这时它将找到所需要的库。
此种方法能够奏效是因为libtest.so的路径名在myprog中不是硬编码,所以可以在执行程序时引导动态链接程序搜索另外一个目录。也就是说,可以移动共享库而不致使引用程序无法运行,但是如果在同动态库链接时使用的是硬编码,那么就无法获得此种灵活性了。
硬编码是什么意义呢?这种做法实际上是违反cc命令行用-l指定待链接的库的约定,而直接使用库文件的全路径名进行链接。例如我们可以将myfunc.c作成一个动态链接库:
$ cc -K PIC -G -o $HOME/myfunc myfunc.c
这里生成的动态库的名称是$HOME/myfunc。由于没有遵循对动态库的命名约定,故我们想链接该库时将不能再使用-l选项的方法。而只好使用如下的“硬编码”:
$ cc -o myprog myprog.c $ HOME/myfunc
使用此种方法,编译链接也能够成功,但如果其后我们将myfunc移至另外某个目录下,则除了重新链接myprog之外别无他法。
动态链接的动态特点使得我们可以在不改变函数调用接口的前提下,对共享库的实现进行一定程度的更新,而不用重新对用到的该共享库的程序进行编译、链接。当然在动态链接库被更新之后,需要核实一下使用到该动态库的程序与新版本的兼容性,这个可以使用ldd命令来完成。

你可能感兴趣的:(C++学习)