前一阵子在QQ上和朋友聊天的时候,总会看到有人说Linux上的应用程序开发是高手才可以完成的,而且这种“迷信”在目前似乎还很普遍。然而,情况并不是这样的,从程序库的支持方面,linux平台为用户级应用程序的开发提供了很多功能强大且丰富的程序库,而且它们大部分是跨平台的(Boost、OpenGL、STL、Qt、Java等)和基于POSIX标准的(glibc等),同时Linux内核还为驱动程序的开发提供了功能完备的内核接口,从开发工具方面,Linux提供了功能强大的编译器GCC和调试器GDB,借助它们的帮助,我们可以很轻松的在Linu x上开发出可移植性的应用程序。既然如此,“迷信”又源于何来呢?我想,一方面由于详细介绍Linux各种开发的书籍较少,各种Linux应用在国内仍不普及,另一方面则是由于很多人在安装好一个Linux后,苦于找不到一个得心应手的IDE环境,从而感到不知所措,毕竟,我们很多人都习惯了写好程序后,按下F5,剩下的任务就让IDE全权代理了。其实想在Linux下如此这般当然也没问题。既然说到了IDE,就让我们从它开始吧,相信选择一个好的IDE环境是你整个学习过程的一个不错的开始。
工欲善其事 必先利其器——IDE篇
其实Linux下有许多功能强大的IDE环境,因为从某种意义上说,Linux是专为开发者准备的操作系统,这个东西当然少不了,在这里为读者介绍一些比较常用的IDE。
KDevelop
这是一个用Qt开发的IDE,其主要支持的语言是C / C++,
Eclipse
近年来,eclipse可以说发展极为迅速,它不仅是一个以java为主的开发平台,其功能强大的插件体系结构使得它可以被当作各种应用程序来使用。作为各种插件的载体,eclipse提供了完整的GUI接口,用户完全可以借助eclipse来只关心自己想做的工作。
Emacs
VIM
山高月晓 水落石出——IDE后台的故事 GCC篇
前面我们简要介绍了一些IDE环境,其中所有C/C++相关程序的编译都是由GCC来完成的,而IDE只不过起到了一个收集编译信息和为我们的项目生成makefile等作用(后面我们会提到)。出于目前Linux开发的特点,C仍是系统开发的主流语言。所以,对GCC有一个全面的了解是很有必要的,一旦IDE不能满足你的需求,我们要有手工打造程序的能力,而且出于学习的目的,我们往往不需要IDE生成的那些复杂的文件,为一个Hello world生成2M多的文件显然是多余的。
GCC的全称是GNU Compiler Collection,从这个名字我们不难看出,GCC代表着一个编译器的集合,目前GCC可以支持C, C++, Objective-C,objective-c++, Fortran, Java, and Ada等语言。但是出于一般性考虑,我们这里只讨论GCC中的C/C++部分。
目前GCC的最新发布版是4.0.0,但是这个版本由于使用了新技术和新的编码规范,很多旧的代码都需要修改才可以通过编译,所以并不推荐使用这个版本。而相对稳定的新版本目前是3.4.4,大家可以到GNU的主页上更新下载。那么究竟GCC强大在哪里,如何使用?下面我就通过几个简单而实际的例子带你看看GCC提供的强大功能。
通过Helloworld的编译熟悉GCC的基本使用方法
似乎为所有新语言提供一个Hello World样本程序已经成为了一种不成文的标准,人们通过它来认识语言的一些基本要素。在这里,我们使用一个Hello World来看看如何用GCC生成可执行文件。
把上面的文件存成helloworld.c,之后打开控制台,输入如下的命令
gcc helloworld.c –o helloworld
如果一切正常的话,你的控制台上应该没有任何输出。用ls查看你的工作目录,你会发现目录下多了一个名为helloworld的可执行文件,之后,执行
./hellworld
就会看到这个程序的输出了
很简单不是吗?但是学过计算机的朋友都应该知道,程序的编译过程要分为下图所示的过程而GCC的强大之处就在于它允许你在上面所示的任何一个过程中停下来查看中间结果,并对其加以控制。
1. 预处理
首先是预处理过程,GCC的-E选项可以让GCC在预处理后停止编译,并向标准输出打印预处理过后的文件。下面的-o用于指定输出文件的文件名。
gcc –E hellowrold.c –o helloworld.cpp
下面是helloworld.cpp的一部分的内容,我们看到,文件已经包含了stdio.h中的内容。
如果我们想执行下一步的编译过程,可以继续使用GCC的-x <language type>选项,该选项用于显示指定文件的后缀名(而不是让编译器根据后缀来自行判断)。我们比较常用的language type有如下几种,(如果读者想获得更为完整参数说名,请参考GCC manual):
l c c-header c-cpp-output
l c++ c++-header c++-cpp-output
l assembler assembler-with-cpp
另外,下表列出了常用的GCC后缀名
文件后缀 | 注释 |
.c | 需要经过预处理的C代码文件 |
.i | 不需要经过预处理的C代码文件 |
.ii | 不需要经过预处理的C++代码文件 |
.h | 需要被预编译的C, C++, Objective-C头文件 |
.cc .cp .cxx .cpp .CPP .c++ .C | 需要被预处理的C++程序文件 |
.hh .H | 需要被预编译的C++头文件 |
.s | 汇编代码文件 |
.S | 需要被预处理的汇编文件 |
当然,你也可以省略掉language type的部分,这时候GCC会根据文件的后缀名自行判断,就像你没有使用该选项一样。
下面继续我们的编译过程
2. 编译
如果我们想获得编译后的源文件可以使用-S选项,该选项让gcc只执行编译(生成汇编文件)而不进行汇编(生成目标文件),此时,我们可以用-o选项指定输出的汇编文件的名称。
gcc –S helloworld.cpp –o hellowrld.S
3. 汇编
另外,我们还可以使用GCC的-c选项来编译和汇编源文件而不链接,此时-o指定的输出文件就是编译后的目标文件名
gcc –x c++ -c helloworld.cpp –o helloworld.o
4. 链接
最后,我们可以利用GCC来把我们刚才生成的.o文件链接成可执行程序
gcc helloworld.o –o helloworld
这一次,我们使用了-o选项指定了可执行文件名,也就是说,根据输入文件类型的不同,-o有着不同的含义。
5. 函数库的链接和包含文件
对于我们编写的任和一个程序,没有库函数的支持是不可想象的,而当我们要使用的头文件和函数库不在GCC默认的搜索路径下的时候(例如OpenGL、Qt、KDE、Boost等),我们就需要手工来告诉GCC他们的位置。
先来看头文件路径的指定。我们可以利用-I<dir_name>来指定我们希望GCC去搜索的头文件目录,例如我们要使用X11的程序,我们就要使用下面的选项
再来看库函数的设置:我们通过-L<dir_name>和-l<lib_name>两个命令行选项完成任务。其中-L用于告诉GCC在<dir_name>中去寻找函数库,而-l选项则告诉GCC使用用户指定的程序库。在Linux中,函数库的命名是遵循UNIX约定的,即lib{lib name},例如libsocket.so,所以当你需要告诉GCC使用这些库的时候,你就可以使用-lsocket选项。通常,这两个命令是结合在一起使用的,例如引用X11程序库的时候,我们可以这样:
–L/usr/X11R6/lib –lX11
另外,GCC在默认情况下使用共享库来链接程序,而当你想链接静态库的时候,一定要使用-static选项,例如-lncurses -static
在这一部分的最后,我们对编译时用到的GCC常用命令做一个简要的总结
命令 | 说明 |
-x <language type> | 显示指定输入文件的格式 |
-c | 编译和汇编源文件,但不链接,输出为.o文件格式 |
-S | 编译源文件,但不汇编,输出为.S文件格式 |
-E | 只对源文件进行预处理,并不编译,输出为经过预处理的源代码 |
我们可以利用上面的-x和-c / –S / –E的组合来控制GCC的整个编译过程,其中-x用于告诉GCC我们从哪里开始,而-c / -S / -E用来告诉GCC在那里结束。 | |
-o output-file | 用来指定输出文件,该选项可以指定很多种输出文件,例如:可执行文件、目标文件、汇编文件或者是预处理过的程序代码等,这要根据具体的命令行参数而定。当然,GCC还提供了默认的-o选项,其中,默认的可执行文件是a.out,目标文件是<file_name>.o,汇编文件是<file_name>.s,预编译头文件的格式是<file_name>.suffix.gch |
-I<dir name> | 告诉GCC在<dir name>中去寻找头文件 |
-L<dir name> | 告诉GCC在<dir name>中去寻找库文件 |
-l<lib name> | 使用名为lib<lib name>.so的程序库 |
-static | 通知GCC链接静态库 |
上面,我们提到了关于GCC编译的常用命令,这里另外补充一些帮助性的常用命令,他们可以让你对GCC的基本配置和使用作一个了解。
命令 | 说明 |
-v | 向标准错误打印编译GCC时使用的命令和预处理器和编译器的编本,如果你在升级GCC时举棋不定,那么不妨在你的控制台上使用这个选项,看看厂商的配置 |
--help | 向标准输出打印GCC命令行选项的描述。如果把这个命令和-v结合起来,--help则会同时打印被GCC调用的进程的命令行描述。如果把-Wextra和—help结合起来,那么,那些没有文档描述的命令行选项也会被显示出来。 |
--target-help | 向标准输出打印每一个工具的特定命令行选项的描述 |
--version | 现实GCC的版本和版权信息 |
在这部分的最后,我们来谈一谈关于构建软件时链接参数的设定问题。在上面的第5部分我们已经提到了,函数库的使用是需要-L和-l一起配合来使用的,但实际上,往往一个像样的程序需要很多库的支持,例如,如果你需要编写一个GTK程序,我们需要下面的链接参数:
-L/usr/lib -L/usr/X11R6/lib -lgtk -lgdk -rdynamic -lgmodule -lglib -ldl -lXi -lXext -lX11 –lm,看上去有些吓人,你可能会问,我如何知道需要这些呢,如果我想编写KDE的程序呢,还有OpenGL呢?其实,情况比你想象的要好很多,在/usr/bin目录下,有很多名为xxx-config的脚本,它们的作用就是向用户显示编译链接程序时使用的参数的。这些脚本可以接受一些参数,比较常用的有—libs用于列出链接特定程序时使用的程序库,另外--cflags用于生成头文件的包含目录,也就是上面我们提到的-I参数。于是,对于GTK程序,我们可以使用下面的命令来编译:
gcc gtksource.c `gtk-config –libs --cflags`
当然,为每一种程序写一个config显然不是一个好办法,目前新的开发包都使用pkg-config这个脚本来生成链接参数。你可以使用pkg-config –list-all查看pkg-config支持的所有链接参数
当你在上面这份列表中查到了自己想要程序包时,就可以使用下面的命令来编译程序了
gcc <source file>.suffix `pkg-config <pkg name> --libs --cflags`
让GCC帮助你更好的工作
上面我们简单介绍了GCC的常用命令行选项,其实GCC的功能比上面提到的那些要丰富得多,GCC对代码的警告、优化、调试等方面提供了丰富的支持,下面我们就从一些例子来看看GCC提供的这些功能。
1. 对问题代码提出警告
GCC对程序代码提供了完整的检查功能,由于C/C++语言本身的特点,很多错误都是程序员无意间犯下的,例如使用了未定义的变量、在bool表达式中使用了=而不是==等等问题,利用GCC提供的代码检查功能,我们可以让编译器为我们找到这些问题,避免运行时发生灾难。
首先,我们来看一个“问题代码”
/* test_warning.c We use this file to check the warning facilities provided by GCC*/
#include <stdio.h>
#include <stdlib.h>
void main() { /* main should return int*/
int a, b;
long long l = 2.2; /* long long type is GNU extension, not standard ANSI / ISO type*/
miss_decl(); /* We call an undeclared function*/
if (a = 0) /* May be we want == here instead of =*/
printf (“a really equals to 0?/n”);
if (b != 0) /* We used uninitialized variables*/
/* %d and “We should put b here” don’t match*/
printf(“We make a mistake again! b = %d/n”, “We should put b here”);
};
void miss_decl() {
/* /* This type of annotation is prohibited*/
printf(“We should put the declaration before it’s been used!/n”);
}
上面这些代码故意制造了很多编程中出现的常见问题,接下来,我们就用这段代码来检测一下GCC提供的各种常用的警告设施。
首先,我们不使用任何警告设施编译上面的程序
gcc test_warning.c –o test_warning
默认情况下,GCC会给出输出,其中GCC识别出了main函数不标准(warning)以及使用了未声明的函数(error)两个问题,但是其他的GCC并未察觉。
1. 利用-pedantic找出不符合ANSI / ISO标准的代码
执行下面的命令:gcc –pedantic test_warning.c –o test_warning
可以看到,这次GCC以警告的形式报告了代码中long long的使用,但是要说明的是我们并不能依赖这个选项来保证我们的代码完全符合ANSI / ISO标准,因为该选项只报告ANSI C要求编译器进行检察的内容。另外,你还可以使用-pedantic-errors让GCC把所有的警告都变成错误。
2. 利用-Wformat检查printf中的参数不匹配问题
执行下面的命令:gcc –Wformat test_warning.c –o test_warning
3. 利用-WComment找出注释中的错误
执行下面的命令:gcc –WComment test_warning.c –o test_warning
4. 利用-Wparentheses查找bool表达式中的=错误
执行下面的命令:gcc –Wparentheses test_warning.c –o test_warning
5. 用-Wuninitialized查找未初始化变量的使用
执行下面的命令:gcc –O –Wuninitialized test_warning.c –o test_warning
值得说明的是,在使用这个选项的时候,一定要配合上-O(后面我们会提到)选项
6. 利用-Wimplicit-function-declaration / -Werror-implicit-function-declaration检查未声明函数的使用
执行下面的命令:gcc -Wimplicit-function-declaration test_warning.c –o test_warning
另外-Werror-implicit-function-declaration和-Wimplicit-function-declaration作用是类似的,只是如果你使用了未声明的函数,前者会把它认为是一个错误。
7. 如果你只是想对你的代码进行全面的检查,你大可不必把上面的选项一并列出来,GCC提供了-Wall选项,含义就是列出所有代码中的警告
执行下面的命令:gcc –Wall test_warning.c –o test_warning
8. 如果你想走另一个极端,也就是不想让gcc输出任何警告,那么使用-w选项,该选项禁止所有的警告
执行下面的命令:gcc –w test_warning.c –o test_warnin
<输出结果>
对于上面所有的选项,你都可以把它们和-Werror选项一起使用,这样就可以把所有的警告都变成错误。另外,如果你只是想对代码进行检查而并不执行编译的话,可使用-fsyntax-only选项,像下面的命令这样
gcc –fsyntax-only test_warning.c
基本上来说,我们常用的一些警告选项就是这些,而其中-Wall更是我们极为常用的功能。
2. 优化选项
这一部分的内容可以分成两部分,一部分是让编译器对代码进行分析后,进行的代码优化,另一部分是我们可以为编译器制定一些关于硬件的信息,让他生成对硬件结合的更好的代码,而我们之所以要用源代码来编译程序,很多情况下,是出于这方面的原因。
首先来看代码优化,从代码的整体优化上,GCC提供了下面的选项
-O –O1
这两个选项的含义是一样的,GCC将执行减少代码尺寸和执行时间的优化,对于那些会严重影响编译时间的优化选项,这个级别的优化并不会执行。
-O2
在这一级别GCC将会提供所有支持的优化,但这其中并不包括以空间换时间的优化手段,例如编译器不会使用循环展开和函数内联。和-O相比,该选项进一步加快了编译时间和生成代码的性能。
-O3
除了-O2提供的优化选项外,还指定了-finline-functions,-funswitch-loops和-fgcse-afer-reload选项,目的只有一个就是全力执行代码优化。
-Os
这个选项是专门用来优化代码尺寸的,-Os打开了所有-O2级别中不会显著增长代码尺寸的优化选项
-O0
该选项代表不执行优化
在这里要说明的是,尽管GCC提供了1~3和s这4个整体优化选项,但从实际的优化效果上来看,往往O3优化出来的程序的效率并不是最高的,而大部分情况下我们都在使用-O2,如果你希望获得最高的效率利益,那么不妨这4个选项都试试。另外,其实这些选项只不过是GCC提供的很多单方面优化的一个组合,如果你想了解更为具体的优化内容,可以去查看GCC手册,出于篇幅限制,这里不细谈了。最后要记住的一点是,如果你的程序是用于高精度数值计算的,那么记住不要使用上面任何的优化选项。
下面来看基于硬件优化,由于这部分和计算机硬件相关,这里仅用Intel的CPU做一些说明:
对于所有为Intel和AMD x86-64提供的优化选项都是用m开头的,下面写一些常用的选项:
-march
该选项用来指定CPU的类型,常用的有i386 / i486 / i586 / pentium-mmx / i686 / pentium2 / pentium3 / pentium-m / pentium4 / prescott / k6 / athlon / athlon-4 / k8等等,读者可以根据自己的情况进行指定。
-mfpmath
该选项用于指定浮点运算单元的类型。包括
387
使用标准的数学协处理器
sse
使用SSE指令集提供的标量浮点运算。在Pentium3 / Athlon-4以及更新的芯片上支持这个特性。另外,在pentium4以及AMD x86-64处理器上,SSE2还可以进行双精度浮点计算。
sse,387
混合使用387数学协处理器和SSE指令集,该选项可以充分的利用CPU的浮点寄存器和xmm寄存器,但是该选项还处在试验阶段。
-malign-double
该选项使得GCC把double / long double / long long类型的变量在4字节或2字节地址上对齐,
在Pentium级的CPU上,这会使得代码的执行速度更快,当然带来的代价是需要更多的内存来执行程序。-mmmx –msse –msse2 –msse3 –m3dnow
这些选项用来启动内置函数直接使用这些处理器扩展指令的功能。在编译3D或多媒体程序的时候,使用他们是非常有效的。
3. 对调试的支持
当程序出错的时候,我们可以在Visual Studio中轻松的进行调试,而在Linux中,一旦出现Segmentation Fault,似乎我们除了用眼睛去看代码就没有更好的选择了,其实情况不然,用GCC向程序加入一些适当的调试信息,我们可以利用GDB去调试程序。在这里,我们介绍最为常用的-g和-ggdb选项。
先来看-g。该选项可以利用操作系统的“原生格式(native format)”生成调试信息。GDB可以直接利用这个信息。尽管我们可以把-O和-g放在一起使用,但是,这种做法是极为不推荐的。
如果你想用GDB来调试程序,那么你可以使用-ggdb来让GCC为GDB生成更为丰富的调试信息,但是,此时你就不能用其他的调试器来进行调试了。
最后要说明的是,上面这两个选项都可以接受一个输出调试信息的级别,默认的级别是2。如果你指定1级(-g1),那么GCC会生成最少的调试信息,这包括函数和全局变量的描述信息,但是对于局部变量和行号等信息,在这个级别是不会输出的。另外一个级别是3级(-g3),在这一级别上,GCC会为程序中的所有宏定义和符号生成调试信息。
小结
通过这篇文章,希望能过对想学习Linux开发中用到的一些基本的技术和知识有一个了解,并且能够自己动手开始做些试验性的工作,其实,这里还有很多问题没有谈到,例如利用GDB进行调试、利用make管理工程、利用autoconf为程序生成配置脚本、利用CVS管理程序源文件等等,这些问题有待在今后的文章中和读者一起交流。