I386的体系结构(上)

I386的体系结构(上)

摘要:本期上半部分将和网友一起聊聊I386体系结构,认识一下Intel系统中的内存寻址和虚拟内存的来龙去脉。下半部分将实现一个最最短小的可启动内核,一是加深对i386体系的了解,再就是演示系统开发的原始过程。作为实例我们将分析计算机的启动流程,然后着手学习开发一个基于I386体系的可启动系统。

内存寻址

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

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

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

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

历史回顾

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

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

石器时代

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

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

青铜时代

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。

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

白银时代

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

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

黄金时代

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

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

 

保护模式

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

实模式和保护模式

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

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

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

为什么需要保护?

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

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

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

谁赋予它保护能力?

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)保存。

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

   

索引

TI

 

RPL

段选择子

RPL 请求特权级

 

                图3

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

中断寻址过程如图4

INT

选择子

偏移量

选择子

偏移量

选择子

偏移量

基地址

基地址

基地址

基地址

基地址

+

中断服务程序


                                     图 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好像没被怎么用到)

 

虚拟内存

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

什么是虚存?为什么需要它?

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

    虚拟内存[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实例简要地看看段页机制如何使用。

Linux分段段策略

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

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

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

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

 

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] .之所以称为虚拟内存是和系统中的逻辑内存和物理内存而言的,逻辑内存是站在进程角度看到的内存,因此是程序员关心的内容。而物理内存是站在处理器角度看到的内存,由操作系统负责管理。虚拟内存可以说是这映射这两种不同视角内存的一个技术手段。

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值