我看保护模式

        保护模式,对于学过操作系统的人来说,是一个熟悉而又陌生的概念。之所以熟悉,就是所有的操作系统的书籍都会说到保护模式;之所以陌生,我敢说很多人学完了之后,压根不晓得什么是保护模式,怎么算是保护模式,再者就是它到底保护了什么东西。

        “你说的不对,怎么能一棒子打死一帮人!保护模式就是进程相互隔离,实现多任务;就是有分页机制,可以节约内存。”,暂且不说对与不对,其实我也不知道我理解的是否是正确的。暂且将自己的一点理解记录一下吧。如果不对,今后发现了,及时纠正吧!闲话少扯,接下来就开始吧。

        现在的计算机的操作系统运行起来之后,无一例外都会进入到保护模式,到底保护模式是什么,它保护了什么呢?要说保护模式,先要从实模式说起,更早的话要从8086开始说了。很多东西现在都看不到了,我也没有仔细研究过。实模式的东西,就把自己看到的一些东西简单说一下,关键在于说保护模式。

        1. 实模式

        最早的个人电脑的CPU是8086,毋庸置疑,它只有实模式,压根不支持什么保护模式。8086为16位的CPU,地址总线为20位,可以寻址1M,数据总线为16位,一次存取一个字节的数据,或者一个字的数据。下图是一个8086的CPU的模型

        

        8086的寄存器都是16位的,它的20位的地址如何获取呢?学过8086汇编的人都晓得,将段寄存器向左移四位,加上偏移地址,就得到了20位的地址,将地址值通过CPU的地址总线就可以访问内存的内容了。16位的寄存器只能访问64K的空间,与1M相比,64k的内存过小,Intel通过 分段寻址的模式,将物理内存访问范围扩大到了1M字节。

        然后再说一下80286 CPU,80286现在看可以说是一个中间产物了,它有实模式和保护模式两个运行模式。在实模式下,兼容了8086CPU,只能访问1M的内存。切换到保护模式,它的地址总线是24位,因此它可以访问16M的内存。但是80286的这个保护模式是一个虚拟保护模式。        

        最后说一下80386 CPU,80386处理器是一个32位的处理器,具有了实模式,保护模式和虚拟8086模式 三个运行模式,完全实现了保护模式。32位的地址总线,可以访问的内存地址范围达到 4G;数据总线32位,一次可以访问4字节的内存。

        保护模式,应该叫做“虚拟地址保护模式”,实现了 内存保护,分段机制保护,硬件虚拟内存存储等机制。(扯一点闲话:这个保护模式是CPU的机制,还是操作系统的机制呢?CPU的!操作系统的!应该这么说吧,Intel的CPU支持了保护模式,然后有了操作系统相应的行为。CPU支持了分页的地址映射机制,然后操作系统有了分页的准备工作,并且有了相应的控制软件/程序)

        留一个问题,CPU可以没有实模式吧?CPU加电后,直接进入32位的保护模式(所谓的)?

        2. 实模式与保护模式的切换

        为了兼容之前的CPU,80X86系列的CPU 都有实模式,然后加上其他的模式(保护模式,V8086模式等)。CPU启动后,首先进入的是实模式(为什么?龟腚!),做一系列的检测。然后进入引导过程,切换到保护模式。(加载内核,初始化内核等等)。那如何从实模式切换到保护模式呢?

        有两个标志位用于在两个模式之间进行切换。

        实模式使用的地址线为 A0-A19 20位地址线,到了286,386地址线被扩展为24位和30位,那么为了兼容8086 CPU,在286,386及其以后的CPU中,A20 就被称为了 A20 Gate,作为开启高地址内存的标记(其实是可以访问更高地址的内存的)。在实模式下A20 Gate被关闭,那么访问范围为1M内存。到了保护模式下,如果A20 Gate被关闭,对于80386处理器(32位地址线)来说,在访问内存时,A20地址线就是无效的,只能当作0处理,这样访问的地址就被分为了 00000-FFFFF, 200000-2FFFFF,300000-3FFFFF等一系列的地址段。所以在进入保护模式时,要将A20地址线打开,就可以访问一系列连续的地址空间了。

        再者,CPU的控制寄存器 CR0的PE位设置为1,表示CPU要以保护模式运行。

        除了着两个位以外,还需要为保护模式设置GDT,LDT等一系列内容,以保证CPU切到保护模式下的正常运行,这个后面来说!

        3. 保护模式

        首先说一下保护模式,保护的含义:

        1. 段基址 / 段界限 定义了一个段范围,段界限之外的内存禁止访问

        2. 段属性对端的定义,规定限制段行为 和性质

        3. 特权级的转换,CPL/RPL/DPL 进行比较,将CPU的CPL 与 段的DPL,以及段,门等的RPL静态定义权限 进行动态的对比,实现系统保护。

        首先说一下保护模式下的内存访问:

        在8086的实模式下访问内存,通过 段地址×16 + 地址偏移 的方式获取20位的地址。而到了保护模式下,段寄存器CS,DS,GS等仍然是16位,而现在的地址总线变为了32位,显然使用这种移位+偏移的方式显然行不通了。那保护模式如何实现这种 [段:偏移] 的寻址呢?答案就在GDT / LDT 上。

        在保护模式下,CS/DS/SS等虽然仍然被称为段寄存器,但是它的函数和实模式下完全不同了,它不再代表真正的段基址,而是一个索引。保存的内容被称为选择子,用于选择GDT/LDT中的一项。选择子的结构如下图所示:

        

        选择子的高13位为描述符索引(就是我们的描述符表中的索引值),低两位为RPL,全称Request Privilege Level,请求权限级别??(暂且这么说吧)用于Ring0~Ring3权限的确认。

        既然段寄存器内容变成了选择子,那么如何寻找段基址呢?答案就是上面提到的GDT/LDT,GDT(Global Descriptor Table)全局描述符表(以GDT为例,LDT类似)GDT表中的元素是全局描述符,它的结构如下图所示:

        

        它有64位,包含的数据一目了然。段基址 1,2 直接组成了32位的地址。段界限 1,2则组成了20位的段界限。DPL两位为描述符的权限,与CPL和RPL完成访问权限的限制,以保护系统或进程。TYPE则表示描述符的类别,包括了存储段描述符,系统段描述符(TSS,调用门,中断门,陷阱门,任务门)。

        可能会奇怪,段界限为20位,那就1M字节啊,这显然不现实啊,32位的基地址,1M的界限,现在的程序一个模块都1M多甚至更大了,这怎么访问啊!不要着急,这个段界限是有单位的,在描述符中,BYTE6的最高位 G 如果为0 表示段界限的粒度为1字节,如果为1时,表示段界限为4k,1M个4K,显然范围编程了4G了。

        举例:

        对于数据访问的保护例子, mov  eax, ds:[ebx]

        这是一个非常平常的一个内存访问的例子,取内存中的四个字节。如何存取呢?执行的当前的代码指令位置为 CS:EIP,CS中保存了代码段的选择子内容,CS的最低两位为CPL,即当前执行代码所处的特权级。EIP为代码段的偏移。要访问的内存为数据段 ( 由DS 指定)中的 EBX 指定偏移处的四个字节。 DS 为要请求的选择子,它的低两位为 RPL,即请求特权级别,DS中保存的选择子会指向GDT中的一个描述符,描述符中有一个DPL(如上所述)。对比 CPL ,RPL与DPL的值,满足DPL>=CPL,DPL >= RPL,且CPL <= RPL。即 当前特权级要高于或等于DS 选择子规定的特权级别,同时 当前的特权级别,以及DS选择子规定特权级别都要高于或等于 DS所指向的描述符中定义的特权级别。(注意 DPL,CPL的值 与他们的特权级别 正相反,即CPL值越小,表明当前执行的特权级别越高),过程如下图所示。

        


        现在问题来了,GDT由谁来创建,系统的每一项描述符如何创建(Windows系统那么大,有那么多的段),一个进程也有好多段,它的描述表如何创建,每一个描述符如何生成呢???(留待以后分解)


        不同特权代码之间的转移:

        上面的任务完成一部分保护工作,就是内存的访问保护。同样,代码的执行也是要保护的,如何保护?代码直接的转移(相互跳转)。Ring3的权限,无法执行权限级别为0的代码。那问题就来了,相互跳转如何实现,如何实现保护?

        代码段间的转移可以由如下的一些指令引起,jmp,call,ret,systementer,sysexit,int n,iret 等。jmp 或 call 可以实现4中转移:

                1. 目标操作数包含了目标代码段的段选择子

                2. 目标操作数指向一个包含目标代码段选择子的调用们描述符

                3. 目标操作数指向一个包好目标代码段选择子的TSS

                4. 目标操作数指向一个任务门,任务门指向包含目标代码段选择子的TSS

        分为两类,直接转移,间接转移。通过jmp/call直接转移,对于非一直代码段,CPL == DPL,RPL <= DPL,也即当前特权级低于目的代码,选择子的权限级别等于或高于目标代码权限级别。对于一直代码,CPL >= DPL,也即当前的特权级别要低于目标代码的特权级。

        间接转移,则是通过门 进行转移。这个时候有两个阶段的权限对比,间接的转移,需要访问门(调用们,中断门,陷阱们等),因此它首先要有门的权限。其次再是和要转移到的代码的特权级别进行比较。

       

        特权级切换:ring0 到 ring3,ring3 到 ring0

        特权级的切换,方法有很多。都需要通过上述的代码转移实现。一方面要满足 跳转的权限要求,同时还需要保存执行的现场。对于特权的切换,如ring3 到ring0的切换,要保存当前执行的 CS:EIP ,确保执行完ring0,返回后可以继续执行;ring3 和 ring0 使用不同的栈,因此需要保存当前栈的SS:ESP,以便返回后,将栈切回到跳转之前的栈状态。当然还有EFLAGS等值。比较典型的有Windows系统上的内核调用,比如I/O


        分页存储:

        80386 CPU支持了分页机制,分页机制也节省内存,一个任务不需要将执行的段完全映射到物理内存即可执行。节省内存,分段情况下有大量的大片的内存碎片,实行分页后,可以将内存碎片降低到4k;同时也为多进程的实现创造有利条件。

        分页机制,将内存空间进行划分,以4K为单位,可以将整个地址空间划分为1M个。每个4K的单位是一页。为了将页表组织起来,可以采用线性结构,那么1M页,每页的首地址为一项(占四字节),那么这个管理结构就需要4M的内存来存储,显然浪费。使用两级的结构,第一级每一项指向一页,第二级的每一页中包含了256项。如果将所有的内存都索引到,需要的更多了,4M+4K,貌似更加不合算,其实在使用中,不需要将所有的内存都索引到,只需要将用到的内存创建对应的页目录即可,这样就可以减少页表占的内存。如下图所示,一个地址通过页目录表,页表项表,以及物理页即可获得对应地址的物理地址。

        

        在CPU中有控制寄存器,就是CR*,其中的CR0寄存器的 PG位表示了CPU是否开启了分页基址。因此在设置号目录表等项后,还需要将CR0的PG位置位,才是正式打开了CPU的分页基址,CPU才会按照上述的方式去获取内存内容。

        另外一个寄存器CR3,CR3中保存了页目录基地址的物理地址。如上图所示,CPU在去内存的内容时,首先从CR3中获取到页目录表的物理地址,在根据虚拟地址值去获取相应的页目录项,页表项,以及对应的物理块。

        在多进程中,每一个进程都会维护一个页表,其中保存了进程用到的内存页的映射。在做进程切换时,要将CR3的值换掉,换成目标进程的CR3值,访问内存是对应所应的也目录表也发生了变化。因此对于同一个地址在不同的进程中,会被映射到不同的物理地址上。

        有一个问题,那么这些也目录如何进行初始化呢,地址空间如何划分,相互如何映射呢?谁来负责规划? 仍然需要探索啊!

        中断和异常:

        在DOS中(实模式),0~1K 的内存空间是中断向量表,每个向量由 CS:IP 格式占用两个字节(一共256项),直接使用中断号索引中断向量的位置,取出CS:IP,即可跳转到对应的地址去,进行中断处理。

        类似于指令和内存访问的CS,DS段寄存器,这里保存的要跳转的CS段基址在保护模式下也无法使用了。如何处理?如何寻找中断处理过程?没错,前面说过了通过映射,这也可以通过映射处理。与GDT的表类似,处理中断和异常,也有一个类似的表,叫做IDT,其中保存了中断描述符,即中断门,陷阱门等。它和我们上面说到的调用们类似(结构如下图),其中包含了 DPL,选择子以及偏移值。

        

        那么中断如何触发呢?说触发之前,首先要说一下硬件中断的映射。在进入保护模式后,0~18 号的中断号已经被Intel CPU默认的除法错 等中断占据了。因此需要将8259A的中断信息重新映射到更高一些的中断号上(具体的映射方法不再介绍了,可以搜索一下资料,有很多)。一般情况下会将硬件中断 映射到 20H 的地方,因为0~31号中断中,19~31是Intel 保留的向量号。32 ~255 的中断号是留给用户,因此将硬件中断重新映射到 20H 开始的中断号上。

        这样,只要触发了中断,无论 int n 还是硬件中断,都可以得到一个中断号。使用这个中断号可以索引到IDT中的 门描述符。根据门描述符,使用其中的选择子,可以获得中断处理过程所在段的描述符,从描述符获取段基址,加上门描述符 中的偏移地址,就得到了中断处理过程的首地址。

        

        同样,在整个过程中,不仅要使用到 中断门等描述符,并且还要访问GDT 表中的段描述符,这个过程中都要进行CPL,RPL与DPL值的对比,确定有权限进行相应的调用操作,或访问操作。

        

        保护模式下的I/O:

        保护模式下的I/O有两个方面的保护:一个是IOPL,类似于DPL,在EFLAGS 寄存器中,第十二,十三位为IOPL的值,要执行IO指令,要满足CPL <= IOPL条件才可以执行,否则执行权限没效果。

        第二个是IO位图,I/O位图开始于TSS中104字节(前104字节为TSS的固定格式,用于保存任务的状态),I/O位图最长可达8K,每一位表示一个字节的端口是否可以访问。

        因此在进行I/O访问时,一方面要判断IOPL的值,同时还要确定要访问的端口,在I/O位图中是否可用。

        


        关于保护模式,肯定不止写的这些内容。但是暂时写这么多吧,也就理解了这么点内容,还是有很多东西没理解透,以后有了新的理解,在添加新的内容。

        By Andy @ 2015-08-02


  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值