Linux应用程序设计
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

2.2 GCC编译器

编译与链接是指源代码转化生成可执行程序的过程,它包括预处理、编译、汇编、链接过程,在Linux中最常用的编译器是GCC编译器,它是GNU推出的功能强大、性能优越的多平台编译器,其执行效率与一般的编译器相比平均效率要高20%~30%,GCC指令的一般格式为:

# gcc [选项] 要编译的文件 [选项] [目标文件]

其中,如果不指定目标文件的名称,GCC默认生成可执行的文件,文件名为:编译文件.out,GCC有超过100个的可用编译选项,主要包括总体选项、告警和出错选项、优化选项和体系结构相关选项。

Linux系统的GCC(GNU C Compiler)是GNU的代表作品之一,目前已发展到了4.5版本,尽管在软件开发社区之外对其知之甚少,但因为其在几乎所有开源软件和自由软件中都会用到,因此它的编译性能会直接影响到Linux、Firefox乃至于OpenOffice.org和Apache等几千个项目的开发。因此,把GCC摆在开源软件的核心地位是一点也不为过。GCC是Linux的唯一编译器,没有GCC就没有Linux,GCC的重要性不言而喻。

2.2.1 编译流程

GCC的编译流程分为4个步骤,分别为:

预处理(Pre-Processing)——把头文件插入到源文件内容中,并且把宏转换为对应的值。

编译(Compiling)——检查代码的规范性、是否有语法错误,以确定代码实际要做的工作,检查无误后,把代码翻译成汇编语言。

汇编(Assembling)——把编译阶段生成的文件转换成目标文件。

链接(Linking)——把程序中一些函数的实现部分链接起来,生成可执行程序文件。

2.2.2 编译选项

GCC有超过100个可用的编译选项,主要包括总体选项、告警和出错选项和优化选项等,接下来对每一类中最常用的选项进行分析。

1.总体选项

GCC的总结选项如表2-1所示。

表2-1 GCC总体选项列表

对于“-c”、“-o”选项是最常用的选项,如在/root/workplace/Gcc目录编写hello.c文件内容为:

/*hello.c*/
#include <stdio.h>
int main()
{
   printf("Hello!!\n");
   return 0;
}

使用-c选项对代码进行编译,生成目标代码:

# gcc hello.c -c hello.o

在当前目录下生成了hello.o的目标文件,该文件不可执行,只有经过链接后才可以生成可执行程序,使用-o选项对目标文件进行链接:

# gcc hello.o -o hello

在当前目录下生成的hello文件为可执行程序文件,可以执行程序。

#./hello
Hello!!

另外两个常用的头文件依赖和库依赖选项分别是“-I dir”和“-L dir”。“-I dir”选项可以在头文件的搜索路径列表中添加dir目录,在该目录中的头文件即可使用尖括号<>引用。由于Linux中头文件都默认放到了“/usr/include/”目录下,因此,当用户要添加放置在其他位置的头文件时,就可以通过“-I dir”选项来指定,这样,GCC就会到相应的目录位置查找对应的头文件。比如在上述目录下增加文件hello.h:

/*hello.h*/
#include <stdio.h>

并把hello.c文件的代码首行修改为:

#include <hello.h>

这样,就可在gcc命令行中加入“-I”选项:

# gcc hello.c –I /root/workplace/Gcc/ -o hello

并能够执行出正确结果。

小知识:在include语句中,尖括号<>表示在标准路径中搜索头文件,而双引号""表示在本目录中搜索。故在上例中,可把hello1.c的“#include <hello.h>”改为“#include "hello.h"”,就不需要加上“-I”选项了。

选项“-L dir”的功能与“-I dir”类似,能够在库文件的搜索路径列表中添加dir目录。例如有程序hello_sq.c需要用到目录“/root/workplace/Gcc/lib”下的一个动态库libsunq.so,则输入如下命令即可:

# gcc hello_sq.c –L /root/workplace/Gcc/lib –lsunq –o hello_sq

需要注意的是,“-I dir”和“-L dir”都只是指定路径,而不是指定包含或者链接的文件,因此不能在路径中包含文件名。

另外值得详细解释一下“-l”选项,它指示GCC去链接库文件libsunq.so。由于在Linux下的库文件命名时有一个规定:必须以l、i、b这3个字母开头。因此在用-l选项指定链接的库文件名时,可以省去l、i、b这3个字母。也就是说,GCC在对“-lsunq”进行处理时,会自动去链接名为libsunq.so的库文件。

2.告警和出错选项

GCC的告警和出错选项如表2-2所示。

表2-2 GCC的告警和出错选项

下面结合实例对这几个告警和出错选项进行简单的分析。

如有以下程序段:

#include <stdio.h>
void main()
{
   long long tmp = 1;
   printf("This is a bad code!\n");
   return 0;
}

这是一个存在告警信息的程序,读者可以考虑一下存在哪些问题?

 "-Wall"

