(024) Linux之编译程序

十年运维系列之基础篇 - Linux

作者:曾林 

联系:1494445739@qq.com

网站:www.jplatformx.com

版权:文章未经同意请勿转载


一、引言

      本章将介绍如何通过源代码来生成可执行程序。开放源代码是Linux自由开源的必要因素,整个Linux系统的开发依赖于开发人员之间的自由交流。对于多数桌面系统用户来说,编译已经是一门失传的艺术。编译技术虽然曾经非常普遍,但是如今,版本发行商却维护着很大的预编译二进制库,以便用户下载使用。

      那么,为什么要编译软件呢?有如下两个原因:

  • 可用性:尽管有些发行版已经包含了版本库中的一些预编译程序,但不会包含用户所有可能需要的应用程序。这种情况下,用户获取所需要软件的唯一方式就是编译源代码。
  • 及时性:虽然有些发行版专注于一些前沿的程序版本,但是多数并不会。这就意味着想要获取最新版本的程序,编译是必不可少的。

 

二、什么是编译

      简单来说,编译就是一个将源代码(由程序员编写的人类可读的程序描述)翻译成计算机处理器能识别的语言的过程。

      计算机处理器(或CPU)在一个非常基础的层次上工作,只能运行称之为机器语言的程序。而机器语言其实就是一些数值代码,它描述的都是一些非常小的操作,比如“增加某个字节”、“指向内存中某个位置”或“复制某个字节”等,并且每一个这样的指令都是以二进制(0和1)的形式表示的。最早的计算机程序就是用这样的数值代码编写。

      汇编语言的出现解决了这一问题,因为它用诸如CPY(复制)和MOV(转移)等更容易的助记符取代了那些数值代码,汇编语言编写的程序会由汇编程序(assembler)处理成机器语言。如今,汇编语言仍然用于某些专门的编程任务,诸如设备驱动和嵌入式系统等。

      之后便出现了高级语言编程,被称为高级语言是因为它们可以让程序员少关注些处理器的操作细节而把更多的精力集中在解决手头的问题上。

      高级语言编写的程序通过编译器转换为机器语言。有些编译器则将高级语言程序转换为汇编语言,然后再使用一个汇编程序(assembler)将其转换为机器语言。

      经常和编译一起使用的步骤就是链接。程序执行着许多共同的任务。例如:打开一个文件,许多程序都会需要进行此操作。如果每个程序都采用自己的方式实现该功能的话便是一种浪费。编写一个用于打开文件的单个程序,并允许其他程序共享它,反而更有意义。提供这种通用任务支持功能的便是库,库中包含了很多的例程,每一个实现的都是许多程序能够共享的通用任务。在/lib和/usr/lib目录中,我们可以发现很多这样的程序。链接器(linker)程序可以实现编译器的输出与编译程序所需要库之间的链接。该操作的最后结果就是生成一个供使用的可执行文件。

 

三、是不是所有的程序都需要编译 

      答案是否定的。有些程序,如shell脚本,可以直接运行并不需要编译,这些文件都是用脚本或者解释性语言编写的。比如Perl、Python、PHP、Ruby以及其他多种语言等。

      脚本语言由一个称为解释器的特殊程序来执行,解释器负责输入程序文件并执行其所包含的所有指令。通常来讲,解释性程序要比编译后的程序执行起来慢。这是因为在解释性程序中,每条源代码指令在执行时都要重新翻译一次该源代码指令。然而在编译后的程序中,每条源代码指令只翻译一次,并且该翻译结果将永久记录在最后的可执行文件中。

      为什么解释性语言如此受欢迎?其实对于许多日常的编程工作,解释性程序的执行速度也是足够的,而其真正的优点在于开发解释性程序要比开发编译性程序简单而快速得多。程序开发总是经历这样一个重复的循环——编码、编译和测试。随着程序规模的逐渐扩大,编译时间也逐渐变长。解释性程序则省略了编译这一过程,因此加速了程序的开发。

 

四、编译一个C程序

      现在我们可以来编译程序了。然而,在执行编译操作前,需要一些工具,诸如编译器、链接器以及make等。gcc(GNU C编译器)是Linux环境中通用的c编译器,最初是由Richard Stallman编写的。我们可以使用如下命令来查看是否系统安装了该编译器。

      如上表明系统已经安装了gcc编译器。

  1. 获取源码

      从一个叫做diction的GNU项目中选择一个程序来进行练习。依照惯例,我们首先创建一个src目录用于存放源码,然后使用ftp下载源代码至该目录。

1 mkdir src
2 cd src
3 ftp ftp.gnu.org

      使用匿名账户anonymous登录gnu的ftp。然后进入到gun/diction目录下,下载最新版本的diction的tar包。如下图所示:

      注意,由于编译源代码时我们便是源代码的维护者,所以我们将其存放在~/src目录中。发行版本自行安装的源代码一般安装在/usr/src目录下,而面向多用户使用的源代码则通常放在/usr/local/src中。

      可以看到,源代码通常以一个压缩的tar文件的形式存在,有时被称为tarball,该文件包含了源代码树,即构成该源代码的目录以及文件的组织框架。下载了该tar文件后,就用如下的命令来解压缩。

