对于绝大多数编译好的程序来说,要想得到处理器的光顾,让它执行一下,必须借助于操作系统。就拿Windows 来说,它为你显示每个程序的图标,允许你双击来运行它们。在内部你看不见的层面上,它必须给将要运行的程序分配空闲的内存空间,并在适当的时候将程序提交给处理器执行。
每种操作系统都对它所管理的程序提出了种种格式上的要求。比如,它要求编译好的程序必须在文件的开始部分包含编译日期,是针对哪种操作系统编译的,程序的版本,第一条指令从哪里开始,数据段从哪里开始、有多长,代码段从哪里开始、有多长,等等,Windows 甚至建议你在文件中包含至少一个用于显示的图标。如果你不按它的要求来,它也不会给你面子,并直截了当地弹出一个对话框,如图4-1 所示,告诉你它不准备,也没办法将你的程序提交给处理器。
图4-1 每种操作系统都会定义它自己的可执行文件格式
每种编译器都有能力针对不同的操作系统来生成不同格式的二进制文件,程序员所要做的,就是在源程序中加入一些相关的信息,比如指定每个段的开始和结束,并在编译时指定适当的参数。如果你对此感兴趣,可以阅读NASM 文档。这是一个PDF 文件,在安装NASM 的时候,它也会被安装。
在特定的操作系统上开发软件肯定不是一件容易的事情。但换个角度考虑一下,操作系统也是一个需要在处理器上运行的软件,只不过比起一般的程序而言,体积更为庞大,功能更为复杂而已。如果我们能绕过它,或者代替它,让计算机一开机的时候直接执行我们自己的软件,岂不更简单?
好,这个主意完全可行。那就让我们慢慢开始吧。
在处理器众多的引脚中,有一个是RESET,用于接受复位信号。每当处理器加电,或者RESET 引脚的电平由低变高时 ,处理器都会执行一个硬件初始化,以及一个可选的内部自测试(Build-in Self-Test,BIST),然后将内部所有寄存器的内容初始到一个预置的状态。
比如,对于Intel 8086 来说,复位将使代码段寄存器(CS)的内容为0xFFFF,其他所有寄存器的内容都为0x0000,包括指令指针寄存器(IP)。8086 之后的处理器并未延续这种设计,但毫无疑问,无论怎么设计,都是有目的的。
处理器的主要功能是取指令和执行指令,加电或者复位之后,它就会立刻尝试去做这样的工作。不过,在这个时候,内存中还没有任何有意义的指令和数据,它该怎么办呢?
在揭开谜底之前,我们先来看看内存的特点。
为了节约成本,并提高容量和集成度,在内存中,每个比特的存储都是靠一个极其微小的晶体管,外加一个同样极其微小的电容来完成的。可以想象,这样微小的电容,其泄漏电荷的速度当然也非常快。所以,个人计算机中使用的内存需要定期补充电荷,这称为刷新,所以这种存储器也称为动态随机访问存储器(Dynamic Random Access Memory,DRAM)。随机访问的意思是,访问任何一个内存单元的速度和它的位置(地址)无关。举个例子来说,从头至尾在一盘录音带上找某首歌曲,它越靠前,找到它所花的时间就越短。但内存就不一样,读写地址为0x00001 的内存单元,和读写地址为0xFFFF0 的内存单元,所需要的时间是一样的。
在内存刷新期间,处理器将无法访问它。这还不是最麻烦的,最麻烦的是,在它断电之后,所有保存的内容都会统统消失。所以,每当处理器加电之后,它无法从内存中取得任何指令。
Intel 8086 可以访问1MB 的内存空间,地址范围为0x00000 到0xFFFFF。出于各方面的考虑,计算机系统的设计者将这1MB 的内存空间从物理上分为几个部分。
8086 有20 根地址线,但并非全都用来访问DRAM,也就是内存条。事实上,这些地址线经过分配,大部分用于访问DRAM,剩余的部分给了只读存储器ROM 和外围的板卡,如图4-2 所示。
图4-2 8086 系统的内存空间分配
与DRAM 不同,只读存储器(Read Only Memory,ROM)不需要刷新,它的内容是预先写入的,即使掉电也不会消失,但也很难改变。这个特点很有用,比如,可以将一些程序指令固化在ROM 中,使处理器在每次加电时都自动执行。处理器醒来后不能饿着,这是很重要的。
在以Intel 8086 为处理器的系统中,ROM 占据着整个内存空间顶端的64KB,物理地址范围是0xF0000~0xFFFFF,里面固化了开机时要执行的指令;DRAM 占据着较低端的640KB,地址范围是0x00000~0x9FFFF;中间还有一部分,分给了其他外围设备,这个以后再说。因为8086 加电或者复位时,CS=0xFFFF,IP=0x0000,所以,它取的第一条指令位于物理地址0xFFFF0,正好位于ROM 中,那里固化了开机时需要执行的指令。
处理器取指令执行的自然顺序是从内存的低地址往高低地址推进。如果从0xFFFF0 开始执行,这个位置离1MB 内存的顶端(物理地址0xFFFFF)只有16 个字节的长度,一旦IP 寄存器的值超过0x000F,比如IP=0x0011,那么,它与CS 一起形成的物理地址将因为溢出而变成0x00001,这将回绕到1MB 内存的最低端。
所以,ROM 中位于物理地址0xFFFF0 的地方,通常是一个跳转指令,它通过改变CS 和IP的内容,使处理器从ROM 中的较低地址处开始取指令执行。在NASM 汇编语言里,一个典型的跳转指令像这样:
在这里,“jmp”是跳转(jump)的简化形式;0xf000 是要跳转到的段地址,用来改变CS 寄存器的内容;0xe05b 是目标代码段内的偏移地址,用来改变IP 寄存器的内容。因此,目标位置的物理地址是0xfe05b。一旦执行这条指令,处理器将开始从指定的“段: 偏移”处开始重新取指令执行。
到了本书第5 章我们就能接触跳转指令了,现在,我们只需要知道,指令的执行并非总是顺序的,有时候不得不根据某些条件来选择执行哪些指令,不执行哪些指令。这个时候,跳转指令是很有用的。
这块ROM 芯片中的内容包括很多部分,主要是进行硬件的诊断、检测和初始化。所谓初始化,就是让硬件处于一个正常的、默认的工作状态。最后,它还负责提供一套软件例程,让人们在不必了解硬件细节的情况下从外围设备(比如键盘)获取输入数据,或者向外围设备(比如显示器)输出数据。设备当然是很多的,所以这块ROM 芯片只针对那些最基本的、对于使用计算机而言最重要的设备,而它所提供的软件例程,也只包含最基本、最常规的功能。正因为如此,这块芯片又叫基本输入输出系统(Base Input & Output System,BIOS)ROM。在读者缺乏基础知识的情况下讲述ROM-BIOS 的工作只会越讲越糊涂,所以这些知识将会分散在各个章节里予以讲解。
ROM-BIOS 的容量是有限的,当它完成自己的使命后,最后所要做的,就是从辅助存储设备读取指令数据,然后转到那里开始执行。基本上,这相当于接力赛中的交接棒。
历史上,有多种辅助存储设备,比如软盘、光盘、硬盘、U 盘等,相对于内存,它们就是人们常说的“外存”,即外存储器(设备)。
从软盘(Floppy Disk)启动计算机,这已经是过去的事了。软盘的尺寸比烟盒稍大一点,但是比较薄,采用塑料作为基片,上面是一层磁性物质,可以用来记录二进制位。这种塑料介质比较柔软,所以称为软盘。
在数据记录原理上和软盘很相似的设备是硬盘(Hard Disk,HDD),而且它们几乎是同一个时代的产物。但是,与软盘不同,硬盘是多盘片、密封、高转速的,采用铝合金作为基片,并在表面涂上磁性物质来记录二进制位。这就使得它的盘片具有较高的硬度,故称为硬盘。
图4-3 一块被拆开密封盖的硬盘
如图4-3 所示,这是一块被拆开的硬盘,中间是用于记录数据的铝合金盘片,固定在中心的轴上,由一个高速旋转的马达驱动。附着在盘片表面的扁平锥状物,就是用于在盘片上读写数据的磁头。
为了进一步搞清楚硬盘的内部构造,图4-4 给出了更为详细的图示。
硬盘可以只有一个盘片(这称为单碟),也可能有好几个盘片。但无论如何,它们都串在同一个轴上,由电动机带动着一起高速旋转。一般来说,转速可以达到每分钟3600 转或者7200转,有的能达到一万多转,这个参数就是我们常说的“转/分钟”(Round Per Minute,RPM)。
图4-4 硬盘的结构示意图
每个盘片都有两个磁头(Head),上面一个,下面一个,所以经常用磁头来指代盘面。磁头都有编号,第1 个盘片,上面的磁头编号为0,下面的磁头编号为1;第2 个盘片,上面的磁头编号为2,下面的磁头编号为3,以此类推。
每个磁头不是单独移动的。相反,它们都通过磁头臂固定在同一个支架上,由步进电动机带动着一起在盘片的中心和边缘之间来回移动。也就是说,它们是同进退的。步进电动机由脉冲驱动,每次可以旋转一个固定的角度,即可以步进一次。
可以想象,当盘片高速旋转时,磁头每步进一次,都会从它所在的位置开始,绕着圆心“画”出一个看不见的圆圈,这就是磁道(Track)。磁道是数据记录的轨迹。因为所有磁头都是联动的,故每个盘面上的同一条磁道又可以形成一个虚拟的圆柱,称为柱面(Cylinder)。
磁道,或者柱面,也要编号。编号是从盘面最边缘的那条磁道开始,向着圆心的方向,从0开始编号。
柱面是一个用来优化数据读写的概念。初看起来,用硬盘来记录数据时,应该先将一个盘面填满后,再填写另一个盘面。实际上,移动磁头是一个机械动作,看似很快,但对处理器来说,却很漫长,这就是寻道时间。为了加速数据在硬盘上的读写,最好的办法就是尽量不移动磁头。这样,当0 面的磁道不足以容纳要写入的数据时,应当把剩余的部分写在1 面的同一磁道上。如果还写不下,那就继续把剩余的部分写在2 面的同一磁道上。换句话说,在硬盘上,数据的访问是以柱面来组织的。
实际上,磁道还不是硬盘数据读写的最小单位,磁道还要进一步划分为扇区(Sector)。磁道很窄,也看不见,但在想象中,它仍呈带状,占有一定的宽度。将它划分许多分段之后,每一部分都呈扇形,这就是扇区的由来。
每条磁道能够划分为几个扇区,取决于磁盘的制造者,但通常为63 个。而且,每个扇区都有一个编号,与磁头和磁道不同,扇区的编号是从1 开始的。
扇区与扇区之间以间隙(空白)间隔开来,每个扇区以扇区头开始,然后是512 个字节的数据区。扇区头包含了每个扇区自己的信息,主要有本扇区的磁道号、磁头号和扇区号,用来供硬盘定位机构使用。现代的硬盘还会在扇区头部包括一个指示扇区是否健康的标志,以及用来替换该扇区的扇区地址。用于替换扇区的,是一些保留和隐藏的磁道。
尽管我们使用硬盘的历史很长,但它一直没能退出舞台,这主要是因为它总能通过不断提高自己的容量来打败那些竞争者。20 世纪90 年代初,40MB 的硬盘算是常见的,能拥有200MB 的硬盘很让人羡慕。看看现在,500GB 的硬盘也不算稀罕,而且价钱也很便宜。
前面说到,当ROM-BIOS 完成自己的使命之前,最后要做的一件事是从外存储设备读取更多的指令来交给处理器执行。现实的情况是,绝大多数时候,对于ROM-BIOS 来说,硬盘都是首选的外存储设备。
硬盘的第一个扇区是0 面0 道1 扇区,或者说是0 头0 柱1 扇区,这个扇区称为主引导扇区。如果计算机的设置是从硬盘启动,那么,ROM-BIOS 将读取硬盘主引导扇区的内容,将它加载到内存地址0x0000:0x7c00 处(也就是物理地址0x07C00),然后用一个jmp 指令跳到那里接着执行:
为什么偏偏是0x7c00 这个地方?还不太清楚。反正当初定下这个方案的家伙已经被人说了很多坏话,我也就不准备再多说什么了。
通常,主引导扇区的功能是继续从硬盘的其他部分读取更多的内容加以执行。像Windows 这样的操作系统,就是采用这种接力的方法一步一步把自己运行起来的。
说到这里,我们可以想象,如果我们把自己编译好的程序写到主引导扇区,不也能够让处理器执行吗?
对于这种想法,我有一个好消息和一个坏消息要告诉你。
好消息是,这是可以的,而且这几乎是在不依赖操作系统的情况下,让我们的程序可以执行的唯一方法。
不过,坏消息是,如果你改写了硬盘的主引导扇区,那么,Windows 和Linux,以及任何你正在使用的操作系统都会瘫痪,无法启动了。
那么,我们该怎么办呢?答案是在你现有的计算机上,再虚拟出一台计算机来。
检测点4.1
1. 硬盘的磁头(盘面)是从数字( )开始编号的;每个盘面磁道是从数字( )开始编号的;每磁道/柱面上的扇区是从数字( )开始编号的,主引导扇区的位置是( )面( )道( )扇区;
2. 如果希望处理器从当前位置转移到物理地址0xc5030 处开始执行,可以使用下面的哪些指令(可多选):
A. jmp 0xc000:0x5030 B. jmp 0xc500:0x0030
C. jmp 0xc503:0x0000 D. jmp 0xbb00:0xa030