从计算机组成讲起。。。

学习计算机已五六年,也一直感觉对计算机基础理论的掌握还是不错的。但最近很多东西都只是知道一些皮毛,只是机械地记得一些概念。作为一个实际存在的物理实体,对于计算机的很多物理实现其实我并不清楚,如内存的物理实现到底是什么。虽然自己并不从事底层开发,但还是觉得需要对计算机的高级抽象到物理实现有个整体理解。

那么,现在就从计算机的组成说起吧。。。

简单地说,目前的计算机由中央处理器、存储器、输入/输出设备组成。

中央处理器(还是不懂,犹豫了半天不知道该怎么写)由控制器、算术逻辑部件和多个寄存器组组成。控制器负责从主存储器中取指令个分析指令类型;算术逻辑部件通过完成算术逻辑运算来执行指令;寄存器用来存放中间结果和一些控制信息,其中最重要的寄存器是程序计数器。对于CPU中的控制器的实现原理目前还是不清楚,但可以猜测的是所谓控制器最后肯定是以各种电信号来指挥CPU中各种部件的工作。值得注意的是,无论是精简指令集还是复杂指令集,其指令系统的微程序存放在一个只读存储器——控制存储器中。一条机器指令编写成一段微程序。每一个微程序包含若干条微指令,每一条微指令对应一条或多条微操作。在当CPU执行机器指令时,会在控制存储器里寻找与该机器指令对应的微程序,取出相应的微指令来控制执行各个微操作,从而完成该程序语句的功能。

存储器顾名思义是用来存储信息的。其物理的基本电子元件是晶体管,然后由晶体管,电阻等实现与、或、非等门电路。门电路的各种组合连接可以实现锁存器、触发器等的功能。而多个触发器组合在一起后构成寄存器可以用来保存超过1位长的数据。其实此时的寄存器已经可以存储信息。但对于大容量的内存,需要通过为每个内存字进行编址的方式进行组织。多个内存芯片以某种方式连接后可以扩大内存的容量(也就是当初考试中扩充内存的题型)。

同样都是晶体管存储设备,而寄存器的存取速度比内存快主要是因为硬件设计不同。内存的设计相对简单,每个位就是一个电容和一个晶体管,而寄存器的设计则完全不同,多出好几个电子元件。同时,二者的工作方式也不同。寄存器的工作方式很简单,只有两步:(1)找到相关的位,(2)读取这些位。而内存的工作方式就要复杂得多。

以上就是内存物理实现的概况,当然遗落了很多的知识点。此处只是对其物理实现的一个总结。

一台计算机启动时,启动BIOS引导程序,加载并执行主引导记录MBR,主引导记录在硬盘上找到可引导分区,将其分区引导记录装入内存,并将控制权交给分区引导记录,由分区引导记录定位根目录,然后装入操作系统。

对于程序的执行,先从代码的编写谈起。程序的执行包括预处理(Prepressing)编译(Compilation)汇编(Assembly)链接(Linking)装载(Loading)五个步骤。

预处理主要是处理那些源代码中以#开始的预编译指令;编译过程分为词法分析、语法分析、语义分析、源代码优化、目标代码生成(产生汇编语言)、目标代码优化6步(后两个步骤依赖于目标机器);接着汇编器是将汇编代码转变成机器可以执行的命令。

经过汇编器产生了二进制目标代码。由于每一种操作系统有自己的二进制文件格式,所以二进制程序不能跨平台运行。操作系统把二进制可执行程序load到内存中之后,会根据默认的这种格式寻找各种数据,比如代码段,数据段和初始化段。所以说 Windows 下面的 exe 可执行文件,lib 静态库,dll 动态库是不可以直接运行在 Linux 系统下面的;MacOS 下面的 Mach-O 可执行文件,静态链接库(a库),动态链接库(so库)也是不能够直接放在 Linux 系统下面运行的。反之亦然,Linux 下面的 ELF 可执行文件,静态链接库(a库),动态链接库(so库)同样不能够在 Window 系统下面运行的。

