红联Linux门户
Linux帮助

I386的体系结构

发布时间:2008-11-12 19:26:26来源:红联作者:ipitx
本文转载自:http://www.kerneltravel.net/journal/ii/part1.htm#_ftn1

第1章 内存寻址
曾经有一个叫“阿兰.图灵”的天才[1],他设想出了一种简单但运算能力几乎无限发达的理想机器----这可不是一个具体的机械玩艺,而是一个思想模型----用来计算能想象得到的所有可计算函数。这个有趣的机器由一个控制器、一个读写头和一条假设两端无限长的带子组成。工作带好比存储器,被划分成大小相同的格子,每格上可写一个字母,读写头可以在工作带上随意移动,而控制器可以要求读写头读取其下方工作带上的字母。

你可千万别觉得这个机器傻得可爱,它可是当代冯.诺依曼体系计算机的理论鼻祖。它带来的“数据连续存储和选择读取思想”正是目前我们使用的几乎所有机器运行背后的灵魂。计算机体系结构中的核心问题之一就是如何有效地进行内存寻址,因为所有运算的前提都是先要从内存中取得数据,所以内存寻址技术从某种程度上代表了计算机技术。

下面就开始一起聊聊关于寻址的故事。

冯.诺依曼体系计算机系统由运算器、存储器、控制器、输入设备、输出设备五大部件组成。运算器就是我们熟知的CPU中的ALU(算术逻辑单元),存储器是内存,控制器是CPU中的控制单元;输入设备就是我们的鼠标键盘等;输出设备就是显示器,打印机等。

1.1历史回顾
计算机的内存寻址技术和世界上的其它事物一样都经历了由简单到复杂,由笨拙到优雅的过程。自我听说计算机到今天,内存寻址方法发生了几次决定性的变革(“史前”的内存寻址方法我连资料都没有找到,真是无据可查了!),而每次变革都带来了软件技术的发展,注入了新鲜血液。

让我们沿着Intel公司的脚步来回顾一下历史吧!(我实在没机会接触除Intel以外的处理器!!!)

1.1.1石器时代
20年前intel推出了一款8位处理器----8080,它有1个主累加器(寄存器A)和6个次累加器(寄存器B,C,D,E,H和L),几个次累加器可以配对(如组成BC, DE或HL)使用来访问16位的内存地址,也就是说8080可访问到64K内的地址空间。另外那时还没有段的概念,访问内存都要通过绝对地址,因此程序中的地址必须进行硬编码,而且也难以重定位,故当时的软件大都是些可控性弱、结构简陋、数据处理量小的工控程序。

人类从来都是不断前进的,很快,几年后intel就开发出了16位的新处理器----8086,这便是内存寻址的第一次飞跃。

1.1.2 青铜时代
8086处理器引入了一个重要概念----段。段描述了一块有限的内存区域,区域的起始位置存在专门的寄存器(段寄存器)中。另外8086处理器可以寻址到1M大的内存空间,因为它的地址线扩展到了20位。可是制造20位的寄存器来存放内存地址在当时显然要比制造20位的地址线难得多。为了克服困难,intel的工程师们想出了个好办法:将内存分为数个64k大小的段,然后利用两个16位值----一个是段地址,另一个是段内偏移量----巧妙组合产生20位的内存地址。换句话说就是把1M大的空间分成数个64k的段来管理(化整为零了)。

系统所需要做的仅仅是:把16位的段地址左移动4位后,再与16位的偏移量相加便可获得一个20位的内存地址,见图1

段基址

偏移

物理地址


图 1

Intel内存地址的描述形式也很贴近上图,采用了“段地址:偏移量”的形式来描述内存地址,比如A815:CF2D就代表段首地址在A815,段内偏移位CF2D。

为了支持段机制,8086为程序使用的代码段,数据段,堆栈段分别提供了专门的16位寄存器CS,DS和SS,此外还给内存和字符串拷贝操作留下了一个目的段寄存器:ES。

段式内存管理带来了显而易见的优势----程序的地址不再需要硬编码了,调试错误也更容易定位了,更可贵的是支持更大的内存地址。程序员开始获得了自由。

1.1.3白银时代
人们的欲望在继续膨胀。intel的80286处理器于1982年问世了,它的地址总线位数增加到了24位,因此可以访问到16M的内存空间。更重要的是从此开始引进了一个全新理念----保护模式。这种模式下内存段的访问受到了限制。访问内存时不能直接从段寄存器中获得段的起始地址了,而需要经过额外转换和检查(从此你不能再随意执行数据段,或向代码段里写东西了,其具体保护和实现我们将在后面讲述)。