允许发出GCC能够提供的所有有用的告警信息。该选项的运行结果如下所示:

# gcc –Wall warning.c –o warning
warning.c:4 警告:main的返回类型不是int
warning.c: 在函数main中
warning.c:7 警告——在无返回值的函数中,return带返回值
warning.c:5 警告——未使用的变量tmp

需要注意,使用“-Wall”选项找出的告警信息使用不同的GCC版本时略有不同。

3.优化选项

GCC具有优化代码的功能,代码的优化是一项比较复杂的工作,可归类为:源代码级优化、速度与空间的权衡、执行代码的调度。

GCC提供了下列优化选项:

-O0——默认不优化(若要生成调试信息,最好不优化)。

-O1——简单优化,不进行速度与空间的权衡优化。

-O2——在O1简单优化的基础进一步优化,包括了执行代码的调度优化。若要优化,该选项最适合,它是GNU发布软件的默认优化级别。

-Os——生成最小执行文件。

一般来说,调试时不优化,一般的优化选项用-O2(GCC允许-g与-O2联用,这也是GNU软件包发布的默认选项),嵌入设备可以考虑使用-Os。

2.2.3 静态库和动态库

我们通常把一些公用函数制作成函数库,供其他程序使用。函数库一般分为静态库和动态库两种,它们都是把函数的实现进行了封装,但两种库在程序编译和链接时的机制却有较大不同。

静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要该静态库。静态库后缀名一般为“.a”,在编译链接时,把该可执行程序用到的库文件中的函数和这些函数的所有依赖函数实现(代码经过编译后生成的二进制码)全部加入到可执行文件中,因此生成的可执行程序文件比较大,但在运行时不再需要库文件,即不需要把库文件放在存储设备中,如果一个库只被一个可执行程序使用且该可执行程序只启动一个进程时,建议使用静态库的方式。

动态库在程序编译时并不会被链接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在。动态库一般后缀名为“.so”,在编译链接时并没有把库文件的函数实现加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省多个可执行程序使用同一个库时,把库函数实现分别编译到各自可执行程序带给系统的冗余开销,如前面所引用的libc.so.6就是动态库,GCC在编译时默认使用动态库。

2.2.4 常见编译错误

GCC编译器如果发现源程序中有错误,就无法继续进行,也无法生成最终的可执行文件。为了便于修改,GCC给出错误提示,用户必须对这些错误提示逐个进行分析、处理,并修改相应的代码,才能保证源代码的正确编译链接。GCC给出的错误提示一般可以分为四大类,下面分别讨论其产生的原因和对策。

1.第一类:C语法错误

错误信息:文件source.c中第n行有语法错误(syntex errror)。这种类型的错误,一般都是C语言的语法错误,应该仔细检查源代码文件中第n行及该行之前的程序,有时也需要对该文件所包含的头文件进行检查。有些情况下,一个很简单的语法错误,GCC会给出一大堆错误,要保持清醒的头脑,不要被其吓倒,必要的时候再参考一下C语言的基本教材。

2.第二类:头文件错误

错误信息:找不到头文件head.h(Can not find include file head.h)。这类错误是源代码文件中的包含头文件有问题,可能的原因包括头文件名错误、指定的头文件所在目录名错误等,也可能是错误地使用了双引号和尖括号。

3.第三类:函数库错误

错误信息:链接程序找不到所需的函数库,例如:

ld: -lm: No such file or directory

这类错误是与目标文件相链接的函数库有错误,可能的原因是函数库名错误、指定的函数库所在目录名称错误等,检查的方法是使用find命令在编译可能搜索的目录中寻找相应的函数库名,确定函数库及目录的名称并修改编译选项中的名称。

4.第四类:未定义符号

错误信息:有未定义的符号(Undefined symbol)。这类错误是在链接过程中出现的,可能有两种原因:一是使用者自己定义的函数或者全局变量所在源代码文件,没有被编译、链接,或者干脆还没有定义,这需要使用者根据实际情况修改源程序,给出全局变量或者函数的定义体;二是未定义的符号是一个标准的库函数,在源程序中使用了该库函数,而链接过程中还没有给定相应的函数库的名称,或者是该函数库的目录名称有问题,这时需要使用函数库维护命令ar检查我们需要的库函数到底位于哪一个函数库中,也可以使用nm命令检查指定的函数库中是否包含指定的函数或符号,确定之后,修改GCC链接选项中的“-l”和“-L”项。

排除编译、链接过程中的错误,应该说这只是程序设计中最简单、最基本的一个步骤,可以说只是开了个头。这个过程中的错误,只是在使用C语言描述一个算法中所产生的错误,是比较容易排除的。编写一个程序,到编译、链接通过为止,应该说刚刚开始,程序在运行过程中所出现的问题,是算法设计有问题,需要更加深入地测试、调试和修改。一个稍为复杂的程序,往往要经过多次的编译、链接和测试、修改。