1 tar xzvf diction-1.11-tar.gz

      注: 该diction程序与所有其他GNU项目的软件一样,都遵循一定的源代码打包标准,Linux系统中许多其他可用的源码同样遵循这样的标准。该标准规定,当源代码的tar文件解压缩后,会创建一个包含源代码树的目录,并且该目录以project-x.xx的格式命名,此格式包含了该项目的名称及其版本号。这一机制有助于实现不同版本之间同一程序的安装。然而,在解压之前先检查源代码树的布局也是很有必要的,因为有些项目并不会创建目录而是将所有文件直接送至当前目录,这可能会给原先井然有序的src目录造成混乱。为了避免这样的事情发生,可以使用下面的命令行检查tar文件的内容。

tar tzvf tarfile

      执行结果如下图所示:

 

2. 检查源代码树

      解压diction的tarball会产生一个新的目录diction-1.11。该目录包含了很多的文件。与其他许多linux源码程序类似,GNU项目的程序也都有提供如README、INSTALL、NEWS和COPYING等这些文档文件。这些文件包含的是程序的描述、安装步骤说明以及许可条款。

      除去说明性文件之外,可以看到主要有以下两种文件,分别以.c和.h结尾。

      .c结尾的文件是c程序文件,它们代表一个个模块。如今,这种将一些大的程序分成较小的、易于管理的小程序片的做法已经司空见惯。这些源代码文件都是普通的文本文件。

      .h结尾的文件是c头文件。这些文件,同样也是普通文本文件。头文件中包含了对源代码文件或库中的例程的描述。编译器在链接这些例程模块时,必须给它提供一个其所用到的所有模块的描述。因此,在diction.c的开头可以看到如下的文本行:

#include "getopt.h"

      该文本行会指示编译器在读取diction.c中的源代码内容时先读取文件getopt.h中的内容,进而读取getopt.c中的内容。getopt.c文件包含的是由style和diction程序所共享的例程模块。

      在getopt.h的include语句上面,还可以看到一些其他include语句,如下所示:

#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

      这些都是用来引用头文件,但是它们引用的是那些不在当前源目录下的头文件。它们由系统提供,为每个程序的编译提供支持。库文件在/usr/include或者/usr/local/include下。

 

3. 生成程序

      大多数程序都是使用一个简单的两行命令来生成的。

./configure
make

      configure程序其实是源代码树下的一个shell脚本,它的任务就是分析生成环境。多数源代码都设计成可移植的。也就是说,源代码可以在多种类型的UNIX系统上生成,只是源代码在生成时可能需要经过细微的调整以适应各系统之间的不同。configure同样会检查系统是否已经安装了必要的工具和组件。

      configure会在检查以及配置build程序的同时输出许多相关信息。运行结束后,会输出如下类似内容。如下图:

      需要重点注意的是,这里并没有错误信息。如果有的话,该configure操作将以失败告终,并且不会生成可执行程序,直到所有的错误都被纠正过来。

      可以看到,configure在源目录下创建了几个新文件,其中最重要的就是Makefile文件。Makefile是指导make命令如何生成可执行程序的配置文件,如果没有该文件,make便无法运行。Makefile也是一个普通的文本文件。

      make程序的作用其实就是输入makefile(通常叫做Makefile),该文件描述了生成最后可执行程序时的各部件之间的联系及依赖关系。

      大多数makefile文件都有很多行,这些行定义了目标文件(在本例中是diction可执行文件),也定义了目标文件依赖的一些文件,剩下的行则描述的是那些将源文件生成目标文件的命令。而后执行make命令,make程序运行时,会使用Makefile文件中的内容指导其操作,其间会产生许多信息。运行结束后,我们可以看到目标文件以及可执行文件都出现在了目录中。

      但是如果再次运行make命令。将会看到如下的信息:

      仅仅出现了这样一个奇怪的信息。到底是怎么回事?为什么它没有再次生成程序呢?其实,这便是make的神奇之处。make并不是简单盲目地重新生成所有东西,它只会生成那些需要生成的文件。在所有的目标文件已经存在的情况下,make会判定源文件没有任何改动,也就不会进行任何操作。当然我们可以删除一个目标文件,然后再次运行make以观察make的执行情况。

      可以看到,make重新生成了此目标文件,并重新进行链接,最后生成可执行文件。这一特性同样指出了make的另一个用法——可以维护目标文件的更新。make坚持一个原则,那就是目标文件一定要比依赖文件新。这个特性有着重要的意义。因为程序员会经常更新部分源代码,然后使用make生成一个新版本。make能够确保所有需要基于刚更新的代码而生成的程序都会生成。

      make能够智能地仅生成执行building操作的目标文件,这一能力让程序员获益不少。其所带来的时间节省效益,虽然对于一些小的项目而言并不是那么明显,但是对于大项目而言却非常重要。要知道,Linux内核可是包含几百万行的代码。

 

4. 安装程序

 

      打包好的源代码一般包含一个特殊的make目标程序,它便是install。该目标程序将会在系统目录下安装最后生成的可执行程序。通常,会安装在目录/usr/local/bin下,该目录是本地主机上生成软件的常用安装目录。然而,对于普通用户,该目录通常是不可写的,所以必须转换为超级用户才可以运行安装。安装语句如下:

make install

      安装结束后,就可以通过命令来查看该程序是否可以正确运行。语句如下:

which diction
man diction

      在安装完可执行程序之后,便可以使用该应用或程序了。

 

五、小节

      本章中我们介绍了三个简单的命令,它们是./configure、make和make install,它们可以用于建立多个源代码软件包。同样,我们还介绍了make在软件维护的过程中所扮演的重要角色。make程序并不局限于源码编译,它还用于所有需要维护“目标/依赖”间关系的任务。

 

posted @ 2015-03-14 23:04  jplatformx  阅读(287)  评论(0编辑  收藏  举报