为了和过去兼容,80286内存寻址可以以两种方式进行,一种是先进的保护模式,另一种是老式的8086方式,被称为实模式。启动时处理器处于实模式,只能访问1M空间,经过处理可进入保护模式,访问空间扩大到16M,但是要想从保护模式返回到实模式,你只有重新启动机器。还有一个致命的缺陷是:80286虽然扩大了访问空间,但是每个段的大小还是64k,这太糟糕了,程序规模仍然受到压制。因此这个先天低能儿注定命不久也。很快它就被其天资卓越的兄弟----80386代替了。

1.1.4 黄金时代
真正的第二次内存寻址飞跃发生在80386身上,它近乎完美的设计将计算机技术推向了一个新高度。

80386的地址总线扩展到了32位,可寻址空间一下扩充为4G,更为先进的是:在保护模式下,它的段范围不再受限于可怜的64K,可以达到4k-4G。这一下可真正解放了软件工程师,他们不必再费尽心思去压缩程序规模,软件功能也因此迅速提升,一切都走向了繁荣.。



第2章 保护模式
保护模式真得是太精妙了,我恨不得专门用一本书来讨论它,但即使那样我也担心不能真正触其精华。不过还是借用那句老话”简单就是美丽”,我争取用最小的篇幅揭示保护模式的真实面目。

2.1实模式和保护模式
保护模式和实模式好比一对孪生兄弟,它们外貌很像,从程序角度来看几乎看不出什么区别,它们都是通过段寄存器去访问内存地址,都通过中断和设备驱动程序来操作硬件,表面上能感觉得到的差异就是保护模式能访问的空间是4G,而实模式只能访问到1M的地址空间。

但实际上保护模式和实模式采用了两种截然不同的思路,保护模式带来的最可贵的优点不是单纯的扩大了内存寻址范围,而是对内存寻址从机制上提供了保护,将系统的执行空间按权限进行了划分。

这种划分到底会带来哪些好处? 我们来推敲一下吧。

2.2 为什么需要保护?
如果你的机器只允许一个任务使用系统资源,比如说系统内存,那么你完全不需要保护资源,因为系统中再没有什么值得你去偷窥的东西了,更别说去破坏什么了。

可惜那样的时代已经一去不复返了,如今的系统需要支持多个用户同时运行多个任务。为了防止你去偷看别人的任务,或恶意或由于你的荒唐行为而破坏别人的任务,系统有责任将每个任务隔离开来,让大家各自运行在不同的空间,免受干扰。这就是保护的第一个方面----任务间保护。要做到任务间保护需要借助虚拟内存技术(我们后面分析它),其基础之一就是保护模式。

除了任务间保护外,另一个必须保护的东西就是操作系统本身,它可是资源调配的首脑呀!绝不能让你有机可趁,擅自进入。必须有一道铁丝网,将你和操作系统隔离开,使你不得越雷池一步。要想拉起这道铁丝网,就需要借助保护模式中的特权级机制。操作系统放在高特权级里,你的任务被放在低特权级里。你没有权利去偷看操作系统的内容。有什么要求只能请示“领导”(就是保护机制),获得批准后才能给你提供服务。这点可谓是保护模式的最直接应用。

2.3 谁赋予它保护能力?
80386之所以能有变化多端的保护手段,追其根本源自保护模式下内存寻址方式发生的革命。传统上我们知道段方式寻址时,是直接从段寄存器中取得的段的首地址,但是在保护模式中是要多经过一次检查手续才能获得想要的段地址。

这里可千万别再说“简单就是美了”,多了这一次中间倒手过程可是保护模式下寻址的关键技术所在呀。倒手的原因我想大概是因为,虽然80386有的通用寄存器(EAX,EDI等等)被扩充倒了32位,但是其中的段寄存器(DS,ES等)仍然只有16位,显然不可能再用16位的段寄存器直接存放4G空间需要的32位地址了,所以必须引入了一种间接办法----将段寄存器中存放的地址换成一个索引指针,寻址时不再是从段寄存器中去寻址,而是先取指针,再通过该指针搜索一个系统维护的“查找表”读出所需段的具体信息。剩下的动作和传统行为没什么区别,将刚刚取得的段的基地址加上偏移量便构成了一个32位地址(即,线性地址)。



段选择子



偏移



基地址

段界限

基地址

段界限



段界限



基地址



段1

段 2

