gcc

来源:互联网 发布:平安证券官方软件 编辑:程序博客网 时间:2024/05/16 08:25

http://gcc.gnu.org/ 

gcc 是 gnu compiler collecti 编译器,是GNU推出的功能强大、性能优越的多平台编译器,是GNU的代表作品之一。

              gcc 和g++什么关系:事实上只有一个c++编译器,那就是g++。g++不仅仅是一个c++预处理器,而是一个实实在在的c++编译器。由于它的名字 gnu c++ compiler 也能缩写成gcc,所以有时候有人叫它gcc也并不错。而我们通常所说的gcc是一个编译器套装,gcc命令只是一个调用各个实际编译器的快捷方式而已。

              gcc将生成一个名为 a.out的文件。在Linux系统中,可执行文件没有统一的后缀,系统从文件的属性来区分可执行文件和不可执行文件。而gcc则通过后缀来区别输入文件的类别,下面我们来介绍gcc所遵循的部分约定规则。

.c为后缀的文件,C语言源代码文件;

.a为后缀的文件,是由目标文件构成的档案库文件;

.C,.cc或.cxx 为后缀的文件,是C++源代码文件;

.h为后缀的文件,是程序所包含的头文件;

.i 为后缀的文件,是已经预处理过的C源代码文件;

.ii为后缀的文件,是已经预处理过的C++源代码文件;

.m为后缀的文件,是Objective-C源代码文件;

.o为后缀的文件,是编译后的目标文件;

.s为后缀的文件,是汇编语言源代码文件;

.S为后缀的文件,是经过预编译的汇编语言源代码文件。

 

gcc的执行过程

gcc的编译流程分为四个步骤:

         预处理(也称预编译,Preprocessing):预处理器CPP将对源文件中的宏进行展开。

         编译(Compilation) :gcc将c文件编译成汇编文件。

         汇编(Assembly) :as将汇编文件编译成机器码。

         连接(Linking) :ld将目标文件和外部符号进行连接,得到一个可执行二进制文件。

(1)预处理阶段

        命令gcc首先调用cpp进行预处理,在预处理过程中,对源代码文件中的文件包含(include)、预编译语句(如宏定义define等)进行分析。编译器将代码中的stdio.h编译进来,并且用户可以使用gcc的选项”-E”进行查看,该选项的作用是让gcc在预处理结束后停止编译过程。
    《深入理解计算机系统》中说的:
     预处理器(cpp)根据以字符#开头的命令(directives),修改原始的C程序。如hello.c中#include <stdio.h>指令告诉预处理器读系统头文件stdio.h的内容,并把它直接插入到程序文本中去。结果就得到另外一个C程序,通常是以.i作为文件扩展名的。
   注意:
        Gcc指令的一般格式为:Gcc [选项] 要编译的文件 [选项] [目标文件]    其中,目标文件可缺省,Gcc默认生成可执行的文件名为:编译文件.out
(2)编译阶段              
            在这个编译阶段中,Gcc首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言。用户可以使用”-S”选项来进行查看,该选项只进行编译而不进行汇编,生成汇编代码。汇编语言是非常有用的,它为不同高级语言不同编译器提供了通用的语言。如:C编译器和Fortran编译器产生的输出文件用的都是一样的汇编语言。
     这个阶段根据输入文件生成以.o为后缀的目标文件。汇编过程是针对汇编语言的步骤,调用as进行工作,一般来讲,.S为后缀的汇编语言源代码文件和汇编、.s为后缀的汇编语言文件经过预编译和汇编之后都生成以.o为后缀的目标文件。

(3)汇编阶段
           汇编阶段是把编译阶段生成的”.s”文件转成目标文件,读者在此可使用选项”-c”就可看到汇编代码已转化为”.o”的二进制目标代码了。

如下所示:


(4)链接阶段
           在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。
          程序中并没有定义”printf”的函数实现,且在预编译中包含进的”stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf” 了,而这也就是链接的作用。

