我们知道那个时候的CPU是非常宝贵的资源。所以必须充分利用,尽量使多个程序能连续的运行。所以一般都会把一批程序以脱机方式输入到磁带上。然后当一个程序执行完成之后,马上运行下一个程序。但是这个过程不是由人来调度的,所以我们需要一个监控程序来控制这些程序一个一个的执行。于是除了我们要运行的程序之外,在内存中还存在这样一个监控程序,我们可以把他看看做早期操作系统的雏形,也就是单道批处理系统。
上图是单道批处理系统的流程图,看起来很简单。这个小程序需要常驻内存,也就是在开始执行其他程序之前,需要把他加载到内存中。当然加载的方式和其他程序相同。然后CPU开始执行这个监控程序。然后这个程序就开始进行批处理操作。于是在内存中,同一时间需要存放两个程序。下图是此时的内存布局.
从此图我们可以发现,这个程序或许并不如我们想象的简单:
在这里我们可以发现,对于单道批处理程序来说,内存不再是独享,而是作业程序和监控程序共享,共享就会有一些问题:
所以,我个人觉得,决定程序加载的位置应该是监控程序来确定。比如0x0000~0x050的内存区域是监控程序的区域,这样作业程序的起始地址就是0x0051,而监控程序通过修改PC计数器,来修改CPU要执行的下一条代码来切换程序控制权。
对于单道操作系统来说,他可以连续的运行多个程序,减少了程序切换时的CPU等待时间。但是它的问题在于,当执行I/O操作加时,CPU是空闲的。于是出现了多道批处理程序。多道批处理程序,把多个程序同时加载到内存中,当其中正在运行的程序执行I/O操作时,CPU可以继续执行其他的程序,而当I/O操作结束后,程序可以继续被执行。
上图是多道批处中会存在2个以上的程序,所以需要一个内存分区表来标识每个程序占用的内存范围,以保证每个程序内存空间的独立。
多道批处理系统的优点在于资源利用率高、系统吞吐量大、平均运行时间长。同样也存在物交互能力,而且需要内存管理,作业调度,设备管理等功能,这就增加了程序的负责的度。
可以说批处理操作系统是现代操作系统的雏形。从DOS到Windows3.1,到Win95,WinXP,Win8,Unix,Linux。现代操作系统可以说是一个庞大的程序,多CPU,多进程,多线程,虚拟内存等等,相比多道批处理应用程序复杂了千万倍。而内存布局也随着硬件和操作系统的发展而发生了变化。但是主要的目的都是提高系统效率,提高系统稳定性安全性。提供更好的交互体验。
到目前为止,我们都只是一直在谈论,程序被加载到内存然后执行的过程,而没有提及到程序是如何从我们编写的代码变为可执行文件的。在我们开始介绍现代操作系统中程序内存布局之前,先看看程序是如何被编译成可执行文件的。因为计算机的发展是和硬件,操作系统,编译器等共同发展分不开的。
现在我们基本都是在可视环境下进行开发,比如Eclipse,VS等开发工具。这些工具功能相当的强大,我们只需专注代码的编写,点几下鼠标,一个可执行文件就被生成出来了。但是在这背后,开发工具到底做了什么呢? 下面一个简单的C程序是如何被编译成可执行文件的呢?
一般来说,一个程序从源代码到可执行文件是通过编译器来完成的,简单的说,编译器的工作就是把高级语言转换为机器码,一个现代的编译器工作流程是:(源代码)--预处理--编译---汇编---链接--(可执行文件)。在Linux下一般使用GCC来编译C语言程序, 而VS中使用cl.exe。下图就是上面的代码在GCC中编译的过程。我们后面讨论的都以C语言为例。编译器
预处理是程序编译的第一步,以C语言为例, 预编译会把源文件预编译成一个 .I 文件。而C++则是编译成 .ii。 GCC中预编译命令如下
当我们打开hello.i 文件是会发现这个文件变的好大,因为其中包含的<stdio.h> 文件被插入到了hello.i 文件中,一下是截取的一部分内容
总结下来预处理有一下作用:
编译是一个比较复杂的过程。编译后产生的是汇编文件,其中经过了词法分析、语法分析、语义分析、中间代码生成、目标代码生成、目标代码优化等六个步骤。大学时有一门《编译原理》的课程就是讲这个的,只可惜当时学的并不好,感觉太枯燥太难懂了。所以当我们语法有错误、变量没有定义等问题是,就会出现编译错误。
通过上面的命令,可以从预编译文件生成汇编文件,当然也可以之际从源文件编译成汇编文件。实际上是通过一个叫做ccl的编译程序来完成的。
上面就是生成的汇编文件。我们看出其中分了好几个部分。我们只需要关注,LFB0这个段中保存的就是C语言的代码对于的汇编代码.。
汇编的过程比较简单,就是把汇编代码转换为机器可执行的机器码,每一个汇编语句机会都对应一条机器指令。它只需要根据汇编指令和机器指令的对照表进行翻译就可以了。汇编实际是通过汇编器as来完成的,gcc只不过是这些命令的包装。
汇编之后生成的文件是二进制文件,所以用文本打开是无法查看准确的内容的,用二进制文件查看器打开里面也全是二进制,我们可以用objdump工具来查看:
上面我们看到了Main函数汇编代码和机器码对应的关系。关于objdump工具后面会介绍。这里生成的.o文件我们一般称为目标文件,此时它已经和目标机器相关了。
链接是一个比较复杂的过程,其实链接的存在是因为库文件的存在。我们知道为了代码的复用,我们可以把一些常用的代码放到一个库文件中提供给其他人使用。而我们在使用C,C++等高级语言编程时,这些高级语言也提供了一些列这样的功能库,比如我们这里调用的printf 函数就是C标准库提供的。 为了让我们程序正常运行,我们就需要把我们的程序和库文件链接起来,这样在运行时就知道printf函数到底要执行什么样的机器码。
我们看到我们使用了链接器ld程序来操作,但是为了得到最终的a.out可执行文件(默认生成a.out),我们加入了很多目标文件,而这些就是一个printf正常运行所需要依赖的库文件。
对于C#和Java这种运行在虚拟机上的语言,编译过程有所不同。 对于C,C++的程序,生成的可执行文件,可以在兼容的计算机上直接运行。但是C#和Java这些语言则不同。他们编译过程是相似的,但是他们最终生成的并不是机器码,而是中间代码,对于C#而言叫IL代码,对于JAVA是字节码。所以C#,JAVA编译出来的文件并不能被执行。
我们在使用.NET或JAVA时都需要安装.NET CLR或者JAVA虚拟机,以.NET为例,CLR实际是一个COM组件,当你点击一个.NET的EXE文件时,它和C++等不一样,不能直接被执行,而是有一个垫片程序来启动一个进程,并且初始化CLR组件。当CLR运行后,一个叫做JIT的编译器会吧EXE中的IL代码编译成对应平台的机器码,然后如同其他C++程序一样被执行