段描述符号

线性空间

0

4G


图 2



线性地址属于中间地址,它还需要一次转换才能映射到实际的物理地址上(下面会看到)。线性地址组成的空间称为线性空间,它和物理地址空间结构相同,都为32位,最大可达4G。



这个索引指针被称作是段选择子(见图2),它共有16位,其中14位用来作为索引,另外2位(RPL)用来作描述请求特权级。通过索引从表中获得的信息,被称为段描述符,它含有段的相关地址信息等。

改变寻址方法的另一个原因主要是为了完成保护使命。多用户多任务环境下,内存寻地工作不再是简单地取得32位的内存地址就可以直接不假思索地放到地址总线上去读写内存了,此刻必须先要对需访问的地址进行合法性检查,看看访问者是不是有权利去访问它要求的地址。如果发现有非法访问企图,则立刻阻止(CPU会产生一个异常)这种危险行为。读到这里,多数的朋友一定要问,靠什么进行检查请求的合法性呢?更细心的朋友还会继续问,检查需要什么信息?这些信息放在那里?

考虑到寻址过程和合法性检测过程需要在同一现场一起进行,所以最理想的办法是能把段地址信息和检测合法性用到的属性信息放在一起(需要的空间更大了),于是系统设计师们便把属性信息、段的基地址和界限都糅合在一起,形成了一个新的信息单元----段描述符号,它整整占用了8个字节。显然,寄存器太小,不够存放段描述符,所以段描述符都被统一存在专门的系统段描述符号表中(GTD或LDT)保存。

说到这里,聪明的朋友可能已经能大概猜出段描述符表中的内容是什么了。内容里一定包含了段基地址、段的大小信息、段的属性信息,而且在属性信息里还包含了和访问权限有关的信息。的确如此,下面图示描述了段描述符的详细信息,其中和保护关系最大的信息要数RPL了(见图3)。



索引

TI



RPL

段选择子

RPL 请求特权级




图3



这种间接寻址方式不仅体现在普通任务寻址上,而且对于中断处理同样适用。传统上中断处理查询方法是在中断产生后,CPU会在中断向量表中搜索中断服务例程(ISR)的地址,地址形式还是段+偏移量。在保护模式中中断产生后,CPU会从中断描述符表(IDT)中根据中断号取得中断服务例程的段选择子和偏移量,然后通过段选择子从段描述符表(GDT)中获得ISR的段信息,再结合偏移量得到需要的实际物理地址。

中断寻址过程如图5所示。

INT

选择子

偏移量

选择子

偏移量

选择子

偏移量

基地址

基地址

基地址

基地址

基地址

+

中断服务程序

































图 4

2.4 特权等级
计算机世界和人类世界一样,最初是没有等级之分的,但当人类社会物质文明逐步发达后,等级也随之而来了;同样,当计算机上的应用软件越来越丰富后,这个虚拟世界也逐渐形成了级别和等级。我们不去评价人类社会的等级制度,只来看看计算机世界中的等级制度,而且只限于保护模式中的等级制度。

80386中共规定有4个特权级,由0到3。0级别权限最高,3级最小。标准的做法是将操作系统核心运行在0级,应用程序运行在其它几个低级别。不过为了简化操作,往往只会用到0和3两个级别。80386中的每个段描述符号中都有DPL字段,它规定了访问该段的最低特权级,只有高于或等于此特权级别的程序能有权访问它。所以在访问内存地址时要将当前特权级(CPL,一般来说就是当前代码段的特权级别)和被访问段的特权级别比较,如果大于等于才允许访问。

除了处理当前特权级别和段的特权级别外,有时还需要使用请求特权级别(RPL),这个子段出自段选择子,主要用来辅助特权保护。比如可以在访问某个段时,指定其请求特权级,那么特权检查时,规则变为将RPL和CPL中特权更高的那个和被访问段的DPL比较。例如,操作系统中的某个例程会把一些资料写到用户段中。若没有特别检查,那么用户可以把一个 DPL为 0 的 段(用户程序不能存取它)传到操作系统处理例程中,因为系统例程有全权写入DPL为0的段,因此用户程序就可以破坏该段中的资料了。为了避免这个问题,系? API 在存取用户传入的段时,可以先把该段选择子的 RPL设定成和用户程序的 CPL 相同,就不会意外地写入原先用户无权存取的段了。 (但RPL在linux中好像没怎么被用到)



