操作系统学习常见疑惑问与答[接口规范部分]
—问题1- 4
这里讨论的操作系统是指基于IA构架80386以上CPU的操作系统,并不包括其他计算机构架之上的操作系统或嵌入式操作系统。
1、问:操作系统如何能够开始控制一台计算机,操作系统的起点是什么?
答:操作系统的“开端”一般被认为是引导程序(Boot),它是操作系统程序中最早被计算机硬件系统加载入内存并执行的部分,引导程序一般规定长度为512个字节,就是从这512字节开始,操作系统被一步步装载入计算机内存,进而最终控制整台计算机。那么引导程序是如何被找到并加载入计算机内存的呢?这里涉及到硬件厂商和软件厂商的协调。协调的结果如下:当计算机加电启后,首先转去执行BIOS中的程序进行硬件自检,如果自检成功,则开始尝试在可引导介质中依次寻找引导程序,可引导介质就是我们计算机的软盘驱动器,光盘驱动器以及硬盘等存储设备,搜索的顺序是按照BIOS里设置的引导顺序进行的。
对于软盘来说,计算机会检查软盘的0面0磁道0扇区(被称为boot sector引导扇区),由于每个扇区512字节,因此刚好容纳下引导程序。如果该扇区最后两个字节依次是55H和AAH,那么就表明该扇区存储的是一段引导程序,进而由BIOS程序将这512个字节依次复制到0X7C00开始的内存单元,然后计算机会跳转到地址0X7C00处执行。至此,计算机真正开始执行我们自己编写的代码了。因为512字节很少,无法利用它做一些复杂的事,所以通常将这512字节的代码用来从存储器载入另一个程序,这个程序不受512字节的限制,因此可以用它来做一些载入系统内核,初始化操作系统的工作,通常这个程序被称为loader。
与此相类似,硬盘Boot Sector也就是硬盘的第一个扇区,它由MBR(Master Boot Record), DPT(Disk Partition Table) 和 Boot Record ID三部分组成。 MBR又称为主引导记录,占用Boot Sector的前446个字节(0~0x1BD),存放系统主引导程序(它负责从活动分区中装载并且运行系统引导程)。 DPT即主分区表占用64个字节(0x1BE~0x1FD),记录磁盘的基本分区信息。主分区表分为四个分区项,每项16个字节,分别记录每个主分区的信息(因此最多可以有四个主分区)。 Boot Record ID即引导区标记占用两个字节(0x1FE~0x1FF),对于合法引导区,它等于0xaa55,这是判别引导区是否合法的标志),合法引导区将会被计算机加载到0x7c00处,并执行之。
由此可见,引导程序有以下特点:软盘中的引导程序应该少于510字节(除去引导标志0x55aa的两个字节),事实上有些时候会更少,因为某些文件系统需要在引导扇区添加若干信息,将占去一些字节;硬盘上的引导程序应少于446字节;引导程序有最大字节限制,但没有最小字节限制,引导程序和其他数据(如ox55aa标志,硬盘分区表等数据)加起来不足512字节的空间需要用数据填充,但应保证程序不会执行到这些数据。
光盘或USB盘的启动过程没有研究过,希望有研究的朋友指点^_^.
2、问:显卡有自己的显存,内存ram中也有一块叫显存,比如A0000开始的那一段,我想问一下这两个显存是一码事吗?
答:要弄清楚这个问题,首先要知道什么是地址映射,为什么平时我们说内存地址空间,而不是说内存空间。因为实模式下,20根地址线,最多可以寻址1M内存,也就是我们有1M的地址资源,这1M的地址我们除了用来寻址物理内存外,还要用它来寻址访问bios rom,还有各种外设的rom(因为计算机体系中并没有提供其他的手段来访问这些rom).这样,1M的地址并不是所有都对应到了物理内存上,有的地址对应到了BIOS的rom,有的则对应到了显卡的显存里。比如A0000开始的一段地址范围被用来寻址图形模式的显存,而B8000开始的一段地址范围用来寻址字符模式的显存。当你用这些范围的地址进行读写“内存”的时候,实际上并没有访问到物理内存,而是被定位到显卡的显存上去了,也就是说在当前这种状况下,这些地址对应的物理内存单元就没用了,因为访问被重定向了,所以永远不可能被访问到。由此也解释了为什么要用“地址空间”这个术语,而不是用内存空间,在这里,我们能用连续的地址,但这些地址并不是都到物理内存上去寻址,而是被各个外设的rom和物理内存瓜分了。所以问题中“内存ram中也有一块叫显存”本身就是错误的说法,这个时候的地址已经不对应着内存了。以下附上PC系统启动时的地址映射安排:
3、DPL,RPL,CPL 之间的联系和区别是什么?RPL和CPL是必须相同吗?如果相同,为什么要釆用两个而不改用一个呢?
答:特权级是保护模式下一个重要的概念,CPL,RPL和DPL是其中的核心概念,查阅资料无数,总结如下:
简单解释:
CPL是当前进程的权限级别(Current Privilege Level),是当前正在执行的代码所在的段的特权级,存在于cs寄存器的低两位。
RPL说明的是进程对段访问的请求权限(Request Privilege Level),是对于段选择子而言的,每个段选择子有自己的RPL,它说明的是进程对段访问的请求权限,有点像函数参数。而且RPL对每个段来说不是固定的,两次访问同一段时的RPL可以不同。RPL可能会削弱CPL的作用,例如当前CPL=0的进程要访问一个数据段,它把段选择符中的RPL设为3,这样虽然它对该段仍然只有特权为3的访问权限。
DPL存储在段描述符中,规定访问该段的权限级别(Descriptor Privilege Level),每个段的DPL固定。当进程访问一个段时,需要进程特权级检查,一般要求DPL >= max {CPL, RPL}
下面打一个比方,中国官员分为6级国家主席1、总理2、省长3、市长4、县长5、乡长6,假设我是当前进程,级别总理(CPL=2),我去聊城市(DPL=4)考察(呵呵),我用省长的级别(RPL=3 这样也能吓死他们:-))去访问,可以吧,如果我用县长的级别,人家就不理咱了(你看看电视上的微服私访,呵呵),明白了吧!为什么采用RPL,是考虑到安全的问题,就好像你明明对一个文件用有写权限,为什么用只读打开它呢,还不是为了安全!
全面解释:
RPL是段选择子里面的bit 0和bit 1位组合所得的值,但这里要首先搞清楚什么是段选择子,根据Intel 的文件(IA-32 IntelR Architecture Software Developer's Manual, Volume 3System Programming Guide)它是一个16Bit identifier (原文:A segment selector is a 16-bit identifier for a segment). 但 identifier 又是什么. identifier 可以是一个变数的名字( An identifier is a name for variables), 简单的说它可以就是一般意义的变数. 这里 16-bit identifier for a segment 可以就是一个一般意义的16bit变数但同时要求对它的值解释的时候必须跟据Intel定下的规则---也就是bit 0和bit 1位的组合值就是RPL等等… 因此在程序里如果有需要的话你可以声明一个或者多个变数来代表这些段选择子,这样的话你的程序在某一时刻就可以有很多段选择子,当然有那么多段选择子就有那么多RPL.可以这样说程序有多少个是RPL是你怎样看待你自己声明的变数. |
程序的CPL(CS.RPL)是CS register 里bit 0和bit 1 位组合所得的值.在某一时刻就只有这个值唯一的代表程序的CPL.
注:CS.RPL代表CS积存器中的Register Privilege Level,并不是一般所说的Request Privilege Level.
而DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级. 一个程序可以使用很多段(Data,Code,Stack)也可以只用一个code段等.在正常的情况下当程序的环境建立好后,段描述符都不需要改变-----当然DPL也不需要改变.
一、对数据段和堆栈段访问时的特权级控制:
要求访问数据段或堆栈段的程序的CPL≤待访问的数据段或堆栈段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL,即程序访问数据段或堆栈段要遵循一个准则:只有相同或更高特权级的代码才能访问相应的数据段 (这样才能保护数据不被随意更改,注意这里仅仅是数据段和堆栈段!). 这里,RPL可能会削弱CPL的作用,访问数据段或堆栈段时,默认用CPU和RPL中的最小特权级去访问数据段,所以max {CPL, RPL} ≤ DPL,否则访问失败。
二、对代码段访问的特权级控制(代码执行权的特权转移):
让我们先来记一些“定律”:
所有的程序转跳,CPU都不会把段选择子的RPL赋给转跳后程序的CS.RPL. (段选择子不过是一个给CPU的"引导"作用,具体讲解请各位看官浏览下我前面的保护模式下寻址 , 段选择子在完成了引导作用后的工作就是进程跟新的段的瓜葛了,CPU与代码如何如何那跟段选择子一点儿都没有关系,所以段选择子的RPL不会给CPL,记住,段选择子仅仅一个引导而已!)
转跳后程序的CPL(CS.RPL)只会有下面的俩种可能
转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL)
或转跳后程序的CPL(CS.RPL) = 转跳后程序的CodeDescriptor.DPL
以 Call 为例(只能跳到等于当前特权级或比当前特权级更高的段):
怎样决定这两种选择,这就要首先知道转跳后程序的段是一致代码段还是非一致代码段.其实也很简单,规则如下:
如果能成功转跳到一致代码段, 转跳后程序的CPL(CS.RPL) = 转跳前程序的CPL(CS.RPL),(转跳后程序的CPL继承了转跳前程序的CPL, 一致一致,翻译时一致含义就是这个意思!)
如果能成功转跳到非一致代码段, 转跳后程序的CPL(CS.RPL) =转跳后程序的Descriptor.DPL。(转跳后程序的CPL变成了该代码段的特权级.我在前面提到DPL是段描述符中的特权级, 它的本意是用来代表它所描述的段的特权级)
怎样才能成功转跳?
这里有四个重要的概念:
1).段的保护观念是高特权级不找低特权级办事,低特权级找高特权级帮忙,相同的一定没问题.(这样想逻辑是没错,事实对不对就不知道.) 也就是县长不找乡长,乡长不求农民,反过来农民求乡长,乡长找县长.这个概念是最重要的。
2) 一致代码段的意义: 让客人很方便的利用主人(一致代码段)的东西为自己办事.但客人这身份没有改变NewCS.RPL=OldCS.RPL所以只能帮自己办事。比方说乡长有一头牛,农民可以借来帮自己种田,但不能种别人的田.但是如果你是乡长当然可以种乡里所有的田。
3) 非一致代码段的意义:主人(非一致代码段)可以帮客人但一定是用自己的身份NewCS.RPL= DestinationDescriptorCode.DPL这里可能有安全的问题, 搞不好很容易农民变县长。主人太顽固了一定要坚持自己的身份,有什么方法变通一下,来个妥协好不好。好的,它就是RPL的用处。
4) RPL: 它让程序有需要的时候可以表示一个特权级更低的身份Max(RPL,CPL)而不会失去本身的特权级CPL(CS.RPL),有需要的时候是指要检查身份的时候。事实上RPL跟段本身的特权级DPL和当前特权级CPL没有什么关系,因为RPL的值在成功转跳后并不赋给转跳后的CS.RPL。
还是要问怎样才能成功转跳啦?这里分两种情况:
普通转跳(没有经过Gate 这东西):即JMP或Call后跟着48位全指针(16位段选择子+32位地址偏移),且其中的段选择子指向代码段描述符,这样的跳转称为直接(普通)跳转。普通跳转不能使特权级发生跃迁,即不会引起CPL的变化,看下面的详细描述:
目标是一致代码段:
要求:CPL(CS.RPL)>=DestinationDescriptorCode.DPL ,其他RPL是不检查的。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL)
上面的安排就是概念1,2的意思(底特权的CPL找到了高特圈的DPL办事,一致么,自然CPL不会变了),此时,CPL没有发生变化,纵使它执行了特权级(DPL)较高的代码。若访问时不满足要求,则发生异常。
目标是非一致代码段:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL≤CPL(CS.RPL)
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
上面的安排就是概念3的意思和部分1的意思----主人(一致代码段)只帮相同特权级的帮客人做事。因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级(CPL)也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
通过调用门的跳转:当段间转移指令JMP和段间转移指令CALL后跟着的目标段选择子指向一个调用门描述符时,该跳转就是利用调用门的跳转。这时如果选择子后跟着32位的地址偏移,也不会被cpu使用,因为调用门描述符已经记录了目标代码的偏移。使用调门进行的跳转比普通跳转多一个步骤,即在访问调用门描述符时要将描述符当作一个数据段来检查访问权限,要求指示调用门的选择子的RPL≤门描述符DPL,同时当前代码段CPL≤门描述符DPL,就如同访问数据段一样,要求访问数据段的程序的CPL≤待访问的数据段的DPL,同时选择子的RPL≤待访问的数据段或堆栈段的DPL。只有满足了以上条件,CPU才会进一步从调用门描述符中读取目标代码段的选择子和地址偏移,进行下一步的操作。
从调用门中读取到目标代码的段选择子和地址偏移后,我们当前掌握的信息又回到了先前,和普通跳转站在了同一条起跑线上(普通跳转一开始就得到了目标代码的段选择子和地址偏移),有所不同的是,此时,CPU会将读到的目标代码段选择子中的RPL清0,即忽略了调用门中代码段选择子的RPL的作用。完成这一步后,CPU开始对当前程序的CPL,目标代码段选择子的RPL(事实上它被清0后总能满足要求)以及由目标代码选择子指示的目标代码段描述符中的DPL进行特权级检查,并根据情况进行跳转,具体情况如下:
目标是一致代码段:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL ,RPL不检查,因为RPL被清0,所以事实上永远满足RPL≤DPL,这一点与普通跳转一致,适用于JMP和CALL。
转跳后程序的CPL(NewCS.RPL) = 转跳前程序的CPL( OldCS.RPL),因此特权级没有发生跃迁。
目标是非一致代码段:
当用JMP指令跳转时:
要求:CPL(CS.RPL)=DestinationDescriptorCode.DPL AND RPL<= CPL(CS.RPL)(事实上因为RPL被清0,所以RPL≤CPL总能满足,因此RPL与CPL的关系在此不检查)。若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
因为前提是CPL=DPL,所以转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL不会改变CPL的值,特权级也没有发生变化。如果访问时不满足前提CPL=DPL,则引发异常。
当用CALL指令跳转时:
要求:CPL(CS.RPL)≥DestinationDescriptorCode.DPL(RPL被清0,不检查),若不满足要求则程序引起异常。
转跳后程序的CPL(NewCS.RPL) = DestinationDescriptorCode.DPL
当条件CPL=DPL时,程序跳转后CPL=DPL,特权级不发生跃迁;当CPL>DPL时,程序跳转后CPL=DPL,特权级发生跃迁,这是我们当目前位置唯一见到的使程序当前执行优先级(CPL)发生变化的跳转方法,即用CALL指令+调用门方式跳转,且目标代码段是非一致代码段。
总结:以上介绍了两种情况的跳转,分别是普通跳转和使用调用门的跳转,其中又可细分为JMP跳转和CALL跳转,跳转成功已否是由CPL,RPL和DPL综合决定的。所有跳转都是从低特权级代码向同级或更高特权级(DPL)跳转,但保持当前执行特权级(CPL)不变,这里有点难于区别为什么说向高特权级跳转,又说特权级没变,这里“高特权级”是指目标代码段描述符的DPL,它规定了可以跳转到该段代码的最高特权级;而后面的CPL不变才真正说明了特权级未发生跃迁。我们可以看到,只有用CALL指令+调用门方式跳转,且目标代码段是非一致代码段时,才会引起CPL的变化,即引起代码执行特权级的跃迁,这是目前得知的改变执行特权级的唯一办法,如果各位读者还知道其他方法请留言告诉我。
以上解释参考了OldLinux BBS的帖子http://www.oldlinux.org/cgi-bin/LB5000XP/topic.cgi?forum=18&topic=73&show=0
4、问:为什么全局描述符表GDT的第0项总是一个空描述符,而局部描述符表却不是这样?
答:首先让我们先来熟悉一下概念。一个任务(Task )通常会涉及多个段,每个段需要一个描述符号来描述(当然不是绝对的一对一关系,一个段也可以由多个段描述符来对应,视具体应用而定),为了便于组织管理,80386把描述符组织成线性表。由描述符组成的线性表称为描述符表。在80386中有三种类型的描述符表(Descriptor Table),分别是全局描述符表GDT(Global Descriptor Table),局部描述符表(Local Descriptor Table)和中断描述符表IDT(Interrupt Descriptor Table)。在整个系统中,全局描述符表GDT和中断描述符表IDT只有一张,局部描述符表可以有若干张,每个任务可以有一张。
在实模式下,逻辑地址是由段基址和段内偏移构成的。保护模式下,规则发生了很大变化,虚拟地址空间(相当于逻辑地址空间)中存储单元的地址由段选择子和段内偏移两部分组成,与实模式相比,段选择子代替了原来的段基址。从本质上来讲,段选择子最终还是要转化成段基址,那么选择子是如何转化为段基址的呢?让我们来看看选择子的结构和用法:
段选择子长16位,其格式如上图所示。从图中可见,段选择子的高13位是描述符的索引值。所谓描述符索引是指描述符在描述符表中的序号。由于描述符总是8个字节的,所以将描述符索引值逻辑左移3位即可得到对应描述符在描述符表中的偏移地址,再加上描述符表起始地址就可以确定描述符的位置,这算是一个小技巧。段段选择子的第2位是引用描述符表指示位,标记为TI(Table Indicator),TI=0表示该选择子指示的是全局描述符表GDT中的描述符,TI=1表示该选择子指示的是局部描述符表LDT中的描述符。第0和第1位称为RPL(Request Privilege Level请求特权级),用于特权级控制,在上一个问题中有详细描述。通过段选择子,我们可以从GDT或LDT中找到需要的段描述符,段描述符中存储着目标段的基址(起始地址),界限(段的范围)以及其他一些控制信息,由此,我们完成了段选择子到段基址的转化。
到这里,我们似乎离开题目太远了,请不要急,我们惟有将基本的概念陈述清楚,才能将问题回答透彻,那么现在开始回答问题。
如前所述,描述符的线性表构成了GDT,LDT,IDT,这些描述符并非都是用来描述数据段或代码段的,有的可能用来指示一个任务,比如任务门描述符,用于任务切换;有的用来指示子程序,比如调用门;还有的用来指示中断处理程序,如中断门、陷阱门等,当然,中断门和陷阱门只存在于IDT中。除此之外,由于CPU把局部描述符表LDT也当作数据段来管理,所以要求每一个LDT都必须有相应的描述符存在于GDT中,暂且称之为LDT描述符。由此可见,GDT、LDT和IDT是由各种不同种类的描述符排列构成的,他们的组成数据各不相同,作用也不同,唯一相同的是他们都是8字节的,都存于描述符表中,都用选择子来定位(中断门和陷阱门用INT 指令后的数字来做描述符索引)。
前面说到GDT和IDT是整个系统一张,而LDT可以每个任务独占一长,用于存储每个任务私有的段的信息,所以当任务发生切换时,LDT也要随之切换,CPU中专门用一个16位的寄存器LDTR来存储当前任务的LDT在GDT中的描述符的选择子,以此来定位当前任务的LDT。同时也存在这么一种情况,那就是一个任务使用的所有段都是系统全局的,它不需要用LDT来存储私有段信息,因此,当系统切换到这种任务时,会将LDTR寄存器赋值成一个空(全局描述符)选择子,选择子的描述符索引值为0,TI指示位为0,RPL可以为任意值,用这种方式表明当前任务没有LDT。这里的空选择子因为TI为0,所以它实际上指向了GDT的第0项描述符,第0项的作用类似于C语言中NULL的用法,它虽然是一个描述符,但却只起到到了标志的作用,规定GDT的第0项描述符为空描述符,其8个字节全为0,就是这个原因。如果把前面的空描述符选择子的TI位改为1,使之指向LDT中的0号描述符,这样的选择子就不是空选择子,它指向的LDT中的0号描述符是可以正常使用的,也就是LDT中没有空描述符一说。