懒蜗牛同学搭建好了各种语言的开发环境,并且开发出了不少的HelloWorld。但他显然并不想就此罢休。凡事都喜欢刨根问底的懒蜗牛同学,还要搞明白一个软件从源代码到打包为成品的整个过程。
软件的最初状态自然就是源代码,要把C语言的源代码变成二进制的程序,离不开GCC。那么GCC到底对源代码做了什么,才能把一段段冰冷的代码变成一只只鲜活的程序呢?(为啥要用“只”......)
“2010年9月3日 降雨
学了这么几天,觉得还是C语言最有意思、最灵活高效。听说好多Linux系统的软件都是拿C语言写的,连Linux内核也是以C语言为主创造的。我能不能也写出一个自己的小软件来呢?不要求功能有多么复杂,但要符合Linux的精神------只做一件事,但要做到最好。
仅仅编译一个HelloWorld很简单,只有一个.c文件,只用一条命令。我应该设计些复杂的程序,实现点有意思的功能才会有进步。编个什么程序好呢?”
懒蜗牛同学之前写过一个HelloWorld程序,但是觉得那个程序实在太简单,没什么意思。于是这回他决定写个稍微复杂点的程序。
【无聊的rubbish 1号】
只见他冥思苦想之后,写下一个源码文件,叫做rubbish.c,看来这懒蜗牛同学还真谦虚。在rubbish.c文件中写了些代码后,懒蜗牛就叫来GCC进行编译,运行了这么个命令:
gcc ./rubbish.c -o rubbish
命令后面那个“-orubbish”的意思,就是指定编译后的二进制文件的文件名叫做rubbish。这样就不会每回都输出为a.out文件了。编译出来后,还冒着热气的rubbish就被懒蜗牛叫进内存运行起来。只见rubbish飞身跳进内存,跑进内存后抢过标准输出设备,对着那个设备大喊一声:“I am a Rubbish!”然后,就跑回硬盘继续睡觉去了。我说懒蜗牛同学呀,不是说设计个复杂点的程序吗?您这个rubbish跟那个HelloWorld有啥区别呀?
*提示:*标准输出设备,即/dev/stdout设备文件,一般该文件映射到当前字符终端。
我们后来管这个弱智的程序叫做rubbish_1号,因为懒蜗牛同学很快又创造出了很多的rubbish。
【同样无聊的rubbish 2号】
不一会儿,懒蜗牛又拿来rubbish 1号的图纸改起来。10分钟后,图纸完成,交给GCC编译,懒蜗牛很自觉地把这个程序命名为rubbish2。
gcc ./rubish.c -o rubbshi2
很快,rubbish 2号诞生!毫无悬念地,懒蜗牛马上让我叫醒rubbish 2号起来干活。于是我走进硬盘,叫醒rubbish 2号。只见rubbish 2号立刻飞身跳进内存,依旧是对着标准输出设备大喊一声:“I am a Ru~Ru~Ru~Rubbish~~~~!”喊完了就跑回去继续睡觉了。懒蜗牛同学成功地利用for循环创造了一个结巴。
【多个文件的rubbish 3号】
15分钟后,rubbish 3号的图纸再次毫无悬念地完成。这回的图纸不光是一个rubbish.c文件了,而是包含了3个文件:rubbish.c、input.c和input.h。这回懒蜗牛依旧是叫来了GCC,他运行这么个命令:
这就是当代码包含多个.c文件时候的编译方式。有人问,那个.h文件呢?.h文件一般是要被包含进某个.c文件的,这个包含的动作一般在预处理的时候就给处理了,不需要在编译命令里写上.h文件。至于什么叫预处理,您别忙,咱们一会儿就会说到。
rubbish 3号这会儿已经起床,跑进内存向懒蜗牛发问:“How many Ru do you want?”然后就等待懒蜗牛输入。懒蜗牛同学输入了一个6,于是就听见rubbish 3号大喊:“I am a Ru~Ru~Ru~Ru~Ru~Ru~Rubbish!”------程控结巴!
【系统调用的rubbish 4号】
rubbish 4号的图纸诞生啦。Vim告诉我说,这回懒蜗牛调用了创建线程的库函数(因为懒蜗牛是用Vim编程的嘛,所以Vim能知道写了些什么)。果然,在编译的时候懒蜗牛运行了:
其中,-lpthread的意思就是要连接创建线程相关的库函数。图纸交给GCC后,很快rubbish 4号诞生,并且很快就起床跑进内存。只见他念动咒语“唵木哒咪咪呀......分!”然后白光一闪,rubbish 4号变成了2个!2个4号同时喊:
“I am a Ru~Ru~Ru~Ru~Rubbish!”
“I am a Ru~Ru~Ru~Ru~Rubbish!”
二重结巴!
刚才咱们见到了编译一个(或一坨)C语言源文件时的操作,下面就仔细说说编译的过程。
在编译的时候,虽然用户用的只有一个“gcc”命令,然而我们说过,GCC是一个编译器套装。他不是一个人,不是只有一个“gcc”命令就能完成整个编译过程的!他们是一个团队,一个以“gcc”命令为首的源代码编译施工队。
施工队主要成员有gcc、cpp、as和ld这么4个命令。其中gcc命令是老大,其他几个干什么活都得听他的调遣,用户一般也只跟gcc命令打交道。当写好了图纸之后(也就是源代码,就比如刚才懒蜗牛写的rubbish.c),虽然用户直接把图纸交给gcc命令就不管了,但其实gcc命令需要去调动其他命令进行各种处理,才能完成编译工作。
(以下文中出现的小写“gcc”均代表gcc命令,而不是整个GCC开发套件。)
【施工第1步------预处理】
一般来说,gcc拿到图纸后,会首先叫来cpp进行预处理。
预处理主要就是将文件里的宏定义进行展开。什么是宏定义呢?人类用户一般都比较懒,或者说,人类能力有限,不愿意写很多重复的,相似的东西,就把这些都定义成宏。比如,这么写:
这就是定义一个叫做TOTAL\_NUMBER的宏,从名字看,这个宏代表了一个总数,数值是18353226。那么以后再要用到这个总数的时候,就直接写TOTAL\_NUMBER就可以了,不用写那一大串数字。而且,如果总数变了,只要在最初#define的位置修改一次就可以,反正就是为了偷懒。
cpp的任务就是把这类的宏定义都替换回去。把所有的TOTAL\_NUMBER都替换成18353226;把所有#include引用的文件内容都粘贴进来等。这么说可能不形象,那咱们找个实例吧,比如有一个hello.c文件,内容是这样的:
那么经过了预处理之后的内容是什么样呢?如果您想体验一下,可以自己试着运行:
“-E”参数的意思就是让gcc只对main.c进行预处理。这样处理后的结果会直接输出到标准输出,不方便查看。所以我们需要进行输出转向,把输出的内容存进hello\_cpp.c文件里。打开这个文件你就会发现,仅仅几行的程序,预处理后变成了800多行!
*提示:*gcc是调用cpp命令来进行预处理,因此也可直接用cpp命令进行预处理,运行以下命令:
效果与“gcc –E”相同。
这800多行就是因为这段代码包含了stdio.h文件,stdio.h文件又包含了很多其他的.h文件,于是在预处理的时候,cpp就把这些.h文件全部粘贴了进来(但其实都没什么用,我们只用到了其中的printf()函数)。
预编译后的文件大约就是下面这个样子:
当然,这里面省略了很多。我们主要关注的是cpp将我们定义的TOTAL\_NUMBER这个宏,替换成了数字18353226了。
这样经过了cpp预处理之后的文件,就该交给gcc去编译了。
【施工第2步------编译】
编译又是什么意思呢?
最初的图纸,也就是没有经过预处理的源代码,那是人写的。一般懂相关语言(比如C语言)的人都能看懂。预处理之后的文件,虽然不那么直观了(TOTAL\_NUMBER看着是不是比18353226直观?光写个18353226还以为是谁的QQ号呢),但终究只是做了下替换,还是人类可以看懂的。而经过编译之后的代码是什么样子呢?还以刚才那个hello.c做例子,运行:
“-S”参数就是告诉gcc,只对文件进行预处理和编译的步骤。这样最终会得到一个hello.s文件,这个文件里就是经过了cpp的预处理,并由gcc进行编译之后的代码了,大约是下面的样子:
这样的代码,就不是普通人类可以看懂的源代码,而是只有终极牛人才能读懂的汇编代码了。汇编代码比较贴近底层的机器码,里面描述的都是一些基本的操作,以机器的思维来描述整个程序。
打个比方。就比如描述切黄瓜的过程,用C语言描述出来就像是“将黄瓜切片”,这么一句就搞定了。要是用汇编,那就是:“左手扶住黄瓜,右手拿起刀,移动刀到黄瓜顶部,刀落下,刀抬起。刀向黄瓜后部移动3毫米,刀落下,刀抬起。刀再向黄瓜后部移动3毫米,刀再落下,刀再抬起。放下刀,走出厨房,走进卧室,找到创可贴,贴在左手食指上......”总之,汇编是一种面向机器的,很复杂的程序设计语言。gcc的任务就是把C语言的源代码转换成贴近机器语言的汇编代码,为下一步as的工作做好准备。
【施工第3步------汇编】
as拿到汇编代码后,要对这样的代码再进行处理,得到真正的机器码。这个处理的过程,也叫汇编。
如果说进行汇编之前的汇编代码是终极牛人才能看懂的,那么经过as汇编之后,得到的机器码压根就不是人能看懂的了。而且从.c的源码文件一直到汇编之前的.s文件都是文本格式的,进行汇编之后就成为二进制的elf格式了。这就不是普通的文本编辑器可以打开的了,需要用专门的软件将其转换为16进制数据查看。转换过来之后大约就是如图8.1所示的样子。
图8.1 汇编后的文件
如果你想看看汇编后的机器码,那么可以运行命令:
“-c”参数的意思大概您也猜到了,就是告诉gcc,只进行预处理、编译、汇编这3个步骤。这样运行之后会输出一个hello.o文件。如果想查看这个文件,运行:
就可以看到一堆乱七八糟数字的机器码了。汇编程序中至少还有些操作的助记符,比如什么add,mov之类的。寄存器也是有名字的,比如叫eax,叫rbp等。但是到了机器码,这些都没有了,都换成了各种各样的数字,半句人话都没了。还以切黄瓜为例,要是用机器码来描述,就相当于说:“用32号设备扶住87号物体,24号设备拿起126号物体,移动126号物体到87号物体顶部,做2635号动作,再做2636号动作......”
【施工第4步------链接】
好了,现在终于得到机器码了,机器码按说就是可以执行的代码了。但是,这时候的程序还是不能直接执行的,为什么?因为还有ld没有出场呢,他的工作叫:链接。
经过预处理、编译、汇编之后的二进制代码,按说已经是机器码了,可以直接运行。但是这里得到的机器码并不完整。就比如刚才说的这个hello.c文件,得到的机器码只是针对这个hello.c文件里面所写的这么点内容的。而这个hello.c里面还调用了printf()函数,这个函数是在系统的标准输入/输出库里面实现的。这部分负责真正向屏幕上打印字符的机器码如果不包含进来,这个程序怎么可能正常地实现功能呢?所以,就需要把这段标准库中的机器码和我们编译出来的机器码“链接”起来。
而且很多时候,一个程序不是一段机器码,而是由很多段机器码组成的。这些机器码分别保存成很多的.o文件,最终也需要把它们都“链接”起来才可以运行。
这时候就需要ld出场了。
ld负责把这些机器码组装起来,并且写明了各段代码的地址、从哪里开始执行之类的标记。就像我们造个机器人,脑袋、胳膊、大腿之类的都做好了,ld就是负责组装的。
经过ld组装的程序,就可以运行了。整个编译过程是不是挺复杂的?不过所幸我们的用户并不需要一步一步地手动做这些步骤,只运行gcc命令就全搞定了。
程序编译出来只是第一步。编译好的程序不一定靠谱,可能会有各种各样的错误,这就需要进行调试。调试有很多方法,但肯定离不开调试工具。这回,懒蜗牛同学就要学习调试工具的使用了。
“2010年9月18日 变天
终于又到周末了。今天在自己的机器上学习编程,逐渐开始入门了。这一阵子最大的收获是学会了自学。通过网络、论坛搜索,也能学到不少有用的东西。像学编程吧,开发环境的搭建、循环结构、文字输入甚至创建进程,都尝试过了。看来编程也不是那么的困难嘛。不过我也知道这只是刚刚入门而已,真的写出个能用的程序和随便写个小程序玩玩还是有很大区别的。
今天写了个程序,不知道为什么,总是运行不起来。以前能够运行的程序可以用printf()函数打印出变量来看看,这个运行不起来的程序怎么调试呢?”
今天起床的时间似乎比平时晚了点,全体起床之后,懒蜗牛同学按部就班地叫来OO老先生记录下一些文字,之后又继续去创造他的rubbish系列程序去了。
【19号扰乱秩序】
这一阵子,我们的懒蜗牛真是笔耕不辍------哦,不对,应该是键盘不辍才对------先后制造了18个rubbish程序。不过很多都被蜗牛删除了,只留有几个:一个是8号。因为8号比较憨厚,性格温顺,不爱打架。还有一个16号。16号很安静,不爱多说话,有点冷漠,但是做起事情来一丝不苟,严格地执行命令。再就是17号很厉害,但是很自大,高傲,总跟别人发生矛盾。他好像和18号还有点什么关系,具体的我就不知道了。最后的18号是个美眉,长得很可爱,一头金发,本事也不错,我们大家看她都很顺眼。而现在懒蜗牛正在制造19号。不知道为什么,我总觉得即将到来的19号将是一个邪恶的坏家伙......
没过多长时间,19号出炉了。只见他起床之后,跑进内存,刚说了几句我是rubbish 19号之类的话之后,就开始乱动别人的东西。一会儿要去狐狸的内存空间里拿数据,一会又要往心有灵犀的地盘里存东西。当然,他的这些企图都没有得逞,要是连这样的小流氓都管不了,我还叫内核么。
【内核严明法纪】
我们这个工作间里面的空间管理是很严格的,谁的空间谁用,别人不能乱动。像19号这么目无法纪,影响他人工作的软件是不能容忍的。眼看着这邪恶的19号,和满工作间无辜受害的软件们,我终于忍无可忍!为了工作间的安宁,为了我稳定内核的荣誉,为了爱与正义,为了世界和平,为了部落,我代表月亮,我,我消灭了你!转眼间,手起刀落,只听咔嚓一声------整个世界安静了。19号被我斩为两段,然后我向懒蜗牛汇报:很遗憾,您的程序出现了段错误,就像图8.2显示的这样(因为他被砍成两段了,所以错误了)。
图8.2 程序出现段错误
懒蜗牛似乎有些不明所以,不知道这个段错误是怎么回事(因为太血腥了,所以我没直说是因为被我剁成两段)。于是就赶紧叫来狐狸上网上查去。通过搜索知道了,段错误的情况有很多(很多种不老实的程序都会被我砍成两段),但大致上都是由于内存指针使用不当引起的。比如没有给指针赋值就去使用,或者虽然赋值但是访问越界等。总之就是动了你不该动的内存就会段错误。
可是到底这个19号是如何动了别人的空间的呢?他到底为什么要去访问非法的地址呢?这些情况虽然我们内存里的软件们看得一清二楚,铁证如山,但是懒蜗牛他不知道啊。他没法钻进内存里来看程序是怎么运行的。那么懒蜗牛能有什么办法看清楚19号的一举一动呢?这时候就需要我们的软件修理工GDB闪亮登场了。
GDB是GNU Debugger的缩写,也就是GNU调试器的意思。它和GCC一样,最初也是由Richard Stallman设计并实现的。图8.3是GDB的吉祥物------一条鱼(不要问我为什么,问Stallman去)。GDB是一个字符界面的调试工具。用过VC的同学应该知道,在那里面调程序的时候可以进入debug模式,能够查看内存、单步执行之类的。我们Linux中,每个软件秉承着“只做一件事情,但做到最好”的原则,将调试这件事情交给了GDB来完成。
图8.3 GDB的吉祥物
【编译出可被调试的程序】
GCC编译出来的程序可以通过GDB来运行,运行的时候,就可以执行设置断点、单步运行、查看变量、查看堆栈等操作。有了GDB,懒蜗牛同学就可以监视程序在内存里面的一举一动了。
不过GDB并不是像狗仔队那样想监视谁就监视谁。像狐狸啦,gedit这样的成品程序是不能被监视的。要想让某个程序被GDB监视,必须在制造他的时候------也就是编译的时候,留出给GDB控制的接口来,GDB才能监视那个程序的一举一动。您看过黑客帝国么?我们机器里的普通程序,就像是里面正常的自然人。而可以被GDB调试的程序就像Matrix世界中的人一样,脑袋后面有个接口,可以接进去控制。那么怎么给程序装这么个接口呢?很简单,就是在编译的时候加上参数“-g”,类似这样编译:
懒蜗牛运行了这么一句,就创造出了脑袋后面有接口的rubbish 19号。之后就可以叫来GDB去调试他了。
*提示:*没有加“-g”参数编译出来的二进制文件也可以被GDB调用并运行,但由于该文件中没有记录机器码与C语言源码的对应关系,因此无法进行设置断点、查看变量、查看源码等操作。
【用GDB调试程序】
编译出了可调试的程序后,就要叫来GDB来运行它,像下面这样:
于是,GDB就会接到命令,赶快掏出各种仪器和工具,并把19号拖进内存里(这时候19号可还没醒哦)。然后从一个大机器上抽出一个长长的电缆,插进19号脑袋后面的接口里,一切准备好之后,向懒蜗牛报告:“一切准备就绪,可以开始了。”也就是打印出下面这样的信息:
进入这个界面,就可以进行调试了。当然,要想调试,需要了解一下GDB的一些基本的指令。我们简单介绍几个常用的。
run命令(或者简写为r)------这个命令很好理解,就是从头开始运行程序。像刚才懒蜗牛运行“gdb ./rubbish19\_debug”后,进入了GDB调试环境,但是并没有自动运行起19号这个程序,需要run一下才行。
break命令(或者简写为b)------这个命令用来设置断点。例如“break 12”的意思就是在程序源码的12行设置断点。这样程序运行到这一行就会停下来。
list命令(或者简写为l)------列出当前程序源代码。遇到问题了总得看看源码吧,或者设置个断点什么的也得看着源码才知道应该断在哪行。那就用这个命令查看源码就可以了。
continue命令(或者简写为c)------这是继续执行的意思。程序遇到断点停下来以后,可以用这个命令继续执行下去,直到碰到下一个断点,或者结束。
print命令(或者简写为p)------这个命令用来打印变量的值。比如print i,意思就是打印出变量i当前的值。
examine命令(或者简写为x)------这个命令用于查看指定内存地址中的数据。例如examine 0x12345678的意思是查看内存中,地址为0x12345678位置所存放的数据。
*提示:*examine命令只能查看当前被调试程序能够合法访问的地址。
next命令(或者简写为n)------这是单步执行的命令。程序遇到断点停下来之后,执行这个命令可向下执行一句代码。
懒蜗牛同学好像是之前看书学习过了,虽然据我所知这是他第一次使用GDB,但是似乎还挺熟练。
进入GDB调试环境后,懒蜗牛输入了r并且回车。于是,GDB按下一个电钮,19号的身体跟着腾地一下站了起来,启动了。插着电缆的19号像他上次启动后一样,还是要去骚扰狐狸妹妹,访问人家的内存空间,于是我也只好再次举起屠龙刀,再次将其一刀两断。
GDB赶快向懒蜗牛报告:19号同学在按照您的图纸进行某某动作的时候,由于侵占他人内存空间,触犯了内存管理条例第287条,因此被处以“断刑”。然后还指出了19号的这种行为在懒蜗牛的图纸中所在的位置,也就是代码行数,类似下面这样:
懒蜗牛一看,rubbish.c文件的第9行就出问题了,太伤自尊了,可是这第9行也看不出什么不对的来。应该是这个指针有什么问题,还是从头看看程序吧。
于是懒蜗牛输入了list命令,GDB赶紧把19号的整个图纸------也就是全部程序的源代码,打印了出来。程序一打印出来,懒蜗牛才看明白:这个buffer指针压根就没有初始化嘛,只是做了声明而已,也没给申请内存空间,就这么用,那不出问题才怪。
前面向您介绍了懒蜗牛创造的rubbish 19号程序,这家伙由于不遵守《Linux系统内存管理条例》被我干掉了。那么我们Linux的内存管理原则是什么呢?都有什么规矩呢?下面就来说说。
咱们说过,一个程序工作的时候需要用到的内存分为几个部分:代码段、数据段、BSS段和堆栈段。其他的段,基本上程序一启动就确定好了,没什么可说的,也没太多要管理的。唯独这个堆栈里面的堆空间,是程序运行起来之后动态申请的,需要我来管理。
【空间的申请和释放】
堆空间是指程序在起床运行后向我申请来的空间,也是一个程序占用得最多的空间。一个程序如果要想使用工作间里的空间,要向我提出要求,说需要多大多大的一块内存空间------这个过程叫做申请。然后我根据工作间里的情况来分配,告诉他,哪块哪块归你,然后这个程序就去用了。
这时候那块地方就单独给这个程序使用,不许别的程序访问了。如果别的程序胆敢来访问这块空间,就像你去人家偷东西一样,必须依法剁成两段(偷东西没这么大罪过吧......)。
那么这块内存空间分给这个程序使用之后就永远给他了么?想得美!你买房还只有70年产权呢,这么珍贵的内存空间怎么可能永久分给一个程序。申请到内存空间的这个程序在做完了相关的事情,不再需要这块空间的时候,他应该跟我报道,说空间我用完了,这块地方可以再给别的程序用了------这个过程叫做释放。
【内存泄漏】
一个有知识有道德有理想的程序,在他回硬盘睡觉以前应该释放掉所有他申请过的空间。如果遇到哪个不良程序,无德青年申请了很多空间不释放,我也没有办法。因为这不像越界访问那样,访问了别人的地址就是访问了,人赃俱获,无法抵赖。而人家不释放空间,也许是因为他一会儿还需要用这块空间呢,我没法证明他申请的空间用不着了。我唯一可以做的就是在他回硬盘睡觉的时候,检查所有他申请过的空间,如果有没释放的,就强制释放掉------你都睡觉去了,你申请的空间肯定用不着了嘛。
*提示:*Linux在程序退出之后将回收程序申请的所有资源,包括内存空间、Socket连接、设备访问等。
但是如果这个程序是个长时间运行的后台服务程序,并且还不断地申请新的空间而不释放,那就麻烦了。内存空间会被他一点一点地消耗光,这就是最烦人的内存泄漏。在我们软件界,内存泄漏是和瓦斯泄漏同样严重的事故。《Linux系统内存管理条例》第3条明确写着------禁止申请不释放!就在第4条禁止抽烟的上面(当然不能抽烟,内存都冒烟了机器还能用么)。
【Windows与Linux对内存使用的不同理念】
其实不光是Linux,Windows里的程序一样需要遵守类似的原则,估计他们那里大概也有个什么《Windows系统内存管理办法》之类的文件吧,反正大家的原理是一样的。不过对于工作间的使用,Windows和我还是有点不同的。
Windows总是喜欢尽量留出空间来,好给新起床的程序用。可是我总觉得,作为一个系统,我怎么能知道用户还有什么程序要运行呢?要是没有程序要来了,工作间里还空那么大地方,不让正在工作的程序用,那不是浪费么?所以我还是习惯尽可能地把东西都搬进工作间里。除了程序们申请多少内存就尽可能给多少之外,剩下的部分,我就把一些可能会用到的库、命令等统统都搬进来,能占多少占多少。
有人问:要是你把这里边都占满了,待会儿有程序要进来咋办?很简单啊,我再搬出去呗!
程序要用空间,也不是一下子都占满吧,他也得把他的东西一点点搬进来。他往内存里搬的时候,我就往外搬,不耽误。所以,当某个程序启动,跟我说:“我要10平米的地方放东西。”的时候,我就先答应他说:“好,你就往那边那10平米放吧。”然后在他往内存里搬数据的时候,我再去给他清理那10平米的地方。也可能他要10平米,但是暂时只用了2平米,那我就先腾出2平米来,等他再要我再腾。他们管我这个方法叫Copy-onwrite。
但是Windows就不同了。可能是因为他们普遍比较胖的缘故吧,都比较懒,不愿意搬来搬去这么折腾。基本上Windows只是在必须用啥东西的时候才把那东西搬到内存里,让内存留出尽可能多的空闲空间。这样,当有程序向他申请内存的时候,就可以用手一指:那块地,归你。然后就不用管了。内存实在不够用的时候就找个比较闲的程序,命令他:你,去硬盘里先忍会儿。
*提示:*图形界面中,可以通过“系统”|“系统管理”|“系统监视器”来查看内存使用情况,如图8.4所示。
图8.4 系统监视器
在字符界面中可以通过“free”命令查看内存使用情况。但需注意“free”命令显示的结果中,第1行为计算了内核缓冲后的使用率。这个使用率一般很高,多数空闲内存都被用于缓冲。“buffer/cache”一行显示的,才是真正的应用软件所占用的内存,如图8.5红线处所示(free命令显示结果默认单位为KB)。
图8.5 free命令
简单的程序直接用GCC编译就可以了。但是如果程序越来越大,源码文件越来越多,再手动调用GCC来编译就显得费事了。有什么好办法呢?
“2010年10月10日 起风
今天好孤独,节后的只有一天的周末还要独自来加班。领导总是不顾员工的死活,就像剥削劳动人民的包工头一样。
不过今天加班的时候倒是抽空完善了我的rubbish程序。现在这个程序已经有16个.c文件了。每次编译的时候敲命令还真麻烦。还好Linux可以记住最近运行的几条命令,不用每次都输入。不过就算这样也挺累的,有没有更好的办法呢?”
最近硬盘里的rubbish越来越多,已经排到31号了。虽然这些程序都不大,31个加在一起也就几兆的大小,可关键是这帮程序大都不着调,不是段错误就是内存泄漏,要不就是乱访问设备。这么多不着调的程序在内存里折腾,指不定什么时候就出事了。
【文件太多,一起编译费时费力】
这些rubbish程序,除了不着调以外,个头越来越大,源码也越来越复杂。从rubbish 1号只有一个.c文件,发展到现在,rubbish 31号一共有10多个源码文件。每次懒蜗牛同学编译rubbish 31号的时候,都要这样:
每次看着他输入这么长一串的命令,我真替他累得慌。并且每次懒蜗牛这么一编译,施工队那哥儿几个就都得跑过来,把这一大堆.c挨个打开,开始从头施工,一个一个编译。有时候懒蜗牛只是在ai.c里面修改了很少的一点,编译的时候施工队的同志们也要从头到尾地重新编译每一个文件。
这就好像盖个楼,完工之后开发商说:一楼这个大门的门把手图纸上画错了,应该用圆的,怎么画成方的了?改了吧。施工方一听,赶紧下令:“图纸画错啦!把楼炸了重新盖!”虽然这样对于拉动GDP发展有很好的作用,但毕竟属于精神不正常的范畴。
【分别编译,一起链接】
那么我们的GCC施工队为什么做这种很抽风的事呢?这不怪施工队,这是因为懒蜗牛输入的命令就是让他们拆了重盖的意思(也就是让他们从头开始编译),他们只是严格执行命令而已。实际上完全不必这样,那些.c文件中,改了哪个只编译哪个就可以。那应该怎么操作呢?
(1)首先,源码写完了之后先各自编译成模块,运行:
这个命令的意思就是把这些.c文件各自编译成.o文件,如rubbish.o、input.o等。
(2)要获得最终的二进制文件,只要再运行:
就生成了最终的rubbish 31号。
(3)那么之后如果修改了某个源文件------比如修改了ai.c文件,只需要重新编译这一个文件就行了,就运行:
这样就编译出了新的ai.o。
(4)得到新的ai.o之后再运行:
重新链接一下,新的rubbish 31号就完成了。这样就省去了重新编译那些没有改动过的文件的时间。
不过这样编译虽然节省了编译的时间,但是敲起命令来也挺麻烦的。有没有更方便的方法呢?当然有。
【大项目需要规划】
其实用户完全不必每次都敲一大长串的gcc命令来编译程序。如果是那样,我们Linux内核有上千个文件,要是编译一次,光敲命令就得敲一上午。
那个GCC施工队毕竟只是个施工队,你要是盖个小厨房,垒个猪圈,这样的小东西直接找他们没问题。直接一编译:“gcc砖头-o猪圈”就出来了。可如果要盖个CBD商圈,里边什么银行、商场、写字楼、炸油条的、卖臭豆腐的、修理自行车的等,一应俱全,这么大的一个工程,你光叫个施工队来肯定搞不定。这得有人进行合理的统筹规划,设计施工方案,然后再让施工队去具体施工。这个规划的人谁呢?按照懒蜗牛现在的做法,这个规划人就是懒蜗牛自己,但他自己又没这本事,怎么办呢?这时候他就需要专业的规划人,能够指挥施工队的包工头------make。
【make的重要作用】
make也是一个程序,像上面说的一样,他就是负责控制整个施工过程的(也就是编译过程)。对于比较小的程序,就一两个.c文件,根本用不着make出马,GCC施工队去编译就行了,因为源文件的结构关系不是很复杂。可是对于稍大一点的程序,像狐狸妹妹、心有灵犀、星爷啊,基本上所有常用的软件,都足够复杂到需要make来对编译过程进行管理。
如果软件大了,编译的时候就不能简单地把一大堆.c的源文件统统一次性编译成一个二进制文件,这种方法太粗鲁了。应该像上面介绍的那样,把一堆.c文件编译成一堆.o文件,然后再把.o文件链接成一个成品的二进制文件。有改动的时候只更新单个的.o文件就可以。
*提示:*不一定非要把每一个.c文件编译出对应的.o文件,可以几个.c文件生成一个.o,一切根据具体代码的需求来设计。
但是这个过程如果由人类来负责,就不那么靠谱了。他们的大脑不可靠,不一定能记清楚这次改了哪些文件,应该更新哪个.o文件。于是,make就义无反顾地挑起了这个重要的担子。当然make也不能靠凭空的想象就来指导包工队干活,什么事情总得有个规划,make也需要一份施工的规划书,这份规划书就是Makefile。
Makefile,顾名思义,就是make用的file。这就相当于一份施工的规划,上面写着整个工程分为几个模块,先用哪几个文件编译成一个什么什么.o,再用哪几个文件编译出一个.o,再怎么怎么一链接,最后得到编译好的二进制程序。
make就根据这份文件来指导GCC他们进行施工。当有某个.c文件被修改之后,make能够根据文件的修改时间智能地判断出哪些模块需要重新编译,重新链接,然后就去让GCC重新编译那些改过的文件,最终生成新的二进制程序。
有了make,写好了Makefile文件,就省去了用户敲一大堆编译命令的烦恼。只要敲一个make命令,其他的,就交给make去做吧。他办事,你放心。
【简单的Makefile示例】
比如说,有这么一个工程,包含了3个文件(咱就不拿懒蜗牛同学的rubbish系列打比方了,源文件太多,还乱)。分别是:main.c、part1.c、part2.c。那么我们就可以试着写出一个用于编译这个工程的Makefile如下:
乍一看可能您不太明白,没关系,咱们慢慢说。当用户运行make命令的时候,make就会来到当前目录下,首先在这个目录里查找Makefile文件,如果没有,就找makefile文件(Linux区分大小写嘛)。如果都没找到,就报错。如果找到了,那就打开Makefile看看该干些什么。
【Makefile中的基本书写格式】
首先我们看到,这个Makefile大致分成了4段,每段的格式都差不多。我们把它总结为这样的格式:
对照着来看,先看第1段:
这段的“目标”是all,all是一个make的关键字,当用户运行make并不加任何参数的时候,make就会来Makefile里找到目标为all的这一段,并且从这里开始干活。
然后,这段的“原料”是main.o、part1.o、part2.o这3个文件。也就是说,要想达到all目标,需要先有这3个文件。于是make就会查找现在是否有这3个文件,一看------没有!
没有没关系,make会继续往下找,这回的目标,就是查找怎么才能搞到main.o文件,结果一找,还真有,第2段就是:
这段的“目标”就是make正要找的main.o,于是赶紧看看原料,是main.c文件。这个文件已经有了。那么怎么用这个原料加工成main.o?看方法:gcc –c main.c。哦,原来运行这条命令就行了啊,于是make就会去调用gcc,来编译出main.o。
*提示:*“加工方法”一行的前面有且必须有一个Tab制表符,不能顶格写,也不能用空格代替Tab。
有了main.o,make再回去看第1段,发现还需要part1.o,part2.o,跟main.o的处理方法一样,根据3、4两段就可以编译出来了。原料都齐全了之后,make就再根据第1段的“加工方法”运行“gcc main.o part1.o part2.o -o mybin”,生成了最终的目标。
另外,刚才说了,如果不加参数,make就去找“目标”是all的段落。其实用户也可以通过参数指定make的目标,比如用户运行:
意思就是去完成Makefile里面,“目标”是main.o的那段任务。于是make就根据Makefile里的记录,只编译出一个main.o来。
*提示:*如果Makefile中没有“目标”为all的段落,并且运行make没有指定参数,则make会执行Makefile中的第1个段落,无论目标是什么。
【根据时间决定动作】
当某一个.c被修改了之后,用户应该如何编译呢?简单,还是只执行make就可以了。剩下的事情,就交给make去做吧。
make会检查每一个“目标”和“原料”的最后修改时间。比如part1.c文件被修改了,那么make就会发现,part1.o的创建时间要早于part1.c的最后修改时间,这说明part1.o需要被重新编译。于是他就会按照这一段的“加工方法”,再次运行gcc -c part1.c,来编译出新的part1.o。
那么现在part1.o的创建时间又比最终的mybin新了,于是make又根据第1段的加工方法执行了gcc main.o part1.o part2.o -o mybin,把新的part1.o和没有变动的main.o、part2.o链接成了新的mybin文件。
【多种多样的“加工方法”】
上面的例子里,加工方法一行基本都是编译或链接命令。其实,宪法里并没有规定“加工方法”必须是跟gcc有关的。其实加工方法这一行写任何命令都可以,并且不一定只写一行,写几行都可以,只要是挨着就行。比如一般的Makefile里都会有类似下面这样一段,用于清理编译结果:
这一段的目标是clean,没有原料。用户执行:make clean的时候,make就会找到这一段,并且发现不需要原料,于是直接执行“加工方法”,于是就删除了所有.o文件和最终的编译目标文件。于是整个源代码的目录里恢复到了编译之前的样子。
与此类似的还有make install,一般就是下面这个样子:
也就是在确认当前目录下有mybin这个编译好的文件后,把这个文件复制到系统中的相应目录,就完成了安装。
一说到make,很多人都记得编译源码包的时候,经常在进行make之前还要运行“./configure”命令,这个命令又是干什么的呢?
“2010年11月15日 晴
这个make工具果然方便啊。听说Windows下的VC其实也使用了类似的东西,只不过把它们都用图形界面封装了起来,所以我看不到了。看来还是在Linux下学习编程才能了解到一些本质的东西呀。
我还看到很多以源码包发布的软件,都会有一个configure脚本,成功地运行了这个脚本以后才能去运行make命令。这个脚本又是干什么的呢?”
懒蜗牛同学最近是越来越不着调了,竟然想把他最新写的那个rubbish 1115号发布到网上去!祸害我们一个系统还不够,还要残害多少青春懵懂的Ubuntu啊。
【rubbish 1115号------放到哪都是个祸害】
其实那个rubbish 1115号也干不了啥正经事,主要就是能陪懒蜗牛同学玩“猜拳”的游戏,所以才这么受宠。说来也怪,我们软件源里面有那么多游戏,懒蜗牛都没兴趣,不知道为什么就对这么个石头、剪子、布的随机函数情有独钟。不过也难说,毕竟是他自己编的嘛,谁的孩子谁不爱呢。可是您自己喜欢那就偷偷摸摸自己玩就行了,干嘛非要发布到网上让他去祸害别的电脑呢?
这家伙经常申请了内存不释放,有时候还假死,如果是不明所以的人用了这个程序,没准还抱怨我们Linux系统不稳定呢。当然,发牢骚归发牢骚,懒蜗牛的命令我们还是得执行,Firefox就正忙着把rubbish 1115号传到网上去呢。
过了一会儿,狐狸妹妹忍着笑就过来了:“你知道懒蜗牛怎么发布他那个程序么?懒蜗牛直接把编译出来的二进制文件贴到了论坛里面。哈哈,他以为这样直接就能运行呢。笑死了。”
【二进制程序------不是放到哪都能运行】
嗯,看来懒蜗牛同学还有很长的路要走啊。这个二进制文件看上去就是单一的文件,但其实他运行起来是需要很多库文件来协助的,不是拿到哪都能运行的。在他们Windows界其实也是这样:Windows 98的程序直接拿到Windows XP下不一定能运行;Windows XP的程序也有很多装不到Windows 7上。但是由于他们的版本比较少,而且系统的各种库和接口等都比较统一,所以也有不少的绿色程序直接复制到系统里就能运行。因此很多人觉得程序就是一个EXE文件,复制到哪里都可以运行。
懒蜗牛写的这个程序,如果复制到一个和我们相同的系统上,肯定可以运行(也肯定可以造成内存泄漏和假死,哼哼)。可是不一定别人的系统就跟你的系统一模一样啊!尤其我们Linux,发行版五花八门,就算相同的发行版,版本不一样,也不一定能行。如果系统里没有这个程序所依赖的那些库,这个程序肯定是运行不起来的。要想知道一个程序依赖于哪些库,可以用ldd命令来查看。
【查看依赖关系------看他到底在哪能运行】
我趁懒蜗牛不注意的时候叫来ldd,运行了一下ldd ./rubbish1115,看看这个软件都依赖什么。ldd向我汇报如下:
看来依赖的还不是很多,算是最基本的了,可照样不能随便放到别的系统上运行啊。比如这个“ld-linux-x86-64.so.2”,明显只有64位系统才可以。“libc.so.6”虽然是个系统就有,但是版本也得合适,不合适也不行。这也是为什么Linux上发布的软件好多都是源码包的原因,因为系统环境实在是各式各样,还是把源码放到目标系统上编译一遍更方便。然而我这些话也就跟您说说,懒蜗牛听不见我说话的,所以他还是把那个rubbish金刚直接贴到网上了。
几天之后,懒蜗牛就收到了应有的回应。
“这个程序在我这运行不了啊?”
“楼主我用SUSE啊,你编译出个SUSE版本的来吧。”
“楼主再好好看看吧,我这里运行不了。”
“我是32位机啊,运行不了这64位的。楼主提供个32位的版本吧。”
......
懒蜗牛同学终于意识到二进制文件不是放到哪都能执行的,可要让懒蜗牛去给每个人编译一个针对他们系统的版本也是不大可能的。好吧,那就发布源码!
可是问题又来了,源码发布的软件包应该是什么样子呢?为了避免再次体现自己的无知,懒蜗牛从网上下载了一个lynx2.8.7.tar.gz软件包。这个Lynx是一个字符界面的浏览器,懒蜗牛倒不是想用它,主要是这个软件体积不大,依赖也不多,正好拿来体验一下源码包的安装。
【要装源码包,先打开看看】
像“.tar.gz”这样的源码包是下载软件时最经常遇到的了。另外还有“.tar.bz2”格式,跟“.tar.gz”的没什么区别,只是压缩格式不同而已。就类似于一个是RAR,一个是ZIP。那么这样的软件包怎么装呢?当然是先把包解开再看了,得先解开压缩包看看里面是什么内容才能知道怎么装啊,就像我问你RAR包怎么装,你能知道么?
“.tar.gz”格式的文件,就像是一个邮局寄来的包裹。你收到一个包裹后怎么办?当然是先打开啦!先找剪子、小刀之类的工具把包裹拆开,然后看看里面有什么东西,根据里面东西的不同来决定怎么处理。里面要是家里寄来的松子核桃之类的特产,就赶快吃了;要是比较难吃的松子核桃什么的,就跟同事分着吃了;要是部手机,就赶快拿出来试试;要是下面还有把手枪,就赶紧拿刚才那手机报警。
【解压TAR包的工具】
这些大概不用我说,智力正常的人都应该知道怎么做。其实TAR包也是如此。拿到一个TAR包之后,先用你的工具把TAR包拆开。工具是啥?有道是解铃还须系铃人,TAR打的包,当然还用TAR来解了。一般解压一个TAR包的命令是:
这里,xzvf是tar命令的参数,我们分别解释一下。
x------参数x意味着要做解包的动作,与之相反的是c,也就是打包的意思。
z------意思是这个包是用gzip压缩过的,需要先调用gzip解压。
v------显示解压的过程,也就是打印出解压出来的文件。如果没有这个参数,则在解压过程中没有任何输出。
f------指明要解压的文件。
另外还有一些常用的参数,也顺道介绍一下。
j------意思是这个包是用bzip压缩过的(也就是.tar.bz2格式),需要先调用bzip2解压。
c------这个参数的意思是要做打包的动作,和x参数相反。
C------注意这个参数是大写的C,用于将解压缩后的文件存放到指定目录。如果没有这个参数,则默认解压到当前目录。
当然,也可以用那个叫做文档管理器的家伙,他的中文名字叫归档管理器,他的英文名字叫file-roller。不过其实他只是个负责用图形界面和用户交流的家伙,真正干活的还得是tar。
*提示:*tar命令后面的参数顺序并没有特定要求,但要确保文件名紧跟在f参数后。如“tar–vzxf xxx.tar.gz”,“tar –xvzf xxx.tar.gz”都是正确写法。
TAR包解开后,一般会得到一个目录,里面有很多的文件。然后干什么呢?有的同学记起来了,看看里面的东西啊。
一般包里面应该有个README文件。文件里写着这个软件是干什么用的、怎么安装、怎么用、作者是谁、何方人士、爱吃什么、身高多少、腰围裤长等信息。也可能安装的方法写在一个叫做INSALL的文件里。总之,应该有相应的文档文件来告诉你这个软件怎么装。不过也有时候软件的作者不厚道,或者忘性大,没有写README或者INSTALL文件。或者文件是有,但是没说清楚到底怎么装,那怎么办呢?只好去给作者写个E-mail鄙视他了。
懒蜗牛把下载来的软件包移动到了他的家目录下,然后运行:
把这个压缩包解压了出来。解压之后是一个目录,叫做lynx2-8-7。懒蜗牛继续运行:
这样就进入了刚刚解压出来的目录里面,用ls看了一下,发现有很多文件:
其中有个叫做INSTALLATION的文件,里面很显然应该写着安装方法。于是懒蜗牛同学打开这个文件看了看,内容很多。不过关于安装,他看到这么几行:
看来是运行这样两个命令就可以了。于是,懒蜗牛同学按照说明,运行了:
这个configure是干什么的呢?
【施工之前,先勘察好环境】
我们知道GCC施工队听make包工头的指挥,make包工头根据Makefile安排工作。这样,如果想把一堆源码编译成二进制的程序,只要执行一下make。执行之后make会在当前目录下寻找Makefile,然后按照上面写的方案,指挥施工队:在这盖个大裤衩,在那盖个水煮蛋,再在中间垒个鸟窝。然后施工队按照命令一点点施工,直到最终完成任务。
然而事情有时候并不是那么简单。没准包工头make下达建设大裤衩命令之后,施工队回来报告:这地方挨着鞭炮厂啊,盖大裤衩还不烧着了?包工头说:那先盖水煮蛋吧。施工队又报告:这地方常年干旱,地下水位也低,这点水连泡面都不够,别说煮蛋了。包工头只好说:那就先盖那鸟窝,总行了吧?施工队再说:鸟窝倒是能盖,就是这地方不通天然气,点不着窝里那火炬啊。
【分析师出马】
遇到这些问题,都是由于开工之前没有对施工的环境、现有的材料进行合理分析导致的。那么我们的这个configure,就是能够解决这种问题的一名分析师。
configure跟make不一样,他并不是常驻在我这里的软件,而是每个源码发行的软件自带的一个脚本。简单点说,铁打的make只有一个,流水的configure每个软件一个。
有了configure之后,编译软件的步骤就多了一步------./configure。让这个分析师首先开始工作,他会检查当地的情况,有什么材料、什么库、什么编译器之类的,都检查一遍,然后因地制宜地设计一份Makefile。如果有足够的水,才允许煮蛋;有远离火种的安全空间才能晾裤衩等。如果条件不满足,configure就会报告错误,告诉用户这里缺少什么,等用户想办法弄齐了再来编译。如果条件满足可以施工,configure就会出一份Makefile。注意,一般configure调查前,目录下是没有Makefile文件的(当然,没有configure的情况另说)。
懒蜗牛运行./configure之后,得到了如下输出结果:
这中间省去了几百行,无非就是“checking for xxxxxx… yes/no”这样的格式。这些输出的意思,就是说configure在对我们系统进行检查,报告有什么材料,没有什么材料。如果不是必需的材料,没有也就没有了。如果是必需的东西没有,那么confiugre就会报错并停止。
最终,我们这个系统里的东西还比较全,configure发现可以施工,于是就生成了Makefile文件。
*提示:*Makefile文件中可以引用另一个Makefile文件,因此一个软件工程中,经常可以看到不同源码目录下都有一个Makefile文件。
生成了Makefile文件,于是懒蜗牛同学就运行make命令:
make开始工作,指挥着GCC施工队进行编译。由于软件很小,马上就编译完了。编译完成之后,就在当前目录下生成了一个叫做lynx的二进制文件。不过如果就这么放在这,运行起来很不方便,所以懒蜗牛同学继续运行了:
这才把这个软件安装在了我们系统里。
*提示:*如果要删除源码安装的软件,可以在源码目录下运行“make uninstall”。作为一个标准的GNU软件,生成的makefile中应该都包含有uninstall的定义,但实际有一些不规范的软件,没有提供uninstall的方法,就只能手动删除了。
【照猫画虎】
有了这么一番感受之后,懒蜗牛知道了一个源码发布的软件包的大概样子。于是照着人家这个软件包,对比一下自己的这个软件。一看,Makefile是现成的,只要再增加一个configure脚本,检查一下系统中有没有gcc之类的编译工具及库文件等就可以了。
要写这么个脚本,需要用到Shell脚本编程的知识。懒蜗牛之前虽然学过一点,但那都只是些皮毛。现在要真去写configure脚本,还真有点力不从心。怎么办呢?对,照猫画虎!看看那个Lynx的configure怎么写的,跟着学就好了。懒蜗牛想得挺好,叫来gedit打开configure一看就傻了------洋洋洒洒连注释带语句一共36379行!这得看到哪年啊!
前面介绍到tar命令,基本上每个Linux系统都会带着这个软件,我这里也是。这个软件是干什么的呢?
tar就是个打包裹的,不过他可不是邮递公司的那种,不会把打好的包扔来扔去。他的能力有点像Windows 7那里的WinZip,他能把很多文件和目录收拾在一起,打成一个包裹,也就是生成一个tar包文件。
*提示:*过去的计算机使用磁带作为长期存储数据的介质(现在依然有使用磁带作为存储介质的场合),tar命令最初就用于将数据打包存储在磁带上。tar就是Tape Archive(磁带归档)的缩写。
可是跟WinZip不一样的是,tar只管打包,不管压缩。原来那些零碎的小文件有多大,打成tar包之后还是多大,只是变成一个整个的文件了而已。有人说,那我想压缩怎么办?别急,我这里还有另一个软件,叫gzip。这个软件就是专门负责压缩和解压缩的,但是他只能压缩单个文件,不能像WinZip那样能压缩一个目录里的很多文件。
这样,tar和gzip就成黄金搭档了。要想实现WinZip那样的功能,就得tar和gzip联手协作。比如有个目录叫aaaa,里面有好几十个文件,总共有10 MB大小。想要压成ZIP那样的压缩包,那就先让tar出手,把aaaa目录打成一个包裹文件------因为gzip只能压缩单个文件嘛。这样tar就把这个目录打成了aaaa.tar文件,这个文件还是10 MB大。然后由gzip出场,把这个文件压缩,压缩完了得标明一下啊,所以就又把文件名改了,叫做aaaa.tar.gz,表示这个文件经过了gzip压缩。这时候这个文件就小了,可能5 MB,也可能7 MB。有时候还有叫xxx.tgz的包,也是一个意思,只是把.tar.gz的扩展名合并了而已。
configure脚本也好,Makefile文件也好,其写法都是有一定规律可循的。并且他们的内容都是有一定复杂度的。对于有规律还复杂的东西,就可以想办法让程序自动实现。
“2010年12月20日 回冷
总算是把代码发布到网上了。很多热心的网友提出了不错的建议和意见。才发现我写出来的程序真的很白痴。经过了这么一个过程,确实在编程方面长进了不少,了解了很多以前不了解的事情。
还有件最不了解的事,就是configure脚本。好几万行的代码啊!牛人们是怎么写出来这么复杂的脚本的?而且好像很多源码包里的configure脚本都差不多,难道都是一个专门给别人写脚本的大牛写的?不解中......”
懒蜗牛的rubbish 1115号放到网上去之后,网络上的众多牛人们,为他修改了很多问题,把他调教得规规矩矩的。现在的rubbish 1115号,也不浪费内存了,也不乱改文件了,也不死机了,腰不酸了,背不疼了,现在我们都开始喜欢这个家伙了。他好,我们也好。这大概就是开源的力量吧。我们现在都不好意思叫他rubbish了,直接叫1115号。
【3万行的脚本真不是人写的】
1115号发布前的那段时间,我们几个软件都很纠结,担心懒蜗牛运行他,担心系统被他搞坏。那时候懒蜗牛也很纠结,纠结的是怎么能够把他发布到网上去。
那时候懒蜗牛整天研究Shell编程的技术,天天对着那3万多行的configure代码,出神地看着,嘴里念叨着:“这是哪位神仙大姐写的脚本啊......这么多行得写多长时间啊......”直到有一天,他终于将眼睛聚焦在了configure文件前面的那段注释中的一句话:
懒蜗牛顿时如醍醐灌顶一般,看着这一行注释,看着这个“Autoconf”,心里反复地呼喊,声音越来越强烈,直到终于爆发,脱口而出:“原来这脚本是用软件自动生成的啊!”顿悟之后懒蜗牛立刻叫来狐狸,本着“内事不明问老婆,外事不明问Google”的宗旨,直奔www.google.com而去。
【3万行的脚本到底是谁写的】
一番查找后,懒蜗牛终于大致了解了Autoconf这个软件。
咱说gcc、cpp、as和ld他们4个命令就像施工队,make就是包工头,configure就是分析师,那这个Autoconf大概就是市政规划局了。有了他,什么Makefile,configure脚本,全都不用自己写,都由他一手代办。
规划局的工作,就是根据源代码的结构和组成,来决定如何根据环境,因地制宜、就地取材地施工,最终派出一个专门的分析师------也就是configure。之后在安装的时候,configure就可以根据目标系统的环境及既定的几套施工方案,来写出合适的Makefile,再交给那里的make去指导施工。
虽然软件叫做Autoconf,但其实并不是只有他一个人。既然叫做规划局,那就不可能是一个人,你见过哪个地方的规划局就一个局长了?他们这规划局成员有4个:Aclocal、Autoconf、Automake、Autoscan。要想自动创建Makefile和configure脚本,就得跟这哥儿4个说。虽然可能你已经自己写了Makefile了,只是缺少configure。但你写的那个不行,他们向来是买一送一,搭配销售。你写的Makefile是没用的,必须得用他们创作的configure和Makefile,具体情况咱们待会儿再说。
这4个人各有各的工作,各司其职:Autoscan负责检查源码目录结构,看看都有哪些需要编译的文件;Aclocal用于检测一些编译环境相关的内容,例如使用哪个编译器;Autoconf负责生成configure脚本;Automake负责生成Makefile的蓝本------makefile.in。
经过一番查找,懒蜗牛同学已经了解了Autoconf这一组软件的使用了,现在他要开始为他的程序加入configure脚本和Makefile了。
首先,懒蜗牛来到存放1115号源码的那个目录,目录里现在有main.c、board.c、ai.c、board.h和ai.h几个文件(1115号已经被网络上的热心爱好者们改装得很精简了)。然后懒蜗牛运行了这个命令:
那么“autoscan”这个命令是干什么呢?
【Autoscan的职能】
Autoscan是负责初步审查项目的。你的工程图纸画好了,得先拿给他看。他看了一遍之后,会给你写个报告。怎么还写报告?当然了,规划局嘛,审批个这处理个那的,不都是部门之间报告来、报告去的么。Autoscan写的这个报告叫做confiugre.scan。
但是这个Autoscan写的confiugre.scan报告,基本上是驴唇不对马嘴。Autoscan这家伙,要论本事,抬举他点说是一般。图纸他都不一定看得懂。所以多数情况下,还得动手改改。改过的报告还得改个名字,叫做configure.in。
正说着,只见Autoscan同学大摇大摆地来到1115号的源码目录,东瞅瞅,西看看,挨个打开每一个文件,终于搞清楚了各个文件之间的关系。然后他按照一套很官方的格式,写了初步审核报告书,存了个文件名叫做configure.scan的文件,之后就回去睡觉去了。报告书的内容大约是这样:
【修改报告】
懒蜗牛拿到报告书,当然知道,这只是万里长征才走完了第一步。赶紧叫来gedit小弟,修改报告书。他把一些完全不着边际的东西删掉,或者注释掉(也就是在那一行的前面加上#号)修改后的报告书是这个样子的:
看着挺多,其实真正有用的就下面这么几行。
AC\_INIT(main.c)------这一句说明这个工程的主要图纸是哪个文件。
AM\_INIT\_AUTOMAKE(rubbish1115,1.0)------这一行是汇报这个项目的名称,叫做rubbish1115,版本是1.0版。
AC\_PROG\_CC------这一句是说,最终的configure需要检查C语言编译器是否正常。
AC\_OUTPUT(Makefile)------这一行是说明,最终的configure需要产生的文件,叫做Makefile。
其他的,都是废话。
懒蜗牛同学把改好的报告改名为configure.in,然后就去叫Aclocal来看报告了。也就是运行了这个命令:
【Aclocal的职能】
Aclocal负责复查Autoscan的报告,并且根据里面的内容,做一些详细的注解和说明。并且把这些注解和说明也写成一个报告,叫做aclocal.m4。
这里所谓的说明,主要是针对configure.in中的一些宏定义,进行详细的阐述。比如configure.in中写的“AC\_PROG\_CC”,只是说明了在最终的configure脚本中,要加入检查编译器的部分。但是编译器怎么个检查法?检查哪个编译器?都没说明白。这些在Aclocal的报告中都会有详细的解释------编译器要使用GCC,configure脚本中,要检测系统是否有GCC编译器。
随着懒蜗牛按下回车键,只见硬盘中的Aclocal不紧不慢地起床,伸着懒腰走进工作间,拿起桌上的茶水杯,掀开杯盖,撇一撇浮在水面上的茶叶,拿嘴吹一吹,喝一口,盖上杯盖,放下杯子,开始看报告。扫了两眼后,就知道是怎么回事了,也不用懒蜗牛多说话,直接写了一份复查报告,叫做aclocal.m4,扔给了懒蜗牛,然后就赶紧回去继续睡觉去了。要说人家Aclocal的办事效率还真是不错,毕竟人家写的aclocal.m4不用懒蜗牛修改,直接就可以往上交了。
主要领导终于出场了,懒蜗牛赶紧又叫来Autoconf,让他指派分析师configure。也就是运行了这个命令:
【Aotuconf的职能】
Autoconf就是专门负责指派分析师。他看了两份报告后,一般会沉思一会儿,说说目前如何困难,人手不足之类的话。最终在用户一再的苦苦哀求,以及威逼利诱之下,无可奈何地说:好,就给你派个分析师吧!如果顺利,一个configure脚本就诞生了。
不过懒蜗牛遇到的Autoconf这回倒是没太耽搁,看了configure.in和aclocal.m4两份报告后,很快就生成了configure脚本。由于懒蜗牛同学的这个程序很简单,因此configure脚本的内容也只有4千多行,不过麻雀虽小,五脏俱全,跟正规的configure脚本没有差别。
那么有了configure脚本就完事了么?当然没有!都给你介绍了规划局有4个人,第4个还没出场呢,怎么能完呢?
这个Autoconf生成的configure要想去工作,是有条件的。他必须搭配规划局制定的施工计划------makefile.in才能工作。有人会问,这个makefile.in是什么啊?我已经有了Makefile还要他干什么?咱不是说了么,你的那个Makefile,甭管写得多么天花乱坠,也是白搭,人家规划局派出来的configure根本都不会瞧上一眼,人家configure要写自己的Makefile来用。你又得说了,那你这configure就赶快写出来自己的Makefile啊。你看你,不讲道理了不是,这Makefile文件那么复杂,哪能就这么凭空写出来,总得有个参考,有个蓝本,有个全市统一Makefile模板之类的东西吧。这个模板,就是makefile.in。那么这个文件从哪来呢?这就用得着Automake了。
【Automake的职能】
Automake的职能就是专门写configure需要的makefile.in(您看咱规划局给您搭配得多好)。不过Automake也不能直接就把makefile.in写出来。人家比较忙,这一点您也得理解。局里那么多重要的事情,今天学习,明天会餐,后天考察什么的。就算不会餐不考察,谁也免不了上个网、偷个菜、斗个地主扫个雷吧?
所以,Automake是没工夫从头给你写出一份makefile.in的,你得先给Automake写好一个框架,然后人家才好动笔。这个框架,就叫做Makefile.am。有了这个框架,交给Automake,他就可以给你写出makefile.in了。
【编写Makefile.am】
懒蜗牛同学没有打听好这个步骤,他直接找来Automake,让他写makefile.in。
Automake拉着长声说:“这个......我们这里呀,工作也比较忙嘛......要按说呢......这个文件我是应该给你写滴。不过我们这里每天这么多人来,我要是一个一个写,哪天才能写完呀。所以同志啊,为了帮助我们提高办事效率,也为了你自己早点拿到Makefile的蓝本,更为了我们能够早日实现共同富裕奔小康------您是不是自己先写个草稿给我,我也好帮你赶快写出蓝本呀。”
懒蜗牛听得都快扔板砖了,心说不就你想犯懒这么点事么,至于跟我废这么多话么。赶紧动手写草稿吧,这个草稿叫做Makefile.am。好在内容很简单:
第1行,是行业规定,一般都这么写。
第2行,说明编译之后的程序应该叫做rubbish1115。
第3行,说这个工程包括main.c、ai.c、board.c这3个文件。
*提示:*第1行的AUTOMAKE\_OPTIONS=foreign是Automake的选项。Automake主要帮助开发GNU软件的人员来维护软件,所以在执行Automake时,默认会检查目录下是否存在标准GNU软件中应具备的文件,例如NEWS、AUTHOR、ChangeLog等文件。设置为foreign则Automake忽略掉对这些文件的检查。
就这么简单,草稿就写完了。之后再把Automake叫出来,总算是给写出了Makefile的蓝本------makefile.in。
做完了这些之后,这个工程就可以打包发布了。用户拿到这个包,解开之后,就直接依次运行“./configure”、“make”、“make install”就把软件安装上了。
懒蜗牛欣喜地看着自己整出的这个像模像样的软件,看看configure脚本,4千多行!心想:不知道的人看见这个脚本一定以为我是大牛吧,哈哈哈。再运行一下configure,看看生成的Makefile,500多行,哈哈,俨然感觉自己已经成为高手了一样。于是,在懒蜗牛的YY中,我们结束了那一天的工作。
咱们的懒蜗牛同学创造了不少的rubbish之后,算是对Linux下面的软件开发有了深入一些的了解了。什么编译原理、Makefile,还有怎么使用configure,怎么使用Autoconf,全都体验了一下。
从这以后,懒蜗牛算是更理解这”乌棒兔”系统了。理解它的开源;理解它的简洁;理解它的效率;理解它的灵活。从此,懒蜗牛和”乌棒兔”幸福地生活在了一起......