第3章 虚拟内存
虚拟内存可是个怎么强调也不过分的概念,它的存在极大地方便了程序设计任务,彻底解放了程序员的手脚。下面我们就看看虚拟内存的作用以及如何在存储管理机制的基础上实现它。

3.1什么是虚存?为什么需要它?
我们知道程序代码和数据必须驻留在内存中才能得以运行,然而系统内存数量很有限,往往不能容纳一个完整程序的所有代码和数据,更何况在多任务系统中,可能需要同时打开子处理程序,画图程序,浏览器等很多任务,想让内存驻留所有这些程序显然不太可能。因此我们首先能想到的就是将程序分割成小份,只让当前系统运行它所有需要的那部分留在内存,其它部分都留在硬盘。当系统处理完当前任务片段后,再从外存中调入下一个待运行的任务片段。的确,老式系统就是这样处理大任务的,而且这个工作是由程序员自行完成。但是随着程序语言越来越高级,程序员对系统体系的依赖程度降低了,很少有程序员能非常清楚的驾驭系统体系,因此放手让程序员负责将程序片段化和按需调入轻则降低效率,重则使得机器崩溃;再一个原因是随着程序越来越丰富,程序的行为几乎无法准确预测,程序员自己都很难判断下一步需要载入哪段程序。因此很难再靠预见性来静态分配固定大小的内存,然后再机械地轮换程序片进入内存执行。系统必须采取一种能按需分配而不需要程序员干预的新技术。

虚拟内存[2]技术就是一种由操作系统接管的按需动态内存分配的方法,它允许程序不知不觉中使用大于实际物理空间大小的存储空间(其实是将程序需要的存储空间以页的形式分散存储在物理内存和磁盘上),所以说虚拟内存彻底解放了程序员,从此程序员不用过分关心程序的大小和载入,可以自由编写程序了,繁琐的事情都交给操作系统去做吧。



3.2实现虚拟内存
虚拟内存是将系统硬盘空间和系统实际内存联合在一起供进程使用,给进程提供了一个比内存大得多的虚拟空间。在程序运行时,只要把虚拟地址空间的一小部分映射到内存,其余都存储在硬盘上(也就是说程序虚拟空间就等于实际物理内存加部分硬盘空间)。当被访问的虚拟地址不在内存时,则说明该地址未被映射到内存,而是被存贮在硬盘中,因此需要的虚拟存储地址随即被调入到内存;同时当系统内存紧张时,也可以把当前不用的虚拟存储空间换出到硬盘,来腾出物理内存空间。系统如此周而复始地运转----换入、换出,而用户几乎无法查觉,这都是拜虚拟内存机制所赐。

Linux的swap分区就是硬盘专门为虚拟存储空间预留的空间。经验大小应该是内存的两倍左右。有兴趣的话可以使用 swapon -s 查看交换分区大小,还可以用vmstat 查看当前每秒换入换出的数据大小(在si/so字段下)

大道理很好理解,无非是用内存和硬盘空间合成为虚拟内存空间。但是这一过程中反复运行的地址映射(虚拟地址映射到物理地址)和虚拟地址换入换出却值得仔细推敲。系统到底是怎么样把虚拟地址映射到物理地址上的呢?内存又如何能不断地和硬盘之间换入换出虚拟地址呢?

利用段机制能否回答上述问题呢?我们上面提到过逻辑地址通过段机制后变为一个32位的地址,足以覆盖4G的内存空间,而系统内存一般也就几百M吧,所以当程序需要的虚拟地址不在内存时,只依靠段机制很难进行虚拟空间地换入换出,因为不大方便把整段大小的虚拟空间在内存和硬盘之间调来调去(老式系统中,会笨拙地换出整段内存甚至整个进程,想想这样做会有那些恶果吧!)。所以很有必要寻找一个更小更灵活的存储表示单位,这样才方便虚拟地址在硬盘和内存之间调入调出。这个更小的存储管理单位便是页(4K大小)。管理页换入换出的机制被称为页机制。

因为使用页机制的原因,通过段机制转换得到的地址仅仅是作为一个中间地址----线性地址了,该地址不代表实际物理地址,而是代表整个进程的虚拟空间地址。在线性地址的基础上,页机制接着会处理线性地址映射:当需要的线性地址(虚拟空间地址)不在内存时,便以页为单位从磁盘中调入需要的虚拟内存;当内存不够时,又会以页为单位把内存中虚拟空间的换出到磁盘上。可见,利用页来管理内存和磁盘(虚拟内存)大大方便了内存管理的工作。毫无疑问,页机制和虚拟内存管理简直是“绝配”。

