迅速理解GCC的工作原理

在学习使用GCC之前,下面的这个例子能够帮助用户迅速理解GCC的工作原理,并将其立即运用到实际的项目开发中去.

 

首先用熟悉的编辑器输入清单1所示的代码: 

 

清单1:hello.c 

 

#i nclude

int main(void)

{

printf ("Hello world, Linux programming!\n" );

return 0;

}

 

 

 

然后执行下面的命令编译和运行这段程序: 

 

# gcc hello.c -o hello

# ./hello

Hello world, Linux programming!

 

 

 

从程序员的角度看,只需简单地执行一条GCC命令就可以了,但从编译器的角度来看,却需要完成一系列非常繁杂的工作.首先,GCC需要调用预处理程序cpp,由它负责展开在源文件中定义的宏,并向其中插入“#i nclude”语句所包含的内容;接着,GCC会调用ccl和as将处理后的源代码编译成目标代码;最后,GCC会调用链接程序ld,把生成的目标代码链接成一个可执行程序. 

 

为了更好地理解GCC的工作过程,可以把上述编译过程分成几个步骤单独进行,并观察每步的运行结果.第一步是进行预编译,使用-E参数可以让GCC在预处理结束后停止编译过程: 

 

#  gcc -E hello.c -o hello.i

 

 

 

此时若查看hello.cpp文件中的内容,会发现stdio.h的内容确实都插到文件里去了,而其它应当被预处理的宏定义也都做了相应的处理.下一步是将hello.i编译为目标代码,这可以通过使用-c参数来完成: 

 

#  gcc -c hello.i -o hello.o

 

 

 

GCC默认将.i文件看成是预处理后的C语言源代码,因此上述命令将自动跳过预处理步骤而开始执行编译过程,也可以使用-x参数让GCC从指定的步骤开始编译.最后一步是将生成的目标文件链接成可执行文件: 

 

#  gcc hello.o -o hello

 

 

 

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

 

#  gcc foo1.c foo2.c -o foo

 

 

如果同时处理的文件不止一个,GCC仍然会按照预处理、编译和链接的过程依次进行.如果深究起来,上面这条命令大致相当于依次执行如下三条命令: 

 

# gcc -c foo1.c -o foo1.o

# gcc -c foo2.c -o foo2.o

# gcc foo1.o foo2.o -o foo

 

 

 

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

 

警告提示功能 

 

GCC包含完整的出错检查和警告提示功能,它们可以帮助Linux程序员写出更加专业和优美的代码.先来读读清单2所示的程序,这段代码写得很糟糕,仔细检查一下不难挑出很多毛病: 

 

◆main函数的返回值被声明为void,但实际上应该是int; 

 

◆使用了GNU语法扩展,即使用long long来声明64位整数,不符合ANSI/ISO C语言标准; 

 

◆main函数在终止前没有调用return语句. 

 

清单2:illcode.c 

 

#i nclude

void main(void)

{

  long long int var = 1;

  printf("It is not standard C code!\n" );

}

 

 

 

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

 

# gcc -pedantic illcode.c -o illcode

illcode.c: In function `main':

illcode.c:9: ISO C89 does not support `long long'

illcode.c:8: 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产生尽可能多的警告信息: 

 

# gcc -Wall illcode.c -o illcode

illcode.c:8: warning: return type of `main' is not `int'

illcode.c: In function `main':

illcode.c:9: warning: unused variable `var'

 

 

GCC给出的警告信息虽然从严格意义上说不能算作是错误,但却很可能成为错误的栖身之所.一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的代码始终保持简洁、优美和健壮的特性.在处理警告方面,另一个常用的编译选项是-Werror,它要求GCC将所有的警告当成错误进行处理,这在使用自动编译工具(如Make等)时非常有用.如果编译时带上-Werror选项,那么GCC会在所有产生警告的地方停止编译,迫使程序员对自己的代码进行修改.只有当相应的警告信息消除时,才可能将编译过程继续朝前推进.执行情况如下: 

 

# gcc -Wall -Werror illcode.c -o illcode

cc1: warnings being treated as errors

illcode.c:8: warning: return type of `main' is not `int'

illcode.c: In function `main':

illcode.c:9: warning: unused variable `var'

 

 

 

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

 