同时目标文件由许多段组成,其中主要的段包括代码段(.text)保存编译后得到的指令数据;数据段(.data)保存已经初始化的全局静态变量和局部静态变量;只读数据段(.rodata)保存只读变量和字符串常量,有些编译器会把字符串常量放到”.data”段;BSS段(.bss)保存未初始化的全局变量和局部静态变量。

虽然经过汇编器产生了二进制目标代码,但该代码并不是可执行程序。目标文件仅仅把当前的源码文件编译成二进制文件,并没有经过链接过程,是不能够执行的。

当一个程序需要将多个模块以某种方式组合时,就出现了链接的概念。链接有静态链接和动态链接。静态链接就是把程序运行的地址划分为了一段一段的片段,有的片段是用来存放代码,叫代码段,这样,可以给这个段加个只读的权限,防止程序被修改;有的片段用来存放数据,叫数据段,数据经常修改,所以可读写;有的片段用来存放标识符的名字,比如某个变量 ,某个函数,叫符号表;等等。由于有这么多段,所以为了方便管理,所以又引入了一个段,叫段表,方便查找每个段的位置。当文件之间相互需要链接的时候,就把相同的段合并,然后把函数,变量地址修改到正确的地址上 。这就是静态链接。由于静态链接对于计算机的内存和磁盘的空间浪费比较严重,且会给程序的更新,部署,和发布会带来很多麻烦,所以出现了动态链接:把链接的过程推迟到运行的时候再进行。链接器存在以下作用:对各个目标文件中没有定义的变量,在其他目标文件中寻找到相关的定义;把不同目标文件中生成的同类型的段进行合并;对不同目标文件中的变量进行地址重定位。

编译完成之后,程序就会被分为代码段、数据段、堆栈段等段数据,并保存相关位置信息。代码段、数据段、BBS段的大小在编译时就可以确定,而堆和栈的大小运行时才可以确定(这些段的产生也和操作系统的内存管理有关,即所谓的操作系统的段式管理是逻辑划分)。

可执行文件格式为头数据和各段数据组成。头数据说明了可执行文件的属性和执行环境,段数据又分为数据段,代码段,资源段等,段的多少和位置由头数据说明。也就是说,不仅仅只是代码段和数据段。这些段由不同的编译环境和编译参数控制,由编译器自动生成可执行文件的段和文件格式。当操作系统运行可执行文件时,会动态建立堆栈段,它是动态的,并且属于操作系统执行环境。

目标代码链接后生成可执行文件,执行时装载入内存。

一段内存,可以既是代码的存储空间,又是数据的存储空间,还可以是栈空间,也可以什么也不是,关键在于CPU中寄存器的设置,即:CS,IP,SS,SP,DS的指向。对于数据段,将它的段地址放在DS中,用mov、add、sub等访问内存单元的指令时,CPU就将我们定义的数据段中的内容当做数据来访问。对于代码段,将它的段地址放在CS中,将段中第一条指令的偏移地址放在IP中,这样CPU就将执行我们定义的代码段中的指令对于栈段,将它的地址放在SS中,将栈顶单元的偏移地址放在SP中国年,这样CPU在需要进行栈操作的时候,比如执行push、pop等指令,就将我们定义的栈段当做栈空间来使用。CPU将内存中的某段内容当做代码,是因为CS:IP指向了那里;CPU将某内存当做栈,是因为SS:SP指向了那里。

堆与栈的空间大小:一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M。现代大地址空间和虚拟内存技术可以将栈和堆放在任何地方,但是二者增长方向也是相反的。栈区存放程序的栈,一种LIFO结构,一般都在内存的高地址段。在X86架构中栈地址是向0地址增长,其他一些架构中相反。