使用页机制,4G空间被分成2的20次方个4K大小的页面(页面也可定为4M大小),因此定位页面需要的索引表(页表)中每个索引项至少需要20位,但是在页表项中往往还需要附加一些页属性,所以页表项实际为32位,其中12位用来存放诸如“页是否存在于内存”或“页的权限”等信息。

前面我们提到了线性地址是32位。它其中高20位是对页表的索引,低12位则给出了页面中的偏移。线性地址经过页表找到页面基地址后和低12位偏移量相加就形成了最终需要的物理地址了。

在实际使用中,并非所有页表项都是被存放在一个大页表里,因为每个页表项占4个字节,如果要在一个表中存放2的20次方个页表项,就需要4M的连续存储空间。这么大的连续空间可不好找,因此往往会把页表分级存储,比如分两级,那么每级页表只需要4k连续空间了。

两级页表搜索如同看章回小说,先找到在哪一章里,然后在找在该章下的哪一节。具体过程看看下图5。



目录



偏移

页框

偏移

目录项

CR3

页表项

页目录表

页表



图 5

综上所述,地址转换工作需要两种技术,一是段机制,二是页机制。段机制处理逻辑地址向线性地址的映射;页机制则负责把线性地址映射为物理地址。两级映射共同完成了从程序员看到的逻辑地址转换到处理器看到的物理地址这一艰巨任务。

你可以将这两种机制分别比作一个地址转换函数,段机制的变量是逻辑地址,函数值是线性地址;页机制的变量是线性地址,函数值是物理地址。地址转换过程如下所示。



逻辑地址----(段函数)---->线性地址----(页函数)---->物理地址。



虽然段机制和页机制都参与映射,但它们分工不同,而且相互独立互不干扰,彼此之间不必知道对方是否存在。

说了这么多道理,下面我们结合Linux实例简要地看看段页机制如何使用。

3.2.1 Linux中的分段策略
段机制在Linux里用得有限,并没有被完全利用。每个任务并未分别安排各自独立的数据段,代码段,而是仅仅最低限度的利用段机制来隔离用户数据和系统数据----Linux只安排了四个范围一样的段,内核数据段,内核代码段,用户数据段,用户代码段,它们都覆盖0-4G的空间,所不同的是各段属性不同,内核段特权级为0,用户段特权级为3。这样分段,避免了逻辑地址到线性地址的转换步骤(逻辑地址就等于线性地址),但仍然保留了段的等级这层最基本保护。

每个用户进程都可以看到4G大小的线性空间,其中0-3G是用户空间,用户态进程可以直接访问;从3G-4G空间为内核空间,存放内核代码和数据,只有内核态进程能够直接访问,用户态进程不能直接访问,只能通过系统调用和中断进入内核空间,而这是就要进行的特权切换。

说到特权切换,就离不开任务门,陷阱门/中断门等概念。陷阱门和中断门是在发生陷阱和中断时,进入内核空间的通道。调用门是用户空间程序相互访问时所需要的通道,任务门比较特殊,它不含任何地址,而是服务于任务切换(但linux任务切换时并未真正采用它,它太麻烦了)。

对于各种门系统都会有对应的门描述符,和段描述符结构类似,门描述符也是由对应的门选择字索引,并且最终会产生一个指向特定段内偏移地址的指针。这个指针指向的就是将要进入的入口。利用门的目的就是保证入口可控,不至于进入到内核中不该访问的位置(回忆前面讲到的中断服务程序寻址,其中从中断描述符号表中获得的就是中断门的描述符,而描述符则制定了具体的入口位置)。



3.2.2 Linux中的分页策略
我们前面大概谈了谈为什么要使用分页,这里看看linux中如何使用分页。

Linux中每个进程都会有各自不同的页表,也就是说进程的映射函数互不相同,保证每个进程虚拟地址不会映射到相同的物理地址上。这是因为进程之间必须相互独立,各自的数据必须隔离,防止信息泄漏。

另外需要注意的是,内核作为必须保护的单独部分,它有自己独立的页表来映射内核空间(并非全部空间,仅仅是物理内存大小的空间),该页表(swapper_pg_dir)被静态分配,它只来映射内核空间(swapper_pg_dir只用到768项以后的项----768个页目录可映射3G空间)。这个独立页表保证了内核虚拟空间独立于其他用户程序空间,也就是说其他进程通常状态下和内核是没有联系的(在编译内核的时候,内核代码被指定链接到3G以上空间),因而内核数据也就自然被保护起来了。

