概述
386之后,引入了处理器的保护模式,在计算机发展中,这是一个大事件,因为保护模式概念的提出,使现代操作系统成为可能。如果不采用虚拟地址的概念,那么现代操作系统要管理如此庞大的资源,肯定是一个噩梦。
实模式:是指直接访问物理内存,指令中的地址值是物理内存地址。
保护模式:是指指令中的地址不再是物理地址,而是虚拟地址,要经过MMU的处理转换成物理地址。
由于保护模式的提出正在32位系统发展之初,因此,实模式一般为16位系统,而保护模式变成了32位地址,这不是实模式与保护模式的本质区别,只是时间的巧合。
在多任务系统中,其目的就是保护各个任务能独立运行,而不被其他任务影响。386的CPU是怎样处理的呢?
现在假设有A和B两个程序要运行(程序运行后一般就成了操作系统中的一个进程,或者叫一个任务,程序、进程、任务可以理解成一个概念),首先弄清楚程序是什么。
程序 是一段代码和数据组成的文件,其目的是为了完成某个任务,如编辑文件、播放音乐。这段代码是可以独立在CPU中运行的,但是这段代码与操作系统是什么关系呢?
1. 操作系统是程序的管理者, 如果没有操作系统,程序是一个存放在硬盘上的文件,不会自动调入到内存中运行,操作系统可以为程序创建一个合适的运行环境。
2. 操作系统为程序提供一些最基本的功能,如读写磁盘、管理内存分配。而这些功能是以API方式提供给程序调用的,也就是说在程序中会调用大量的操作系统提供的子程序(API),如果没有这些子程序,你向屏幕print一个字符,画一个图形窗口,就需要自己写出这些功能,那编写程序就是个噩梦。
从CPU角度看程序:程序就是小段代码,只是整个大程序的一小部分, 程序的绝大部分是操作系统提供的代码,而程序员只是编写了调用API的小部分代码来完成自己需要的功能。
从内存角度看程序:程序只是一片数据,这片数据中包含了代码和处理的数据,至于代码从哪开始执行,程序之间怎么跳转,由程序代码本身决定。
从操作系统角度看程序:程序仅仅是被本操作系统管理的一段代码和数据,我可以决定把这段代码放到内存的哪个地方,也可以决定什么时候启动这段代码的运行。
第一步,BIOS启动:系统上电后,CPU开始执行第一条程序,CPU永远也不知道哪个是操作系统内核,哪个是用户程序,它唯一会做的就是执行一条指令,并输出结果,然后再从内存的紧接着的单元取下一条指令。而程序的跳转是由指令本身修改指令指针(PC)指向新的内存地址(或者由外部中断或异常引起PC的修改)实现的。CPU上电后,PC从地址0开始执行(或者从其他地址开始装载指令,这取决于CPU的设计),而地址0处必然是一条可执行指令,在个人计算机中,CPU上电是从主板的BIOS中开始执行的,为什么不从其他地方执行呢,这是由硬件设计规则决定的,BIOS是挂接在CPU的地址总线上的。BIOS程序会完成主板的监测和基本初始化,并构建起中断向量表,然后再从硬盘(也可以是其他存储设备)加载第一个扇区内容到内存,从哪里加载引导扇区,可以在CMOS中设定,而对于不同存储设备的读写必须有BIOS的支持,如果BIOS中没有USB设备的读写程序(驱动),那么CMOS中也不会让用户可以选择从USB启动系统。
当BIOS从磁盘加载第一个引导扇区(512字节),之后,会检查其中是否可执行代码,如果不是,那么就报错“不是启动盘”。
第二步,操作系统引导:磁盘的第一个扇区中会有信息指出哪个分区是活动分区,加载活动分区的引导扇区,正式启动操作系统的加载过程。此时是运行于实模式的,因为此时的CPU处于初始化前的状态,寄存器中的值是初始化值,只有更改寄存器的值为某些特定值才能启动保护模式。操作系统的引导程序会初始化基本的硬件系统,如内存控制器、堆栈及其他一些CPU寄存器。然后会把操作系统内核拷贝到内存中,内核在内存中的物理位置有些是固定的,有些是不需要固定的,对于哪些是必须固定在特定地址,这由操作系统设计决定。
第三步,启动保护模式,在进入保护模式前,我们需要做什么准备呢? 在386的保护模式中,GDTR是必须要初始化的,这个寄存器保存了全局描述符表的虚拟地址,注意是虚拟地址,一旦启动保护模式,所有的地址都是虚拟地址,而不是物理地址。什么是全局描述符,这个是由操作系统设计者决定,还是CPU设计者决定呢?这个是CPU设计者决定的,因为GDTR中的描述符格式对于每个操作系统来说都一样,描述符大小为8个字节,有多个个描述符由GDTR的值中的一个域(某部分位)决定。关于GDTR格式可以参考相关文件,而描述符用来做什么呢? 描述符就是用来描述整个内存中放了什么东西,每一项描述了该段内存的起始地址、性质(如是代码还是数据)、长度等。而在其他一些寄存器中,就不再对内存做具体描述,如代码段寄存器CS,其值(16位值)仅仅包含一个索引值(如08表示在GDT表中的第8项)。
还有一个寄存器CR3,是页表基址寄存器,是告诉MMU,页表存放在内存中哪个位置了,GDT表和页表都是操作系统引导程序必须首先建立的内存数据,其位置可以在任何地方,其开始地址会被引导程序放入到对应的寄存器GDTR和CR3中。页表是什么呢?首先弄明白为什么要引入页表,在多任务环境中,我们要运行不同的程序,为了不引起程序之间的相互影响,引入了“保护模式”的概念。
给不同的程序分配不同的内存块,这个块可大,可小。由操作系统来管理,例如程序A访问内存空间0x00001000~0x00002000,程序B访问0x00003000~0x00004000,似乎是合理的,但是我们知道,从CPU的角度来看,当一个指令执行时,可以访问任何的4G空间(32位系统),CPU指令设计者并没有规定什么指令只能访问某个空间,操作系统内核以及程序A和程序B,所有的指令代码是同一套。那么怎样才能限制程序A不至于访问到程序B的空间。首先我们看现代操作系统的“分时”概念,分时系统中,是设定一个计时器中断,如每隔10ms就产生一个中断,这个中断发生会时就跳转到中断处理程序,而这个程序就是操作系统内核进程调度的核心。因此任何任务的运行最多一次连续运行10ms,之后就产生中断接管CPU,操作系统根据自己设计的规则(这个规则相当复杂的,也是一个优秀操作系统性能的体现)决定再次启动那个进程运行。当我们切换任务(进程)的时候,就必须先保存当前正在运行的进程(如寄存器值),然后恢复某个进程的状态(如装入寄存器值)。
假设我们有某种映射机制,让0x00000000与物理地址0x00001000对应,当程序A中一个指令 MOV EAX, [0x00000000]执行时,实际访问的是0x00001000,而当程序B运行同样的指令,访问的是0x00003000,那么至少要存在两个条件:
1)可以在指令执行前将指令译码,并能及时修改其中的虚拟地址为对应的物理地址,这个在具备保护模式的CPU中都有一个叫MMU的硬件来处理,因为这个处理越快越好,因此必须采用硬件处理,而且必须计算简单。
2)有了处理部件,还需要有“米”,也就是需要有映射表,到底哪个虚拟地址对应哪个物理地址呢?这个就是页表的功能,我们在内存中如果直接画出一块内存给程序A使用,例如从0x00001000开始的100MB空间给程序A,但是程序A可能只需要10MB的空间,因此不能给每个进程首先分配固定大小的内存区域。因为随着程序的运行,内存消耗不断变化,因此就必须动态给你分配内存。
先不管页表具体怎么构建,首先应该明白:
1)每个进程都有自己的页表,用来把虚拟地址映射到物理地址,这样相同的虚拟地址就可以映射到不同的物理地址。
2)当每次进程切换时,CR3(ARM处理器为协处理器的寄存器C2保存页表基址)的值也会不同,用于指向进程自己的虚拟地址映射页表。
386保护模式中,有分段模式和分页模式:
分段模式,是用来直接把不同的进程的虚拟地址映射到一整块内存中,如进程A的内存被映射到0x00100000开始的10MB空间,这种模式,是映射简单,但是必须清楚每个程序需要用到的内存大小,不适合于做大型操作系统。因此windows和linux均采用分页模式,也不使用分段,也就是说段寄存器是没用的。
分页模式中, 存储器按照4K(或4K的倍数)来划分成颗粒,每个小块可以分配给不同的进程使用。
如一个虚拟地址0x00001000,被MMU怎么处理成物理地址呢?
在分段模式下,虚拟地址是一个段内偏移地址,而物理地址= 段寄存器值 + 偏移地址(程序指令中的地址)。每个进程的段寄存器是不同的,段寄存器的值是由操作系统管理的,保证了不同进程间的物理内存地址不会重叠。
在分页模式下,MMU会自动从CR3寄存器找到页表基址,然后根据虚拟地址(程序指令中的地址)的前20位找到页表中该页(每个虚拟地址对应的页号是确定的)所对应的物理页(物理页大小4K),而虚拟地址的低12位是该页内的偏移地址。页表分为页表目录和具体的页表,数据量是比较大的,MMU之所以能根据虚拟地址快速计算出物理内存,完全取决于虚拟地址的格式是固定的,正由于虚拟地址到物理地址的映射必须要查表,因此速度必然会收到影响。 具体的页表的处理可以查阅相关文档。
在386中,一个段描述符,包含有权限控制,如是否可执行,是在ring0可执行,还是ring3可执行。 在windows中,不使用分段方式管理内存,但是仍然使用段寄存器的权限控制,如在windows下,cs段寄存器永远指向GDT表的第8项,而GDT中的段描述符中规定了当前用户程序进程的执行权限是ring3,当切换到内核模式下时,cs则指向GDT的第一项,指明了内核当前状态可执行的权限级别为ring0,这样,当CS不改变时,用户程序是不能执行只能在ring0级别才能执行的指令(如IO操作)。
(在保护模式与实模式下,其地址运算模式其实是不变的,这是因为使用了不可访问的32位段寄存器,这个寄存器是CPU自己维护的。如CS寄存器被程序更改时,CPU会自动根据当前的模式改变32位影子段寄存器,如果是实模式下,直接复制16位的cs寄存器值到32位寄存器中,如果是保护模式,则是从GDT表中加载相应项的段基址到32位CS寄存器中。对于地址的计算,CPU永远是采用不可访问的32位隐藏寄存器进行计算,物理地址 = 段基址 + 虚拟偏移地址。 在windows中,由于所有的段基址=0,所以实际上物理地址就不用考虑段基址。可以想象,如果从实模式切换到保护模式,如果不修改cs的值,那么其段基址仍然有效,这样指令中的地址实际上就是偏移地址。操作系统引导程序也可以利用这个原理,当切换到保护模式时,仍然可以按照实模式下的物理地址访问方式访问内存,保证切换到保护模式时不至于引起程序指针PC的值变成无效地址。)
采用分页管理内存后,每个进程占用了物理内存的一些内存页,这些页可以是连续的,也可以是不连续的,这些页被页表索引,页表中的虚拟地址是连续的,称为“线性地址”,例如页表中第100页的物理基址为0x000A0000,而第101页的物理基址可能为0x01000000,但是虚拟地址100页和101页是连续的。
进程就不必关心物理地址的分配,因为每个进行的虚拟地址都是独立的,程序就可以访问4G虚拟空间,而4G内的任何一个地址会被页表转化成实际的物理地址。这样编译器和程序员就不在关心其他程序的存在,而仅仅关注自己的4G虚拟地址空间。当然因为程序要使用大量的内核API,而这些代码是被放在高2G或高1G空间的,因此用户程序不能直接访问这些内核空间。这种限制是通过操作系统软件设置决定的。
由于进程切换是强制性的,到时间(如10ms)就切换,因此内核程序为不同的进程服务(被不同的进程代码调用),因此其访问安全性的设计非常重要,内核不能直接访问用户空间的地址(如0~2G),并不是指令不容许,而是这样的访问不安全。
最后
以上就是兴奋板栗为你收集整理的保护模式的全部内容,希望文章能够帮你解决保护模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复