总体来说,程序源代码被编译以后主要分成两种段:程序指令和程序数据。代码段属于程序指令,而数据段和.bss段属于程序数据。很多人可能会有疑问:为什么要那么麻烦,把程序的指令和数据的存放分开?混杂地放在一个段里面不是更加简单?其实数据和指令分段的好处有很多。主要有如下几个方面。

程序的指令和数据分开原因:

一方面是当程序被装载后,数据和指令分别被映射到两个虚存区域。由于数据区域对于进程来说是可读写的,而指令区域对于进程来说是只读的,所以这两个虚存区域的权限可以被分别设置成可读写和只读。这样可以防止程序的指令被有意或无意地改写。

另外一方面是对于现代的CPU来说,它们有着极为强大的缓存(Cache)体系。由于缓存在现代的计算机中地位非常重要,所以程序必须尽量提高缓存的命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU的缓存一般都被设计成数据缓存和指令缓存分离,所以程序的指令和数据被分开存放对CPU的缓存命中率提高有好处。

第三个原因,其实也是最重要的原因,就是当系统中运行着多个该程序的副本时,它们的指令都是一样的,所以内存中只须要保存一份改程序的指令部分。对于指令这种只读的区域来说是这样,对于其他的只读数据也一样,比如很多程序里面带有的图标、图片、文本等资源也是属于可以共享的。当然每个副本进程的数据区域是不一样的,它们是进程私有的。不要小看这个共享指令的概念,它在现代的操作系统里面占据了极为重要的地位,特别是在有动态链接的系统中,可以节省大量的内存。比如我们常用的Windows Internet Explorer 7.0运行起来以后,它的总虚存空间为112 844 KB,它的私有部分数据为15 944 KB,即有96 900 KB的空间是共享部分(数据来源见图3-2)。如果系统中运行了数百个进程,可以想象共享的方法来节省大量空间。

内存的分段的原因:8086CPU有20根地址线,最大可寻址内存空间为1MB。而8086的寄存器只有16位,指令指针(IP)和变址寄存器(SI、DI)也是16位的。用16位的地址寻址1MB空间是不可能的。所以就要把内存分段,也就是把1MB空间分为若干个段,每段不超过64KB,在8086中设置4个16位的段寄存器,用于管理4种段:CS是代码段,DS是数据段,SS是堆栈段,ES是附加段。