那么在用户进程需要访问内核空间时如何做呢?

Linux采用了个巧妙的方法:用户进程页表的前768项映射进程空间(<3G,因为LDT 中只指定基地址为0,范围只能到0xc0000000),如果进程要访问内核空间,如调用系统调用,则进程的页目录中768项后的表项将指向swapper_pg_dir的768项后的项,所以一旦用户陷入内核,就开始使用内核的页表swapper_pg_dir了,也就是说可以访问内核空间了。页机制就说到这里,许多地方是需要网友们自己感悟的。



--------------------------------------------------------------------------------

[1] 传说他16岁开始研究相对论,虽然英年早逝,但才气纵横逻辑学,物理学,数学等多个领域,尤其是在数理逻辑上的所作所为奠定了现代计算技术的理论基础。后来以他的名字命名的“图灵”奖被看作计算机学界的最高荣誉。

[2] .之所以称为虚拟内存,是和系统中的逻辑内存和物理内存相对而言的,逻辑内存是站在进程角度看到的内存,因此是程序员关心的内容。而物理内存是站在处理器角度看到的内存,由操作系统负责管理。虚拟内存可以说是映射到这两种不同视角内存的一个技术手段。
上半期我们一起学习了I386体系结构,下半期我们的主要目标是实现一个能启动而且可以进入保护模式的简易操作系统。所以本期首先来分析一下计算机的启动流程,然后着手学习开发一个基于I386体系的可启动系统。



一、启动流程
我们先来分析系统的启动流程吧!

1.硬件准备
计算机加电的瞬间,整个系统包括RAM在内的几乎所有部件,都处于一种随机的混乱状态,所以根本谈不上做什么实际工作。因此,启动机器首先得将系统带出这种黑暗和混乱的状态。。

硬件在这个时候必须责无旁贷,挺身而出。启动一开始,硬件系统(是主板吧!)就会给CPU的Reset的管脚发送一个信号,促使CPU将自己的主要寄存器(包括cs和eip)设置为启动状态──实际上就是把这些寄存器的内容设置成一些预定值(cs=F00h,eip=FFF0h)。我们都知道指令寄存器(eip)决定系统下一步要执行什么,所以指令寄存器被预置成一个固定值,那么该固定位置上的指令就会被执行----在这里eip指向了位于物理地址0xFFFFFFF0上的指令,所以该指令便作为系统加电后执行第一条指令了。

硬件系统通常会把一个只读芯片的存储区映射到这个位置上(0xFFFFFFF0)──这种芯片一般来说就是ROM。所以ROM中存放的程序就会被调用执行----换句话说就是机器加电后首先执行ROM中存放的程序,传统上称该程序为BIOS(Basic Input/Output System)。

2.BIOS的作用
按照启动流程,该谈谈BIOS了。

BIOS是固化在机器ROM芯片中的一小片程序,在机器启动后会首先执行这片代码。那么这段代码到底是做什么的呢?为何要首先执行它呢?



要回答这个问题,就要想想计算机的启动过程。也许你会不加思索地回答说,启动就是计算机从磁盘里调入操作系统,然后开始运行它。豪无疑问,你的回答是正确的,但是你忽略了一点----机器发现磁盘,找到操作系统在磁盘中位置这个“小”过程。显然,这个过程不能借助于磁盘中的程序来完成,因为还没找到磁盘啦,所以机器自己必须“留一手”,用本身“自带”的程序来找磁盘,读磁盘数据。这一手就是BISO中的程序片。

别小看这小片程序,它可是关系到系统能否正常启动的关键数据呀,所以必须保护它不会被篡改,这也正是为什么要把它固化在 ROM中的原因。一般情况下BIOS中的数据不会被破坏,但也有些特例,比如陈英豪搞的CIH病毒,这个可恶的病毒会破坏BISO数据(只能破坏那些可写入的几款BISO),造成你的系统无法启动。



具体地讲,BISO 中的程序包含POST(加电自检)、开机菜单设置、装载引导扇区和 BISO中断等几部分程序片。

l POST会在开机后首先运行,去检测内存大小和初始化硬件设备。对于基于PCI总线的体系结构,这个过程很关键,因为只能通过这个过程来保证所有的硬件设备不会因为中断或I/O端口等资源发生冲突。这个过程完成时,会在屏幕上显示系统中PCI设备的清单。

l 接着启动菜单(setup menu)被执行。启动菜单程序为用户提供设置系统参数(如时钟,启动设备等)的配置界面(多数情况下,用户开机时按下”DEL”键盘,会进入配置界面)。