函数库一般分为静态库和动态库两种:

             静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为”.a”。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。

            使用静态链接的好处是,依赖的动态链接库较少,对动态链接库的版本不会很敏感,具有较好的兼容性;缺点是生成的程序比较大。

             动态库一般后缀名为”.so”,如前面所述的libc.so.6就是动态库。gcc在编译时默认使用动态库。使用动态链接的好处是,生成的程序比较小,占用较少的内存。

完成了链接之后,gcc就可以生成可执行文件,如下所示:

           运行该可执行文件,出现正确的结果如下。当所有的目标文件都生成之后,gcc就调用 ld来完成最后的关键性工作,这个阶段就是连接。在连接阶段,所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中连到合适的地方。

 

用一个经典的hello.c程序为例:

#include<stdio.h>int main(void){        printf("welcome to linux world!\n");        return 0;}

[redhat@bogon ~]$ gcc -E hello.c -o hello.i  //这个阶段处理预定义和头文件包含#include,并做语法检查。——预编译过程
[redhat@bogon ~]$ gcc -E hello.c  // 用-E来查看预编译详细过程.

[redhat@bogon ~]$ gcc -S hello.c -o hello.s //这个阶段,生成汇编代码。——编译过程
[redhat@bogon ~]$ as hello.s -o hello.o //这个阶段,生成目标代码。——汇编过程  
[redhat@bogon ~]$ gcc hello.o -o hl  //链接过程。生成可执行代码.链接过程,链接分两种:静态链接和动态链接.见上.
[redhat@bogon ~]$ ./hl    //生成了可执行文件,程序运行:
welcome to linux world!

上面过程描述了使用gcc进行预处理(生成*.i文件)、编译(生成*.s文件)、汇编(生成*.o文件)、链接(声称可执行文件)的四个步骤,其实上面四个步骤可由下面一条语句完成:

[redhat@bogon ~]$ gcc hello.c -o hl
[redhat@bogon ~]$ ./hl
welcome to linux world!

 

说明:

[redhat@bogon ~]$gcc -E hello.c -o hello.i  //编译阶段,输入的是中间文件*.i  ,-o 是生成可执行文件的输出选项。
[redhat@bogon ~]$gcc -S hello.c -o hello.s  //编译后生成汇编语言文件*.s
[redhat@bogon ~]$gcc -c hello.c -o hello.o  //在汇编阶段,将输入的汇编文件*.s转换成机器语言*.o
[redhat@bogon ~]$gcc hello.o -o hello       //最后,在连接阶段将输入的机器代码文件*.s(与其它的机器代码文件和库文件)汇集成一个可执行的二进制代码文件。

GCC常用的两种模式:编译模式和编译连接模式。

[redhat@bogon ~]$ gcc hello.c  //作用:将hello.c预处理、汇编、编译并链接形成可执行文件。这里未指定输出文件,默认输出为a.out。编译成功后可以看到生成了一个a.out的文件。在命令行输入./a.out 执行程序。./表示在当前目录,a.out为可执行程序文件名。

[redhat@bogon ~]$gcc hello.c -o hello //作用:将hello.c预处理、汇编、编译并链接形成可执行文件hello。-o选项用来指定输出文件的文件名。输入./hello执行程序。
[redhat@bogon ~]$gcc -E hello.c -o hello.i //作用:将hello.c预处理输出hello.i文件。
[redhat@bogon ~]$gcc -S hello.i  //作用:将预处理输出文件hello.i汇编成hello.s文件。
[redhat@bogon ~]$gcc -c hello.s //作用:将汇编输出文件hello.s编译输出hello.o文件。
[redhat@bogon ~]$gcc hello.o -o hello  //作用:将编译输出文件hello.o链接成最终可执行文件hello。输入./hello执行程序。
[redhat@bogon ~]$gcc -O1 hello.c -o hello //作用:使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。输入./hello执行程序。
[redhat@bogon ~]$gcc hello.cpp -o hello-lstdc++ //作用:将hello.cpp编译链接成hello可执行文件。-lstdc++指定链接std c++库。.编译使用C++ std库的程序。


 下面用gcc来编译程序查看过程。

