2.5 执行环境
我们知道,不同的处理器具有不同的机器指令集,为一种处理器编写的程序在另一种不同的处理器上无法识别和执行,这就解释了为什么用机器语言和汇编语言编写的程序不能运行在另一种截然不同的计算机上。
不过C是高级语言,用它编写的程序更像在用人类的自然语言描述做什么事情、怎么做,而并不直接对应于任何机器指令,所以在翻译一个C程序时,需要指明处理器类型或者计算机架构,这样才能有针对性地翻译成机器指令的序列,这也解释了为什么人们经常说C是可移植的语言。例如,为了使用英特尔奔腾4处理器的机器指令来生成可执行文件,可以用下面的方法来翻译源文件,-march选项用于指定一个处理器类型:
gcc -march=pentium4 c0201.c
然而在我们日常的应用中,这个选项通常是不需要的,C实现会自动应用一个适当的处理器类型。这是因为从本质上说,C实现并不是什么神奇的东西,它只是普通的可执行程序,只不过它能生成别的可执行程序。
和别的可执行程序一样,每个特定的C实现都是针对特定的操作系统而开发的,而每一个操作系统都只运行在特定的处理器上。比如说,GCC不是“一套”软件,它有不同的版本,每个版本只针对特定的处理器—操作系统组合。大体上,你所选择和安装的版本是与你所用的处理器和操作系统相契合的,而这自然也就成了它翻译源文件时的默认选项。如果程序的编写和运行都在同一台计算机上,这种默认的行为没有什么问题;如果生成的可执行文件将要运行在别的计算机上,而它使用了另一款迥异的处理器,则必须使用-march选项来用那种处理器的机器指令生成可执行文件。
除此之外,在翻译一个C程序时,还要考虑操作系统的问题。操作系统的作用主要有两个,第一个作用是为人们的日常操作提供便利。拿大家比较熟悉的Windows来说,一开机,就出现了桌面和开始菜单,桌面上有程序和文档的图标,菜单里有程序列表。双击桌面上的图标,程序就开始运行,或者文档被打开以供浏览、编辑、通过网络发送或者打印;在程序列表中选择一个程序,那个程序就能开始运行。如果没有操作系统,你将无法完成各种操作,也没办法使用计算机。
操作系统的第二个作用是管理硬件和软件(程序),你能够在Windows、Linux等操作系统里随意地执行任何一个程序,是因为它们都服从操作系统的组织和管理。当你安装一个程序时,由操作系统负责提供磁盘空间——磁盘空间是由操作系统来统一管理的,如果每个程序都不服从统一管理而任意读写磁盘空间,就会乱套,甚至覆盖其他程序的内容;当你运行一个程序时,由操作系统负责将其载入内存中的空闲区域并将处理器的控制权交付于它。物理内存是有限的,且由操作系统管理,操作系统知道哪些位置是空闲的,可以使用。操作系统大都是多任务的,可以同时打开和运行多个程序。如果内存紧张,没有空闲位置,它可以将别的程序移到磁盘上以腾出空间来运行新程序。如果同时运行的程序很多,操作系统还要对它们进行周期性的轮转和调度,好让它们都有机会在处理器上执行,看上去就像所有程序都在同时运行一样。
对于程序员来说,使用操作系统的好处是既方便又省力。在没有操作系统的日子里,程序员非常自由,但这种自由的代价是任何事都要亲力亲为,任务很繁重。首先,电脑由很多硬件组成,他需要自己编写代码来管理和使用那些硬件。比如,他需要亲自将文档的内容转换成适合打印机的格式,并编写代码来驱动打印机工作;他需要亲自安排文件在硬盘上的存储形式和存储位置,并编写代码来驱动和访问硬盘。如果他希望电脑在同一时间能运行自己的好几个程序,还必须亲自编写代码来控制哪一个程序暂停,哪一个程序再次投入运行。总之,他要做的事情实在是太多了,这还没有考虑到一个前提条件:如何将编写的程序装入内存,然后让处理器执行?这事情必须得自己想办法来做!
操作系统为程序员们做了大部分的底层工作,它提供了各种设备的驱动和管理功能,这样一来,程序员就不用再编写直接和设备打交道的程序了。如果程序员编写的程序想保存些东西到硬盘,那他可以简单地将要保存的内容提交给操作系统,请求它来完成硬盘控制和数据保存工作。至于保存到哪里,怎么保存,都不重要,只要下次还能读出来就行了。同时,如果有多个程序都在同一时间访问同一个设备,操作系统也能对这些请求进行仲裁和调度,提供排队功能。
当然,也有很多程序不依赖操作系统这类软件就能自主地运行,最典型的就是操作系统本身,以及种类繁多的嵌入式计算机软件,比如智能家电、仪器仪表和工业控制设备内部的控制软件。在这种非通用的电脑上,你要编写的程序通常对硬件有全部的控制权,通常也不借助于其他程序的帮助就能开始运行。
对于C程序员来说,你开发的程序需要操作系统或者其他系统软件的支持吗?还是不需要?这是个大问题,有没有操作系统或者其他系统软件的支撑,这是个运行环境,称为执行环境。执行环境是需要在编写程序前就提前规划的。如果你在写程序之前就决定让它运行在操作系统或者其他系统软件之上,那么,该程序的执行环境属于宿主环境;相反,如果你决定让程序能够独立运行,不借助于操作系统或者其他系统软件的帮助,那么,该程序的执行环境属于独立环境。
注意,宿主环境和独立环境与电脑有没有安装操作系统或者其他系统软件无关。即使你的台式PC安装了操作系统或者其他系统软件,但是,你的程序在运行时不需要预先启动操作系统,也不需要操作系统或者其他系统软件的任何支持,那这个程序的执行环境也必须不折不扣地被视为独立环境。
所以,从本质上讲,执行环境并不是指你拥有什么样的电脑,也不是指程序将要运行的电脑有没有操作系统或者其他系统软件,而是指,你决定让程序运行的环境是什么(需不需要操作系统或者其他系统软件的支撑)。
一个我们熟悉的、运行于独立环境下的程序实例是操作系统内核。操作系统不需要借助于任何其他操作系统就能自主运行,但它的确可以用C语言来编写。要知道,C语言的其中一个标签就是“系统开发语言”。
在用C语言书写程序时,如果它是针对宿主环境的,那么,源文件中必须有一个名字叫作main的函数。但,这是为什么呢?
首先,为了生成一个运行在宿主式环境下的可执行程序,你需要在翻译的时候给出一个选项来告诉C实现,生成的可执行程序需要借助于操作系统这样的系统软件才能运行。例如,对于GCC,这个选项是-fhosted:
gcc -fhosted c0201.c
当然,这个选项通常是不需要的,因为GCC的默认动作是生成宿主式环境的可执行程序。
我们知道,操作系统的功能之一是管理应用程序。当我们在Windows下双击一个程序的图标时,操作系统要读取并分析这个程序,为它分配内存空间,加载它,做一些初始化的工作,然后把处理器的控制权交给它。
所以,在这种情况下,C实现不单单要依据你的源文件来生成相应的机器代码,还要根据操作系统的要求附加一些特定的代码和数据,这样,操作系统就可以根据这些数据知道如何加载这个应用程序,而这些附加的代码则用于执行一个初始化过程,创建一个可以和操作系统通信的特定的工作环境。一旦初始化完成,这段初始化代码就可以调用函数main,从而正式开始运行应用程序。所以,对于运行于宿主环境的程序来说,函数main其实是指定了一个入口点。“main”是一个约定的名字,C实现翻译一个程序时,它需要根据这个名字来找到充当入口点的那个函数。
相反地,如果要将C的源文件翻译成在独立环境下执行的程序,那么,他可以在运行翻译程序时提供翻译选项,告诉翻译程序,生成的可执行程序必须脱离像操作系统这样的系统软件而独立运行。此时,生成的机器代码比较“纯粹”,与你在C源文件中表达的意图一致。除此之外,基本上不包含更多额外的东西。
比如,如果使用GCC,则-ffreestanding和-nostartfiles选项用于指定生成独立式环境的可执行文件:
gcc -ffreestanding -nostartfiles c0201.c
那么,是不是独立环境下的C程序就不能有main函数?非也。只不过,如果你希望C实现将源文件翻译成在独立环境下运行的程序,那么,翻译程序将不再特殊看待这个main函数,而将它视为一个普通的函数。
在前面,我们已经编写了一个能够从1加到100的程序,这个程序没有任何问题,翻译后可以得到能够在宿主式环境下运行的程序,但是程序运行后不会显示任何结果,计算结果只能在调试器里观察到。
对于C语言的初学者来说,不能在屏幕上显示程序的运行结果,会让他们觉得心里空落落的,像少了点什么,不踏实。但是我们已经说了,就我们目前所掌握的知识,尚不足以展开这方面的讨论,还需要再拖一拖,留到后面的章节里揭晓。
实际上,不能在屏幕上显示结果是一件好事,这可以促使大家熟悉调试器,在调试器中观察程序的行为和执行结果,我们的学生通常缺乏这种训练。程序调试是一门很重要的技能,很多时候,程序中的问题不是靠在屏幕上输出结果来发现的,而必须依靠调试器来观察和分析。