库依赖 

 

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

 

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

 

# gcc foo.c -I /home/xiaowp/include -o foo

 

 

 

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

 

# gcc foo.c -L /home/xiaowp/lib -lfoo -o foo

 

 

 

值得好好解释一下的是-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在链接时只用到静态链接库,可以使用下面的命令: 

 

# gcc foo.c -L /home/xiaowp/lib -static -lfoo -o foo

 

 

 

代码优化 

 

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

 

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

 

下面通过具体实例来感受一下GCC的代码优化功能,所用程序如清单3所示. 

 

清单3: optimize.c 

 

#i nclude  

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;

}

 

 

 

首先不加任何优化选项进行编译: 

 

# gcc -Wall optimize.c -o optimize

 

 

 

借助Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间: 

 

# time ./optimize

Result is 400002019.000000

real    0m14.942s

user    0m14.940s

sys     0m0.000s

 

 

 

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

 

# gcc -Wall -O optimize.c -o optimize

 

 

 

在同样的条件下再次测试一下运行时间: 

 

# time ./optimize

Result is 400002019.000000

real    0m3.256s

user    0m3.240s

sys     0m0.000s

 

 

 

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

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

 

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

 

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

 

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

 

调试 

 

一个功能强大的调试器不仅为程序员提供了跟踪程序执行的手段,而且还可以帮助程序员找到解决问题的方法.对于Linux程序员来讲,GDB(GNU Debugger)通过与GCC的配合使用,为基于Linux的软件开发提供了一个完善的调试环境.默认情况下,GCC在编译时不会将调试符号插入到生成的二进制代码中,因为这样会增加可执行文件的大小.如果需要在编译时生成调试符号信息,可以使用GCC的-g或者-ggdb选项.GCC在产生调试符号时,同样采用了分级的思路,开发人员可以通过在-g选项后附加数字1、2或3来指定在代码中加入调试信息的多少.默认的级别是2(-g2),此时产生的调试信息包括扩展的符号表、行号、局部或外部变量信息.级别3(-g3)包含级别2中的所有调试信息,以及源代码中定义的宏.级别1(-g1)不包含局部变量和与行号有关的调试信息,因此只能够用于回溯跟踪和堆栈转储之用.回溯跟踪指的是监视程序在运行过程中的函数调用历史,堆栈转储则是一种以原始的十六进制格式保存程序执行环境的方法,两者都是经常用到的调试手段. 

 

GCC产生的调试符号具有普遍的适应性,可以被许多调试器加以利用,但如果使用的是GDB,那么还可以通过-ggdb选项在生成的二进制代码中包含GDB专用的调试信息.这种做法的优点是可以方便GDB的调试工作,但缺点是可能导致其它调试器(如DBX)无法进行正常的调试.选项-ggdb能够接受的调试级别和-g是完全一样的,它们对输出的调试符号有着相同的影响. 

 

需要注意的是,使用任何一个调试选项都会使最终生成的二进制文件的大小急剧增加,同时增加程序在执行时的开销,因此调试选项通常仅在软件的开发和调试阶段使用.调试选项对生成代码大小的影响从下面的对比过程中可以看出来: 

 

# gcc optimize.c -o optimize

# ls optimize -l

-rwxrwxr-x  1 xiaowp   xiaowp  11649 Nov 20 08:53 optimize  (未加调试选项)

# gcc -g optimize.c -o optimize

# ls optimize -l

-rwxrwxr-x  1 xiaowp   xiaowp  15889 Nov 20 08:54 optimize  (加入调试选项)

 

虽然调试选项会增加文件的大小,但事实上Linux中的许多软件在测试版本甚至最终发行版本中仍然使用了调试选项来进行编译,这样做的目的是鼓励用户在发现问题时自己动手解决,是Linux的一个显著特色.为调试编译代码(Compiling Code for Debugging)为了使 gdb 正常工作, 你必须使你的程序在编译时包含调试信息. 调试信息包含你程序里的每个变量的类型和在可执行文件里的地址映射以及源代码的行号. gdb 利用这些信息使源代码和机器码相关联. 

◆在编译时用 -g 选项打开调试选项.

 

转自:http://blog.sina.com.cn/s/blog_69e96b37010100d7.html

 

你可能感兴趣的:(Tool_Chain_GCC)