l 再下来就是从启动设备中装载引导扇区(第0个扇区),即加载第0个扇区,载入位置固定在0000:7C00处(这是个默认的规定)。

l 而ROM中的 BISO中断程序则是用来控制屏幕、键盘、磁盘驱动、串口等简单驱动设备的。引导扇区被载入时就会很频繁地调用这些中断来读取磁盘扇区数据 (这些中断在系统启动后,不一定再继续被使用,比如Linux系统中就会重写这些中断程序,不过也有系统仍然使用BISO中断,比如DOS系统)。



总结一下,BISO首先会执行自检程序,然后可按用户需要进入启动菜单进行系统配置;如果不需要配置则去启动设备载入引导扇区。到此BISO的任务完成了,剩下的启动任务都交接给引导扇区中程序去继续完成吧。总而言之,BISO要做的就是找到启动设备(启动分区所在设备),然后从中读取第0个扇区(启动扇区了)就OK了。



现在问题到了启动扇区了。启动扇区安排在磁盘(软盘或硬盘)的第一个扇区,大小为512个字节,它的标志是末尾两位是0x55和0xAA----BISO当检测到这两个字节时,就会跳转到0000:7C00处执行(如果没检测到,自然不跳了)。



从0000:7C00地址开始,启动任务交给了启动装载(boot loader)程序继续完成,该程序负责将操作系统内核载入系统并完成必须的初始化工作,再把执行权交给操作系统运行。

启动程序包含两部分内容:

l 启动扇区程序(boot sector program)

存于启动设备第0个扇区中的代码被称为启动扇区程序,它占有512个字节,最后两个字为0x55和0xAA,512字节内未用部分填充0。这片程序很小,所以功能只限于抛砖引玉----加载下一阶段程序(通常情况,不绝对! 有时会加载另一个启动扇区程序,比如MBR里的引导扇区;有时可以直接载入紧接着启动扇区的内核,即位于第1个扇区的内核)

l 第二阶段程序[1](second stage program)

第二阶段程就是上面所提到的下一阶段程序,它一般紧跟着启动扇区程序存放(编译时指定)在磁盘。当它被载入内存后,接着就会被执行。第二阶段程序主要负责----加载内核到内存----然后----进行必要的初始化工作,为内核运行做准备。具体要做的工作主要是为进入保护模式作初始化工作。

3.进入保护模式
到了谈谈进入保护模式的时候了!

保护模式并不是你想进就能进入的,在进入之前你必须作些准备:

1. 建立一个可用的全局描述符号表GDT,这个表里最好能填入系统所要使用的代码和数据段描述符号。

2. 关闭中断。别在进入保护模式时被外界打断。

3. 将GDT装入GDTR寄存器(原因自己去看书吧)。

4. 设置机器状态寄存器MSW 的PE位,表示开启保护模式。

5. 将数据段和堆栈段选择子装入到DS段寄存器中。

6. 进行一个远跳转(装入CS 和IP/EIP) 来将32位的代码选择子装入CS,至此进入保护模式。



进行跳转是为了清除芯片上的预取指令及译码序列,因为PE置位后,机器从实地址方式转入了保护方式,而预取及译码的指令不再有效,为了避免机器错误寻址,必须进行一条段间跳转指令,来废弃预取的译码序列。



到此为止,内核运行环境才被真正建立起来了,内核主程序要做的事情剩下初始化页表(启动分页机制,映射内存)、初始化系统中断\异常表等(建造IDT,设置中断、异常入口)、初始化设备(注册设备驱动程序,初始化外设)、启动网络、文件系统等等,总之是为运行应用程序继续做准备工作(有关内核启动后的工作可参见第一期的第四部分中初始化基本的操作环境一节)。



二.简易操作系统代码
好了,多说无益,我们一定要把上面的概念反映到代码里来理解!下面我们就来看看怎样开发一个可启动的操作系统吧!



我们参照Linux系统的代码组织结构来开发一个小小的操作系统(准确地说它不是操作系统,它仅仅走了一个简易的系统启动流程),它能带你进入保护模式,完成一些系统需要的初始化工作,然后能给你一个启动后通常能见到的假界面----因为该界面还不能和你交互,所以说是个假界面(没有实现SHELL等功能)----我们希望通过该系统能让大家实地感受一下系统启动的过程,也希望通过这个过程能让大家认识操作系统的原始开发过程。