用#define定义一个常量。实际上编译器的工作分为两个步骤,
先是预处理(Preprocess),然后才是编译,用gcc的-E选项可以看到预处理之后、编译之前的程序。

#include<stdio.h>#include<stdlib.h>#define N 20int a[N];void gen_random(int upper_bound){        int i;        for(i = 0;i < N; i++){                a[i] = rand() % upper_bound;        }}void print_rndom(){        int i;        for(i = 0;i < N; i++){                printf("%d ",a[i]);        }        printf("\n");}int main(void){        gen_random(20);        print_rndom();        return 0;}

// run
[redhat@localhost ~]$ ./a.out
3 6 17 15 13 15 6 12 9 1 2 7 10 19 3 6 0 6 12 16

[redhat@localhost ~]$ gcc -E rand.c  //或者 cpp rand.c

extern void exit (int __status) __attribute__ ((__nothrow__)) __attribute__ ((__noreturn__));

# 658 "/usr/include/stdlib.h" 3 4

........//部分略


 extern int rpmatch (__const char *__response) __attribute__ ((__nothrow__)) __attribute__ ((__nonnull__ (1))) ;
# 926 "/usr/include/stdlib.h" 3 4
extern int posix_openpt (int __oflag) ;
# 961 "/usr/include/stdlib.h" 3 4
extern int getloadavg (double __loadavg[], int __nelem)
     __attribute__ ((__nothrow__)) __attribute__ ((__nonnull__ (1)));
# 977 "/usr/include/stdlib.h" 3 4

# 3 "rand.c" 2

int a[20];
void gen_random(int upper_bound)
{
 int i;
 for(i = 0;i < 20; i++){
  a[i] = rand() % upper_bound;
 }
}
void print_rndom()
{
 int i;
 for(i = 0;i < 20; i++){
  printf("%d ",a[i]);
 }
 printf("\n");
}
int main(void)
{
 gen_random(20);
 print_rndom();
 return 0;
}

由此可见:预处理器做了两件事情:
一是把头文件stdio.h和stdlib.h在代码中展开,
二是把#define定义的标识符N替换成它的定义20
(在代码中做了三处替换,分别位于数组的定义中和两个函数中)。
以#号开头的语法元素称为预处理指示(Preprocessing Directive)。
此处,用cpp rand.c命令也可以达到同样的效果,只做预处理而不编译,cpp表示C preprocessor。

 

用gcc 查看 警告提示信息

         GCC包含完整的出错检查和警告提示功能,它们可以帮助Linux程序员写出更加专业和优美的代码。 

写一个llcode.c程序为例:

#include<stdio.h>void main(void){        long long int var = 1;        printf("It is not standard C code!\n");}

main函数的返回值被声明为void,但实际上应该是int;
使用了GNU语法扩展,即使用long long来声明64位整数,不符合ANSI/ISO C语言标准;
main函数在终止前没有调用return语句。

        下面来看看GCC是如何帮助程序员来发现这些错误的。当GCC在编译不符合ANSI/ISO C语言标准的源代码时,如果加上了-pedantic选项,那么使用了扩展语法的地方将产生相应的警告信息:

[redhat@bogon ~]$ gcc -pedantic llcode.c
llcode.c: In function ‘main’:
llcode.c:4: warning: ISO C90 does not support ‘long long’
llcode.c:3: warning: return type of ‘main’ is not ‘int’

        需要注意的是,-pedantic编译选项并不能保证被编译程序与ANSI/ISO C标准的完全兼容,它仅仅只能用来帮助Linux程序员离这个目标越来越近。或者换句话说,-pedantic选项能够帮助程序员发现一些不符合 ANSI/ISO C标准的代码,但不是全部,事实上只有ANSI/ISO C语言标准中要求进行编译器诊断的那些情况,才有可能被GCC发现并提出警告。

      除了-pedantic之外,GCC还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W开头,其中最有价值的当数-Wall了,使用它能够使GCC产生尽可能多的警告信息: 

 [redhat@bogon ~]$ gcc -Wall llcode.c
