用GCC开发linux应用程序(经典)
2009年11月01日 星期日 23:58
作为自由软件的旗舰项目,Richard Stallman 在十多年前刚开始写作 GCC 的时候,还只是把它当作仅仅一个 C程序语言的编译器;GCC 的意思也只是 GNU C Compiler 而已。经过了这么多年的发展,GCC 已经不仅仅能支持 C语言;它现在还支持 Ada 语言、C++ 语言、Java 语言、Objective C 语言、Pascal语言、COBOL语言,以及支持函数式编程和逻辑编程的 Mercury 语言,等等。而 GCC 也不再单只是 GNU C语言编译器的意思了,而是变成了 GNU Compiler Collection 也即是 GNU 编译器家族的意思了。另一方面,说到 GCC 对于操作系统平台及硬件平台支持,概括起来就是一句话:无所不在。 1 程序编译过程 GCC是CUI(命令行交互界面)程序,这让许多从Windows走出来 Guier们感到恐惧。实际上它也有许多前端窗口界面,Windows下有Dev C++,Linux下譬如KDevelopment,但既然选择了GCC还是将CUL进行到底吧,没有难与不难的问题,只有做与不做的问题! 下面基于一个具体而微的程序,讨论GCC的使用。示例程序如下: //test.c#include int main(void){ printf("Hello World!/n"); return 0;} 这个程序,一步到位的编译指令是: gcc test.c -o test 输出的可执行文件名为test,Windows用户可能会感到奇怪,可执行文件明怎么没有.exe扩展名呢?Linux系统中,文件类型并非以扩展名识别的! 实质上,上述编译过程是分为四个阶段进行的,即预处理(也称预编译,Preprocessing)、编译(Compilation)、汇编 (Assembly)和连接(Linking)。 1.1 预处理 运行预处理命令: gcc -E test.c -o test.i 或 gcc -E test.c 可 以输出test.i文件中存放着test.c经预处理之后的代码。打开test.i文件,看一看,就明白了。后面那条指令,是直接在命令行窗口中输出预处 理后的代码,而不是以文件作为输出设备。gcc的-E选项,可以让编译器在预处理后停止,并输出预处理结果。在本例中,预处理结果就是将stdio.h 文件中的内容插入到test.c中了。 gcc的-o选项,用于输出处理结果到文件中。 1.2 编译为汇编代码 预处理之后,可直接对生成的test.i文件编译,生成汇编代码: gcc -S test.i -o test.s gcc的-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件。 生成的汇编代码如下: .file "test.c" .section .rodata .align 4.LC0: .string "Hello World,Linux programming!" .text.globl main .type main, @functionmain: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $4, %esp movl $.LC0, (%esp) call puts movl $0, %eax addl $4, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.1.0 20060304 (Red Hat 4.1.0-3)" .section .note.GNU-stack,"",@progbits 1.3 汇编(Assembly) 如果你学过汇编语言,那么你就该知道程序编译到了这个地步,应当使用汇编器,将汇编语言翻译为机器代码了。这一步尤其重要,因为它决定了你生成的程序,能够运行在哪种机器上。gcc使用的汇编器是gas。 在Intel IA-32平台上,还有一些常用的汇编器有:
对于上一小节中生成的汇编代码文件test.s,gas汇编器负责将其编译为目标文件,如下: gcc -c test.s -o test.o 1.4 连接 gcc连接器是gas提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。 对于上一小节中生成的test.o,将其与C标准输入输出库进行连接,最终生成程序test: gcc test.o -o test 在命令行窗口中,运行test这个小程序,让它说HelloWorld吧! 2、多个程序文件的编译 通常整个程序是由多个源文件组成的,相应地也就形成了多个编译单元,使用GCC能够很好地管理这些编译单元。假设有一个由test1.c和 test2.c两个源文件组成的程序,为了对它们进行编译,并最终生成可执行程序test,可以使用下面这条命令: # gcc test1.c test2.c -o test 如果同时处理的文件不止一个,GCC仍然会按照预处理、编译和链接的过程依次进行。如果深究起来,上面这条命令大致相当于依次执行如下三条命令: # gcc -c test1.c -o test1.o# gcc -c test2.c -o test2.o# gcc test1.o test2.o -o test 需要打这么多编译指令,看着都累,许多Guier们又要抱怨了。的确如此,如果单单使用GCC来编译你的程序,一千个程序源文件的项目编译至少要在命令 行窗口中敲1k次文件名,才能完成一次编译。如果代码有了改动,重新编译,需要再原样输入一次编译指令。再技术高超的Cler也会累死的,但是很奇怪,那 些Cler们至今依然活的很生龙活虎,这得益于GNU Make工具,详情见Make基础一节。 3、检错 GCC包含完整的出错检查和警告提示功能,可以帮助程序员写出更为标准、健壮的代码。如下面的代码: //illcode.c#include void main(void){ long long int var = 1; printf("It is not standard C code!/n"); printf("long long int var=%d",var);} 这种代码,可能在老的C语言课本里能够见到,但它是不符合ANSI/ISO C语言标准的。我让同学在Visual Stdio .net 2003上编译了一下,没检测出什么问题来。下面看看GCC可不可以: gcc -pedantic illcode.c -o illcode 输出结果: illcode.c: 在函数 ‘main’ 中:illcode.c:5: 警告:ISO C90 不支持 ‘long long’illcode.c:4: 警告:‘main’ 的返回类型不是 ‘int’ -pedantic编译选项并不能保证被编译程序与ANSI/ISO C标准的完全兼容,它仅仅只能用来帮助Linux程序员离这个目标越来越近。或者换句话说,-pedantic选项能够帮助程序员发现一些不符合 ANSI/ISO C标准的代码,但不是全部,事实上只有ANSI/ISO C语言标准中要求进行编译器诊断的那些情况,才有可能被GCC发现并提出警告。 如果采用默认的编译,即:gcc -pedantic illcode.c -o illcode。输出: test.c: 在函数 ‘main’ 中:test.c:4: 警告:‘main’ 的返回类型不是 ‘int’ 上面的示例中,long long int是GNU C的扩展类型,表示64位整型数,这种类型没有纳入C/C++标准中,可见GCC默认的编译指令,无法完全检测出不符合标准C/C++的代码,但要比 Visual Stdio .net 2003一声都不吭要好一些。如果使用-pedantic选项,GCC就可以基本上按照标准C/C++进行代码检测了,不要挑剔什么,迄今为止没有任何一 款编译器完全支持标准C/C++的。 除了-pedantic之外,GCC还有一些其它编译选项也能够产生有用的警告信息。这些选项大多以-W开头,其中最有价值的当数-Wall了,使用它能够使GCC产生尽可能多的警告信息。 GCC给出的警告信息虽然从严格意义上说不能算作错误,但却很可能成为错误的栖身之所。一个优秀的Linux程序员应该尽量避免产生警告信息,使自己的 代码始终保持标准、健壮的特性。所以将警告信息当成编码错误来对待,是一种值得赞扬的行为!所以,在编译程序时带上-Werror选项,那么GCC会在所 有产生警告的地方停止编译,迫使程序员对自己的代码进行修改,如下: gcc -Werror test.c -o test 输出: cc1: warnings being treated as errorstest.c: 在函数 ‘main’ 中:test.c:4: 警告:‘main’ 的返回类型不是 ‘int’ 4、库文件连接 人家已经发明了轮子,而且物美价廉,那么我们就实在没有必要浪费生命再去发明同样的轮子!开发软件时,完全不使用第三方函数库的情况是比较少 见的,通常来讲都需要借助许多函数库的支持才能够完成相应的功能。从程序员的角度看,函数库实际上就是一些头文件(.h)和库文件(so、或lib、 dll)的集合。虽然Linux下的大多数函数都默认将头文件放到/usr/include/目录下,而库文件则放到/usr/lib/目录 下;Windows所使用的库文件主要放在Visual Stido的目录下的include和lib,以及系统文件夹下。但也有的时候,我们要用的库不再这些目录下,所以GCC在编译时必须用自己的办法来查找 所需要的头文件和库文件。 GCC采用搜索目录的办法来查找所需要的文件,-I选项可以向GCC的头文件搜索路径中添加新的目录。例如,如果在 /home/lyanry/include/目录下有编译时所需要的头文件,为了让GCC能够顺利地找到它们,就可以使用-I选项: # gcc test.c -I /home/lyanry/include -o test 同样,如果使用了不在标准位置的库文件,那么可以通过-L选项向GCC的库文件搜索路径中添加新的目录。例如,如果在 /home/lyanry/lib/目录下有链接时所需要的库文件libtest.so,为了让GCC能够顺利地找到它,可以使用下面的命令: # gcc test.c -L /home/lyanry/lib -ltest -o test 上面这条命令中,值得好好解释一下的是-l选项,它指示GCC去连接库文件libfoo.so。Linux下的库文件在命名时有一个约定,那就是应该以 lib三个字母开头,由于所有的库文件都遵循了同样的规范,因此在用-l选项指定链接的库文件名时可以省去lib三个字母,也就是说GCC在对-lfoo 进行处理时,会自动去链接名为libfoo.so的文件。(注:至于在Windows下该怎样连接库文件,未做尝试,以后再谈) Linux下的库文件分为两大类分别是动态链接库(通常以.so结尾)和静态链接库(通常以.a结尾),二者的区别仅在于程序执行时所需的代码是在运行 时动态加载的,还是在编译时静态加载的。动态加载,意味着内存中仅存在一份库代码,所调用的函数只是在调用程序中存在一个映像。而静态加载,意味着将库中 所调用的函数代码复制到调用程序中。如果库中存在同名的静态库和动态库,则在默认情况下, GCC在链接时优先使用动态链接库,只有当动态链接库不存在时才考虑使用静态链接库,如果需要的话可以在编译时加上-static选项,强制使用静态链接 库。例如,如果在 /home/xiaowp/lib/目录下有链接时所需要的库文件libtest.so和libtest.a,为了让GCC在链接时只用到静态链接库,可 以使用下面的命令: # gcc test.c -L /home/xiaowp/lib -static -ltest -o test 5、优化 代码优化指的是编译器通过分析源代码,找出其中尚未达到最优的部分,然后对其重新进行组合,目的是改善程序的执行性能。GCC 提供的代码优化功能非常强大,它通过编译选项-On来控制优化代码的生成,其中n是一个代表优化级别的整数。对于不同版本的GCC来讲,n的取值范围及其 对应的优化效果可能并不完全相同,比较典型的范围是从0变化到2或3。 编译时使用选项-O可以告诉GCC同时减小代码的长度和执 行时间,其效果等价于-O1。在这一级别上能够进行的优化类型虽然取决于目标处理器,但一般都会包括线程跳转(Thread Jump)和延迟退栈(Deferred Stack Pops)两种优化。选项-O2告诉GCC除了完成所有-O1级别的优化之外,同时还要进行一些额外的调整工作,如处理器指令调度等。选项-O3则除了完 成所有-O2级别的优化之外,还包括循环展开和其它一些与处理器特性相关的优化工作。通常来说,数字越大优化的等级越高,同时也就意味着程序的运行速度越 快。许多Linux程序员都喜欢使用-O2选项,因为它在优化长度、编译时间和代码大小之间,取得了一个比较理想的平衡点。 下面通过具体实例来感受一下GCC的代码优化功能,所用程序如下: //testOpt.c #include int main(void){ double counter; double result; double temp; for (counter = 0; counter != 2000.0 * 2000.0 * 2000.0 / 20.0 + 2000; counter += (5 - 1) / 4) { temp = counter / 1979; result = counter; } printf("Result is %lf/n", result); return 0;} 首先不加任何优化选项进行编译: gcc -Wall testOpt.c -o testOpt 借助Linux提供的time命令,可以大致统计出该程序在运行时所需要的时间: $time ./testOpt Result is 400001999.000000real 0m7.759suser 0m7.444ssys 0m0.008s 接下去使用-O1优化选项来对代码进行优化处理: gcc -Wall -O testOpt.c -o testOpt 测试运行时间: $time ./testOPt Result is 400001999.000000 real 0m2.445s user 0m2.436s sys 0m0.000s 接下去使用-O2优化选项来对代码进行优化处理: gcc -Wall -O2 testOpt.c -o testOpt 测试运行时间: $time ./testOPt Result is 400001999.000000 real 0m2.338s user 0m2.320s sys 0m0.004s 尽管GCC的代码优化功能非常强大,但作为一名优秀的Linux程序员,首先还是要力求能够手工编写出高质量的代码。如果编写的代码简短,并且逻辑性强,编译器就不会做更多的工作,甚至根本用不着优化。特别在以下一些场合中应该避免使用优化:
|