和Linux一样我们将试验系统(就叫它SagaLinux吧)的启动部分代码分为boot.s(引导扇区中的汇编代码[2],负责载入setup.s到内存----就是启动扇区程序)和setup.s(负责继续载入内核,并进行一定初始化和进入保护模式----就是第二阶段程序)两部分,而且将它们置于boot目录下;也和linux一样,我们将内核代码仍叫作kernel.c,并将它置于目录kernel下,它的功能弱得只能用来演示流程,而没有任何实际用途----它建立了4个页表,映射16M空间;初始化了中断表;注册了键盘中断,使得可响应键盘命令;然后清屏幕(洗了脸来等你看它,呵呵)。

代码在这里不详细分析了,这里仅仅给出它的执行过程图







转移0扇区(0x7c00h->0x90000h)

读0扇区到0x7c00h

执行启动扇区代码(90000h--90200h

载入setup(位于90200--90800h)

boot.s

BISO完成

扇区1-3

扇区0





































boot.s流程图







载入kernel(位于70000h-74000h)

打开A20地址位

建立GDT(位于80000h)

建立GDT(位于80000h)

初始化中断控制器(pic)

进入保护模式(跳入代码段内7000h处)

扇区5-37

代码段(0-16M)

数据段(0-16M)

Setup.s













































setup.s流程图

建立页表并初始化

建立IDT表并初始化

安装键盘驱动程序

清屏

Kernel.c



kernel.c流程图





就到这吧,有空去看看试验代码。结合上面的讲解,好好琢磨琢磨源代码,我相信你一定能领会操作系统启动过程和I386体系的特点。



要想试试这个系统,就去找个软盘插入软驱,然后执行make ; make bootdisc就会在软盘中写入系统了。去试试用该软盘启动系统吧,看看会有什么结果。



感谢Faik Yalcin Uygur !我们的启动试验代码就是借用他的系统源代码----我仅仅修改了名字。



--------------------------------------------------------------------------------

[1]对于可动态配置的启动程序,比如lilo和grub,在加载内核前还包含一个用户接口程序,提供用户选择启动选项的能力。



[2] 因为启动扇区被装载到内存时,CPU是处于实模式的,因此程序规模等受到极大限制,访问空间被限制在1MB之内。但是GCC并不关心程序是在实模式还是在保护模式,用它编译出来的程序很难在实模式下运行,因此多数代码都使用汇编代码完成。

实验代码下载:

45 0;} /* Style Definitions */ p.MsoNormal, li.MsoNormal, div.MsoNormal {mso-style-parent:""; margin:0cm; margin-bottom:.0001pt; mso-pagination:widow-orphan; font-size:12.0pt; font-family:宋体; mso-bidi-font-family:宋体; color:black;} h1 {mso-margin-top-alt:auto; margin-right:0cm; mso-margin-bottom-alt:auto; margin-left:0cm; mso-pagination:widow-orphan; mso-outline-level:1; font-size:24.0pt; font-family:宋体; mso-bidi-font-family:宋体; color:black; font-weight:bold;} a:link, span.MsoHyperlink {color:blue; text-decoration:underline; text-underline:single;} a:visited, span.MsoHyperlinkFollowed {color:blue; text-decoration:underline; text-underline:single;} p.MsoDocumentMap, li.MsoDocumentMap, div.MsoDocumentMap {mso-style-noshow:yes; margin:0cm; margin-bottom:.0001pt; mso-pagination:widow-orphan; background:navy; font-size:12.0pt; font-family:宋体; mso-bidi-font-family:宋体; color:black;} p {mso-margin-top-alt:auto; margin-right:0cm; mso-margin-bottom-alt:auto; margin-left:0cm; mso-pagination:widow-orphan; font-size:12.0pt; font-family:宋体; mso-bidi-font-family:宋体; color:black;} address {color:black;} @page Section1 {size:595.3pt 841.9pt; margin:72.0pt 90.0pt 72.0pt 90.0pt; mso-header-margin:42.55pt; mso-footer-margin:49.6pt; mso-paper-source:0;} div.Section1 {page:Section1;} -->

不好意思,要看完整内容请点开头的网址.

[ 本帖最后由 ipitx 于 2008-11-12 19:41 编辑 ]
文章评论

共有 3 条评论

  1. flytiger 于 2009-12-27 18:35:59发表:

    不错,讲得很透彻

  2. wangdu2002 于 2008-11-12 23:07:40发表:

    楼主高人,学习了!

  3. Kttlon 于 2008-11-12 20:38:43发表:

    有意思