llcode.c:3: warning: return type of ‘main’ is not ‘int’
llcode.c: In function ‘main’:
llcode.c:4: warning: unused variable ‘var’

     GCC给出的警告信息虽然从严格意义上说不能算作是错误,但却很可能成为错误的栖身之所。一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持简洁、优美和健壮的特性。 

     在处理警告方面,另一个常用的编译选项是-Werror,它要求GCC将所有的警告当成错误进行处理,这在使用自动编译工具(如Make等)时非常有用。如 果编译时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改。只有当相应的警告信息消除时,才可能将编 译过程继续朝前推进。执行情况如下:

[redhat@bogon ~]$ gcc -Wall -Werror llcode.c
cc1: warnings being treated as errors
llcode.c:3: warning: return type of ‘main’ is not ‘int’
llcode.c: In function ‘main’:
llcode.c:4: warning: unused variable ‘var’

   对Linux程序员来讲,GCC给出的警告信息是很有价值的,它们不仅可以帮助程序员写出更加健壮的程序,而且还是跟踪和调试程序的有力工具。建议在用GCC编译源代码时始终带上-Wall选项,并把它逐渐培养成为一种习惯,这对找出常见的隐式编程错误很有帮助。

 

gcc 库依赖 

         在Linux 下开发软件时,完全不使用第三方函数库的情况是比较少见的,通常来讲都需要借助一个或多个函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实 际上就是一些头文件(.h)和库文件(.so或者.a)的集合。虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库 文件则放到/usr/lib/目录下,但并不是所有的情况都是这样。正因如此,GCC在编译时必须有自己的办法来查找所需要的头文件和库文件。

         GCC采用搜索目录的办法来查找所需要的文件,-I选项可以向GCC的头文件搜索路径中添加新的目录。例如,如果在/home/xiaowp/include/目录下有编译时所需要的头文件,为了让GCC能够顺利地找到它们,就可以使用-I选项:

[redhat@bogon ~]$ gcc yh.c -I /home/xiaowp/include -o yh

       同样,如果使用了不在标准位置的库文件,那么可以通过-L选项向GCC的库文件搜索路径中添加新的目录。例如,如果在/home/xiaowp/lib/目录下有链接时所需要的库文件libfoo.so,为了让GCC能够顺利地找到它,可以使用下面的命令:

[redhat@bogon ~]$ gcc yh.c -L /home/xiaowp/lib -lyh -o yh

         值得好好解释一下的是-l选项,它指示GCC去连接库文件libfoo.so。Linux下的库文件在命名时有一个约定,那就是应该以lib三个字母开头, 由于所有的库文件都遵循了同样的规范,因此在用-l选项指定链接的库文件名时可以省去lib三个字母,也就是说GCC在对-lfoo进行处理时,会自动去 链接名为libfoo.so的文件。

        Linux下的库文件分为两大类分别是动态链接库(通常以.so结尾)和静态链接库(通常以.a 结尾),两者的差别仅在程序执行时所需的代码是在运行时动态加载的,还是在编译时静态加载的。默认情况下,GCC在链接时优先使用动态链接库,只有当动态 链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接库。例如,如果在 /home/xiaowp/lib/目录下有链接时所需要的库文件libfoo.so和libfoo.a,为了让GCC在链接时只用到静态链接库,可以使 用下面的命令:
[redhat@bogon ~]$ gcc yh.c -L /home/xiaowp/lib -static -lyh -o yh

gcc 代码优化

        代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。GCC提供的代码优化功能非常强大, 它通过编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的GCC来讲,n的取值范围及其对应的优化效果可能并不完全相 同,比较典型的范围是从0变化到2或3。

        编译时使用选项-O可以告诉GCC同时减小代码的长度和执行时间,其效果等价于-O1。在这 一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。选项-O2告诉GCC除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项-O3则除了完 成所有-O2级别的优化之外,还包括循环展开和其它一些与处理器特性相关的优化工作。通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越 快。许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间,取得了一个比较理想的平衡点。


下面通过具体实例来感受一下GCC的代码优化功能,所用程序如yh.c。