参考(程序是如何运行的高级语言的编译:链接及装载过程介绍

以上关于程序分段的描述,简而言之就是在程序的可执行文件中已经将程序分段,当程序加载入内存以后,在内存中也是分段管理的(内存管理方式了解一下)。


一直对编程语言、操作系统API、系统调用的实现以及代码的可移植性究竟是什么有疑惑(编译器是用高级编程语言写的,高级编程语言写的程序又是由编译器进行编译的,这好像叫做“编译器的自举”;操C语言和内存有关的底层是用什么语言实现的?不过话说回来,这种类似“鸡生蛋,蛋生鸡”的问题,再计算机领域最直接的答案应该是第一个肯定是由汇编等和机器语言接近的低级语言实现的,总有一个开始)。先记录一点东西吧:

C语言的标准库和系统调用

在一个论坛上看到如下答案,记录一下:

对于编程语言,基本上是这样进化的:

先用机器语言写出汇编器,然后就可以用汇编语言编程了,然后再用汇编语言编写汇编器。

先用汇编语言写出 C 编译器,然后就可以用 C 语言编程了,然后再用 C 语言来写 C 编译器。

有了 C 编译器与 C 语言,就可以在这个基础上再编写高级语言的编译器或解释器或虚拟机了。

非 C 系语言,进化过程同上。

至于操作系统,先用汇编语言写一个操作系统。Ken Thompson 干过这样的事,他用汇编语言以及他自创的一种解释性语言——B 语言写出来 unix 第一版,是在一台内存只有 8KB 的废弃的计算机上写出来的。然后 Dennis Ritchie 发明了 C 语言,然后 Ken 与 Dennis 又用 C 语言在一台更好的计算机——16 位机器上将 unix 重写了一遍。

至于 Windows 系统,MS 先是买了 QDOS,然后又在 QDOS 里引入了一些 Unix 的元素,然后比尔·盖茨靠着这个买来的系统赚了一大笔钱,然后就在 DOS 系统上开发了 windows 3.1,windows 95 ……

(可以理解为操作系统和编译器是平行发展的两条路。。。编译器是由滚雪球的方式发展而来的,有了编译器,操作系统的内核就可以由高级语言实现了)


操作系统API与C标准库的关系

 

如上图所示,从宏观上来看,Linux操作系统的体系架构分为用户态和内核态(或者用户空间和内核)。内核从本质上看是一种软件——控制计算机的硬件资源,并提供上层应用程序运行的环境。用户态即上层应用程序的活动空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。 系统调用是操作系统的最小功能单位,这些系统调用根据不同的应用场景可以进行扩展和裁剪,现在各种版本的Unix实现都提供了不同数量的系统调用,如Linux的不同版本提供了240-260个系统调用,FreeBSD大约提供了320个(reference:UNIX环境高级编程)。

C标准库是在操作系统API上加入独特的算法封装成标准接口的库,使用C标准库可以屏蔽底层实现细节,比如fopen这样的函数,在Windows上通过调用CreateFileEx实现,在linux上通过调用open系统调用实现。不仅是包装,还在上层使用独特的算法提供了用户态缓冲区的功能。(C标准库是对系统调用的封装,和系统平台无关;而系统调用和系统相关;也就是说)


java可移植性的实质

大多数编译器产生的目标代码只能运行在一种CPU上(如Intel的x86系列),即使那些能支持多种CPU的编译器也不能同时产生适合多种CPU的目标代码。如果你需要在三种CPU(如x86、SPARC和MIPS)上运行同一程序,就必须编译三次。但JAVA编译器就不同了。JAVA编译器产生的目标代码(J-Code)是针对一种并不存在的CPU–JAVA虚拟机(JAVAVirtualMachine),而不是某一实际的CPU。JAVA虚拟机能掩盖不同CPU之间的差别,使J-Code能运行于任何具有JAVA虚拟机的机器上。即使经过重新编译,大多数的用C和C++编写的Windows程序也不能在Unix或Macintosh系统上运行。这是为什么呢?因为程序员在编写Windows程序时使用了大量的WindowsAPI和中断调用,而Windows程序对系统功能的调用与Unix和Macintosh程序有很大的差别,所以除非将全套WindowsAPI移植到其它操作系统上,否则重编译的程序仍不能运行。

 

不论你在什么系统中编译的java,得到的都是统一的字节码(中间码),在windows中需要有windows版本的JVM来执行,要是到了linux下,只要下载linux版本的JVM来执行就可以了这就是java的跨平台可移植性。

其实每种编程语言最开始设计的目的不同,其设计的各种机制也不同。

 


后记:花费了两天的时间来总结这些知识点,也不知是否有用。其实并没有学习到新的知识。如果说还是有所收获的话,那可能就是将以前学习的计算机组成原理、操作系统汇编语言等知识融会贯通了。应该也算一些长进吧。

其实最大的体会应该是对以前所学习到的知识的一种重新理解。经过这两天的重新梳理,其实发觉大学的时候并没有真正学会那些知识,只是单纯的记下一些东西,对于这些东西产生的原因以及作用并不了解。时至今日,才发现当初所学各门课程之间的联系。也不知是该为当初只是成绩好的自己感到悲哀,还是对今日恍然大悟的自己感到庆幸。。。

计算机科学是一个很庞大的学科体系,当前的很多发展都是站在前人的肩膀上进行发展的。以上内容只是自己对计算机组成及运行的粗浅理解,应该有不准确的地方,暂且记录吧。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值