Linux在很多人眼中是非常好的操作系统,不仅因为它的内核和函数库的完整源代码都是公开的,而且因为它拥有许多好用的程序开发工具。 下面就介绍几种常用的编程工具,熟悉这些工具对于开发Linux应用程序是很有必要的。当然了,像其它Linux程序一样,更详细的内容你能在man手册或info页中找到。
gcc是GNU提供的优秀的软件之一,其性能不亚于任何商业编译器。它具有惊人的可移植性,而它也号称其最优化功能非常强大。gcc功能实在太多(不信的话你试一下man gcc),这里无法一一列举,只能介绍一些常用的参数。
-o filename
:这可能是你最常用的一个选项了。它用来产生以指定名字的可执行输出文件。如果不指定文件名,gcc产生默认的a.out。键入./filename就可执行程序,查看结果。
$ gcc hello.c -o hello
$ ./hello
hello world!
提示:在使用gcc命令的时候不要用Tab键来补齐文件名,因为那可能会覆盖源文件。
-c
: 只编译不链接,产生以.o结尾的目标文件。当然它也可以同时编译多个文件。
$ gcc -c hello.c
$ls -l hello.o
-rw-r--r-- 1 wangcong wangcong 888 6月 11 23:20 hello.o
-g/-ggdb
: -g参数可以让gcc将一些调试信息加入目标文件中,而-ggdb专为gdb调试器产生足够的调试信息。如需用gdb调试程序应加上此项。注意:这会使目标文件大小增加,我们最好只在测试时使用这个参数,最后发行版中将其去除。
例如:
$ gcc hash.c -o hash -ggdb
-Idir
: 要求gcc在搜寻头文件时除了默认的目录/usr/include,也要到指定的目录dir中去找。
比如,编译test.c时要用到/home/myname/test.h,我们可以:
$gcc -I/home/myname test.c -o test
-llibname
: 尝试链接指定名为liblibname.a的函数库。注意-l选项的顺序是有意义的。
比如,test.c中用到了数学函数库,我们要链接libm.a,可以:
$ gcc test.c -lm -o p
-Ldir
: 让gcc也要在指定的目录dir中去寻找-l指定的函数库。
例如我们要链接/home/myname/libtest.a,我们这样做:
$gcc -I/home/myname -L/home/myname -ltest test.c -o test
-On
: 最优化选项。后面的n越大最优化程度越大。一般最常用的是-O2。-O0是不优化,这也是默认的。
$gcc -O2 -o foo foo.c
-Wall/-w
: -Wall将打开所有警告,而-w将关闭所有警告。在编译C程序时,强烈建议你加上-Wall选项,因为gcc给出的很多警告都是相当有用的。当然还有折中的选择,具体请参阅man手册。
-lefence
: 将libefence.a函数库链接上,给程序加上电子篱笆,可以用来检查内存泄漏。挺有用的选项。
-S
:用来产生相应的汇编源文件,默认的文件名是
filename.s
-ansi/-std=standard
:standard可以是以下内容:c89,c9x,c99,gnu89,gnu9x,gnu99等。其中-std=c89就相当于-ansi。而gnuxx相当于cxx加上gnu扩展。 当然了,它也可以用来调试C++,Fortran等其它程序。 另外,即使你编译时使用了-Wall选项,也建议你使用lint检查一下你的C程序,它能给出很多有用的信息。Linux常用的lint工具是
splint
。[12]对GCC做了全面的介绍,绝对是首先参考。
make是一个通用的工程管理工具,它可以“自动化编译”,极大地提高了软件开发的效率,无怪乎它被誉为“Unix发展的一个中流砥柱”。make的使用是根据预先设定的规则来运行的,这些设定的规则记录在一个文件中,即
makefile
。
make命令的格式是:
make [ -f makefile ] [ option ] ... target ...
-c dir
:将指定目录设为make开始运行后的工作目录。
-f filename
:将指定的文件作为makefile
-k
:使make尽可能地编译,即使命令调用返回一个错误,make也不会停止运行。这个功能很有用,例如,当需要移植的时候;你可以创建尽可能多的目标文件,然后可以在不用等待中间文件创建的同时,移植到不能创建的文件处。
下面简单地介绍一下makefile的基本书写规则。makefile基本书写规则如下:
target ... : prerequisites ...
command
...
target是目标文件,也就是你要产生的文件或一个标签; prerequisites是产生目标文件所需的文件列表;command是make需要执行的命令。第一个命令可以加“;”跟在倚赖文件列表后,也可以 另起一行书写。注意:当命令另起一行书写时一定要加tab键,否则make不认识。一行放不下时可以用“\”来续行,在一行开头用“#”开始注释。
例如,有下面一个简单的工程,其倚赖关系如下:
上图中,工程example倚赖于test.o 和main.o,而test.o倚赖test.h和test.c,main.o倚赖main.c和main.h。我们将这样写makefile:
example:test.o main.o
gcc -o example test.o main.o
main.o:main.c main.h
gcc -c main.c
test.o:test.c test.h
gcc -c test.c
但是,你会发现,我们编译完成后,test.o和main.o没用了,我们应该删除它们。 那么应该怎么做呢?我们可以加一个动作名字clean,让这个动作完成删除作用。
clean:
rm -f test.o main.o
要执行删除只要运行
$make clean
就可以了。但是这还不够, 因为我们也有可能想产生一个目标文件叫做clean,这就会造成冲突。为了避免如此, 我们增加一行:
.PHONY: clean
.PHONY是一个伪指令,向make说明clean不是一个文件。当计算clean依赖关系时,make都会忽略任何名为clean的文件。 make除此之外还有大量的高级功能,比如:宏变量,流程控制等。 如果你想了解更多关于make的知识,请看[5]。
其实,makefile并不好写,尤其是对于大型工程项目。我们可以使用imake,它是一个makefile的生成程序,利用了C的预处理程 序,你只要写一个imakefile文件,就能使用imake来产生一个makefile。当然,还有其它的makefile生成器,像
ICmake
,
automake
等,具体内容请阅读它们的相关文档。
gdb是一款优秀的调试器,对它熟悉了你就会发现它真的很好用。在使用gdb调试程序前,你应该确认编译时加上了-ggdb选项,那样才能产生更多的调试信息。
提示:虽然你可以同时使用-ggdb和-O选项,但是不建议你那么做,因为一些最优化功能会让调试变得复杂。
下面将通过一个程序展示gdb的使用方法。有错误的程序是test.c。
$ gcc test.c -ggdb -o test
$ gdb test
GNU gdb Red Hat Linux (6.1post-1.20040607.41rh)
Copyright 2004 Free Software Foundation, Inc.
...
This GDB was configured as "i386-redhat-linux-gnu"...
(gdb)
好了,现在gdb已经准备好接收命令了。
(gdb) l
1 #include < stdio.h>
2 int main(void){
3 int i=0;
4 for(;i<=10;i++);
5 printf("Now the value of i is:%d\n",i);
6 return 0;
7 }
l/list
命令的功能是列出源代码。如果后面带上行号,则会列出指定行号附近的代码。如果有多个文件的话,你也可以通过指定文件名:行号来列出对应的代码。
(gdb) b main
Breakpoint 1 at 0x8048384: file test.c, line 3.
b/break
是插入断点,后面可以是函数名,也可以是行号。
(gdb) r
Starting program: /misc/myprogram/test
Breakpoint 1, main () at test.c:3
3 int i=0;
r/run
命令是运行程序,直到断点处或程序结束。
(gdb)watch i
Hardware watchpoint 2: i
watch
是用来观察变量的变化的,一旦指定变量发生变化就停止程序。
(gdb) n
4 for(;i<=10;i++);
(gdb) n
Hardware watchpoint 4: i
Old value = 7216256
New value = 1
0x08048396 in main () at test.c:4
4 for(;i<=10;i++);
n/next
是执行下一行程序。i的值发生了变化,程序停了下来。
(gdb) p i
$1 = 1
p/print
用来打印指定表达式的当前值。
...
(gdb) c
Continuing.
Now the value of i is:11
Program exited normally.
c/continue
的功能是继续执行程序直到下一个断点处或程序结束。
(gdb) q
$
q/quit
是退出gdb调试程序。 调试完毕,如果你想把调试信息从执行文件中去掉而又不重新编译,可以使用命令:strip
filename
。
使用gdb我们可以很快找出错误。gdb还有很多高级功能,比如进行汇编级别的调试,而且可以和emacs搭配使用,是非常优秀的调试工具。[8]中对它进行了更全面的介绍。
有一些程序设计工具,它可以告诉你程序执行的效率,整个程序的调用结构,函数调用关系等。这样的工具真的很有用。gprof就是这样的一个效率分 析工具,它能产生一份详细的列表,列出程序执行的一些统计值,其中包括每个函数被调用的频率,被谁调用,所花费时间等。要使用gprof,在用gcc编译 时要加上-pg参数。这个参数会在目的文件中加上gprof所需的信息,也会将执行文件连接上支持性能分析的函数库。
你只要直接执行编译时加过
-pg
的程序即可,执行完毕后会在当前工作目录下产生一个gmon.out的文件,它记录了gprof所需的信息,我们可以用gprof来读取它。
$ gcc bintree.c -o bintree -pg
$ ./bintree
...
$ gprof bintree gmon.out
Flat profile:
Each sample counts as 0.01 seconds.
no time accumulated
% cumulative self self total time seconds seconds calls Ts/call Ts/call name 0.00 0.00 0.00 28 0.00 0.00 print 0.00 0.00 0.00 7 0.00 0.00 btree_insert ...
gprof的输出信息相当冗长,上面省略了很多。由于上面的函数执行太快,而gprof的计时单位相当粗略,所以那些函数的执行时间都显示0.00秒。gprof能为我们提供不少有用的信息。
另一个常用的计时工具是
time
命令,它可以精确地测量程序运行时间。使用它只需在其后面 加上要测量的命令即可。比如:
$time ./test>/dev/null
显示如下,分别是实际耗时,用户级别耗时和系统级别耗时:
real 0m0.056s user 0m0.014s sys 0m0.006s
它还能格式化输出,还能测量输入/输出,消息数目,非常方便。还有一个更简单的
calls
程序,它只显示源代码中函数调用的树状结构。而
strace
能够显示程序执行时用到的系统调用,具体请参阅相关手册。
ld是GNU的链接器,支持生成大量的可执行文件格式,它是一款复杂而快捷的工具。 ld有许多灵活的参数来控制其链接过程。这里只介绍主要几个参数。
如果你希望用example.ld作为链接脚本,可以这样使用:
ld -T example.ld
如果你想把一系列的.o文件链接到另外一个.o文件中,可以像这样用ld:
ld -r foo.o bar.o -o example.o
如果你希望ld生成一个Linux上使用的共享库,可以用:
ld -shared ...
如果你想去掉文件中的符号表等内容,加上-s选项。而-S选项则只去掉调试信息。
ld -s ...
如果你想把
entry
作为程序入口,可以:
ld -e entry ...
如果你想把.text段加载到0x0处,可以:
ld -Ttext 0x0 ...
查看帮助信息可以用:
ld --help
了解更多,请查看Steve Chamberlain的文章[6]。
如果你在维护包含很多源文件的一个大型项目,每次更新后都推出完整的源程序是不太可行的,所以最好就用patch程序来更新原来的程序。这样,当 你的程序升级时,你只要推出源文件对应的patch文件,其他人就能通过执行patch来使用那个patch文件,以获得最新版本的程序。
比如我们有一个程序foo,我们把它更新为bar,我们可以用diff程序这样产生patch文件:
$diff -u foo bar > foo.patch
-u
参数指定使用特殊的diff输出格式,否则得到的patch格式怪异,一般人都没法看懂。
foo.patch
就包含了描述foo和bar不同的信息,我们将用这个文件来更新。
提示:一定要注意diff命令后面的文件名顺序,一定是旧的文件在前,新的文件在后!
接下来,我们用patch来更新:
$patch foo < foo.patch
使用过某个patch文件后,若要再次使用该patch文件,patch程序会产生警告信息:询问是否以-R方式执行,-R将还原文件。这样很好,防止进行了误操作。当然,有时你想更新的是整个目录中的所有源文件,比如foo和bar两个目录,我们可以:
$diff -cr foo bar >foo.patch
$patch -p0 < foo.patch
diff的
-c
参数表示输出上下文格式,
-r
参数表示递归的比较两个目录。
-p0
告诉patch程序被更新的文件的路径名并没改变。
diffstat是一个很有用的工具,它可以列出patch 所引起的变更的统计(加入或移出的代码行)。输出关于patch的信息,执行:
$diffstat -p1 foo.patch
更多选项请参阅patch和diff的使用手册。 和diff类似的工具还有比较三个文件的diff3,合并文件的sdiff,简单比较的cmp,分行比较的comm等,这里就不一一介绍了。
CVS是非常优秀的版本控制工具,比较适合大项目的版本控制。甚至可以说没有CVS就不会有今天GNU的流行。GNU绝大多数的项目都采用CVS来控制版本。熟悉CVS对Linux程序员来说是非常有必要的。
CVS的使用很复杂,这里只做简单介绍。CVS管理着
repository
,里面存储着正式的源代码。每个在
repository
项目叫做
module
。开发者不能直接编辑
repository
中的文件,而是先要取出(
check out
),编辑完后再把源代码传送回
repository
(
commit
)。
首先,我们应该把环境变量
CVSROOT
设成我们想要存储
repository
的目录:
$mkdir /usr/local/CVSROOT
$export CVSROOT=/usr/local/CVSROOT
提示:不要把CVS的repository建在和你想要加入repository之中的项目相同的目录中。这会导致一个死循环。
储存目录建好后,我们就可以建立
repository
了:
$cvs init
然后要在
repository
中建立项目。在CVS中建立项目有很多方法,我们可以用下面的命令:
cvs import dir manufacturer tag
其中
dir
是项目所属的目录,
manufacturer
是作者名(设成别的也可以),
tag
是版本编号,比如说是
initial
或
start
。例如:
$cvs import example myname initial
一旦在CVS中建立了项目,开发者们就可以开始各自的工作了,他们互不影响。
提示:避免直接编辑CVS repository中的文件,这不符合CVS设计者的本意。CVS控制的文件被设置为只读来避免这种情况的发生。
比如有一个程序有一个名为
example
的模块,可以这样“取出”它的本地目录:
$cvs checkout example
CVS会把
example
所有的文件和目录取出,这样你就可以编辑这些文件了。在编辑完成后,你可以这样将改变的文件写回
repository
中:
$cvs commit -m “description of your changes”
你也可以只写入某个文件:
$cvs commit example.c
当然,在你修改时,很可能你的同事已经将他编辑的文件写入
repository
中去了。这时,CVS不会让你写入,而是要求你先更新再写入。可以这样来更新:
$cvs update
有时候,你想在项目中加入或删除某个文件,你可以这样做:
$cvs add extra.c
这就把extra.c加入进去。删除文件有点麻烦,你必须先在你的工作目录中把你想删除的文件删除,然后在使用下面的命令:
$cvs remove
这样虽有点麻烦,但可以防止你误删,有必要时还可以进行恢复。
当然,开源项目几乎都是放在网上的。所以CVS也可以允许你远程使用:
$export CVSROOT=:cvsserver:[email protected]:/cvs
$cvs login
...
CVS password:
...
$cvs checkout example.c
...
很多大的开源项目都是使用CVS进行版本控制的,要参与开源项目,非常有必要熟悉它。关于CVS更详细的介绍见[7]第13章。另外,RCS,Subversion也是同样出色的版本控制工具,[7]中对它们也作了介绍。 gCVS是CVS的图形化前端,使用非常简单。
objdump是一个不错的工具,它有很多功能,反汇编,显示调试信息,显示目标文件中的符号表或段信息等等。 而objcopy可以根据你的需要复制,翻译目标文件。
参见《瑞士军刀──valgrind》一文
cxref可以分析C源程序,为它输出一个交叉引用的列表文件,这个输出文件可以是普通文本格式, 也可以是HTML,Latex等格式,对阅读代码非常方便。cxref命令格式如下:
cxref filename [ ... filename] -options
不同版本的cxref选项不太相同,以我使用的1.5版为例,讲一下它的用法。其它版本请参考它自带的手册。
-Nbasename:指定输出文件的前一部分的名字,默认的是cxref。
-Rdirname:当要分析的C源程序分布在多个目录中时,用此来指定源代码树的根目录。
-Odirname:输出交叉引用文件到指定的目录。
-htmlxx:xx可以是20或者32,使用HTML2.0或3.2格式输出。
除了上面这些之外,Linux还有很多其它优秀的工具, 像著名的
vi
[4]和
emacs
[3]文本编辑器,词法分析工具
flex
, 编译器的编译器
yacc
,排版工具
tex
,
groff
等。 有的发行版还带了图形化的IDE,像
Qt
,
KDevelop
等。 Linux自带了不少相关文档,而且篇幅有限,这里不再全面介绍。Linux真的是一个不错的开发平台。
[1]Programming with GNU Software
, Mike Loukides and Andy Oram, O'Reilly
[2]Sed & Awk, Second Edition
, Dale Dougherty and Arnold Robbins, 1997, ISBN 1-56592-225-5, O'Reilly.
[3]Learning GNU Emacs, Third Edition
, Debra Cameron, James Elliott and Marc Loy, ISBN 0-596-00648-9, O'Reilly
[4]Learning the vi Editor, Fifth Edition
, L.Lamb, 1990, O'Reilly
[5]Managing Projects with GNU make, Third Edition
, Robert Mecklenburg, 2004, ISBN 0-596-00610-1, O'Reilly.
[6]Using LD, the GNU linker
, Steve Chamberlain.
[7]Linux in a Nutshell, 5th Edition
, Ellen Siever, Stephen Figgins and Aaron Weber, 2003, ISBN 0-596-00482-6, O'Reilly & Associates.
[8]
《用GDB调试程序》,CSDN文档中心
[9]GNU Automake
, David MacKenzie and Tom Tromey, For version 1.3, 3 April 1998.
[10]Sed - An Introduction
, Bruce Barnett and General Electric Company, 2005.
[11]Getting started with awk
, HMC Computer Science Department.
[12]GCC: The Complete Reference
, Arthur Griffith, McGraw-Hill