#include <stdio.h> int main(void){double counter;double result;double temp;for (counter = 0; counter < 2000.0 * 2000.0 * 2000.0 / 20.0 + 2020; counter += (5 - 1) / 4) {temp = counter / 1979;result = counter; }printf("Result is %lf\n", result);return 0;}

首先不加任何优化选项进行编译:
[redhat@bogon ~]$ gcc -Wall yh.c -o yh
借助Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间:

[redhat@bogon ~]$ time ./yh
Result is 400002019.000000

real    0m11.023s
user    0m9.714s
sys     0m0.010s

接下去使用优化选项来对代码进行优化处理:

[redhat@bogon ~]$ gcc -Wall -O yh.c -o yh
在同样的条件下再次测试一下运行时间:

[redhat@bogon ~]$ time ./yh
Result is 400002019.000000

real    0m1.769s
user    0m1.610s
sys     0m0.005s

 

        对比两次执行的输出结果不难看出,程序的性能的确得到了很大幅度的改善,由原来的14秒缩短到了3秒。这个例子是专门针对GCC的优化功能而设计的,因此优 化前后程序的执行速度发生了很大的改变。尽管GCC的代码优化功能非常强大,但作为一名优秀的Linux程序员,首先还是要力求能够手工编写出高质量的代 码。如果编写的代码简短,并且逻辑性强,编译器就不会做更多的工作,甚至根本用不着优化。

       优化虽然能够给程序带来更好的执行性能,但在如下一些场合中应该避免优化代码: 

        程序开发的时候 优化等级越高,消耗在编译上的时间就越长,因此在开发的时候最好不要使用优化选项,只有到软件发行或开发结束的时候,才考虑对最终生成的代码进行优化。 

       资源受限的时候 一些优化选项会增加可执行代码的体积,如果程序在运行时能够申请到的内存资源非常紧张(如一些实时嵌入式设备),那就不要对代码进行优化,因为由这带来的负面影响可能会产生非常严重的后果。

        跟踪调试的时候 在对代码进行优化的时候,某些代码可能会被删除或改写,或者为了取得更佳的性能而进行重组,从而使跟踪和调试变得异常困难。


#define 定义的常量与枚举定义的常量的区别??

   首先,define有仅用于定义常量,也可以定义更复杂的语法结构,称为宏(Macro)定义。
   其次,define定义是在预处理阶段处理的,而枚举是在编译阶段处理的。
如:

#include<stdio.h>#define PECTANGULAR 1#define POLAR 2int main(void){        int RECTANGULAR;        printf("%d %d\n",RECTANGULAR,POLAR);        return 0;}

 

上面用gcc编译的只是一个文件,现如何用gcc来编译多个文件呢??

       在采用模块化的设计思想进行软件开发时,通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用GCC能够很好地管理这些编译单元。假设有一个由test1.c和test2.c两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序test,可以使用下面这条命令:

[redhat@localhost ~]$ test1.c test2.c -o test

同时处理的文件不止一个,GCC仍然会按照预处理、编译和链接的过程依次进行。如上面这条命令大致相当于依次执行如下三条命令:
[redhat@localhost ~]$ gcc -c test1.c -o test1.o

[redhat@localhost ~]$ gcc -c test2.c -o test2.o 

[redhat@localhost ~]$ gcc test1.o test2.o -o test

           在编译一个包含许多源文件的工程时,若只用一条GCC命令来完成编译是非常浪费时间的。假设项目中有100个源文件需要编译,并且每个源文件中都包含 10000行代码,如果像上面那样仅用一条GCC命令来完成编译工作,那么GCC需要将每个源文件都重新编译一遍,然后再全部连接起来。很显然,这样浪费 的时间相当多,尤其是当用户只是修改了其中某一个文件的时候,完全没有必要将每个文件都重新编译一遍,因为很多已经生成的目标文件是不会改变的。要解决这个问题,关键是要灵活运用GCC,同时还要借助像Make这样的工具。

详见Makefile节。

 

gcc编译器如何工作要更为详细的信息请参考编译器手册。