改变
- 32位引入了保护模式,保护模式实现的核心是描述符
- 它可以存放在内存的任何位置,至于基地址和界限则存放在GDTR中
- 所以他提供了很多种保护
- 当我们跳转的时候,他会检查描述符是否存在,你是否有权限跳转到该段
- 当你运行指令的时候,他会判断取指令的地址是否越过段界限
- 【这里比较奇怪的是堆栈段的段界限计算方式】
- 当计算机启动的时候,默认是在16位实模式下,当在32位模式下运行的时候,段描述符是不可缺少的
- 所以我们需要在16位模式下创建引导扇区的代码段,堆栈段,数据段三个描述符,并且放置到相应位置
- 然后将描述符的选择子回写到主引导扇区的头部段里面【即创建段描述符和段重定位】
- 在切换到32位模式之后,继续运行主引导扇区的代码,此时可以将操作系统的内核代码加载到内存当中
- 遵循同样的套路,他读取内核代码头部的信息,知晓内核代码段的长度,数据段的长度以及汇编地址偏移,加上放置内核的物理基地址,根据这些信息创建段描述符,并且放置到GDT的序列中,然后将选择子回写到头部最后执行跳转
- 至于内核加载程序的流程也是一样的
加载流程
- 从硬盘加载头部信息
- 读取头部信息组装段描述
- 将描述符放置到GDT中
- 获得段选择子回写到头部
- 然后进行跳转
过程重定位
- 操作系统本身会提供很多封装好的函数供用户程序使用
- 可是调用call和Jump都需要知道跳转的段地址和函数偏移
- 但操作系统一方面不可能把这些信息透露给你,而是操作系统的段和偏移是随时改变的
- 在16位时代解决这个问题,使用软中断
- 在32位时代,使用过程重定位,用户程序将自己使用到的操作系统的函数名称全部放置在头部salt表中
- 内核在自己的内部有一张函数名称和地址的对照表salt表
- 在加载用户程序的时候,就扫描用户程序的salt表,如果和内核的函数名匹配,就将用户程序对应位置的函数名称改为内核函数的地址
任务和特权级
-
在描述符中有两位是用来表示特权级的,即00,01,10,11一共四种特权级,0123,操作系统在加载用户程序的时候会自动为其分配特权级,一般为3,数字越小则特权级越高
-
段选择子是16位的,其中13位用来表示真正的选择子,剩下1位表示LDT或者GDT【1表示为LDT】,还剩下两位是用来表示RPL请求特权级
-
在上一节中,我们访问系统的API,都是直接提供该函数的段选择子和该函数的偏移。但是在含有特权级之后,用户程序3级不能直接转移到0级程序。
-
CPL是指当前代码段的特权级
-
需要通过门或者依从。因此将系统API的段选择子和偏移制作成门描述符,只有当CPL》门描述符的特权级《段旋转子的特权级时才允许访问
-
当调用门的时候,偏移其实是没有用的【已经写在描述符中了】,通过Jump跳转步会改变当前CPL【如果是非依从】,通过call会改变当前CPL【如果是非依从】,为了防止栈空间溢出,要提供对应等级的栈。
-
比如3级需要额外提供0,1,2级的栈。
-
至于依从,则是另一种方式从低特权级转移到高特权级,在段描述符的type字段的C位,如果是0则是非依从,如果是1,则是依从的
-
调用依从的代码有个要求,必须当前特权级小于等于它,因为任何时候不允许特权级从高到低的转移。【其实也不是完全没有办法】
-
之所以称为依从,是因为调用该依从过程并不改变CPL,他是在调用者的特权级别上运行的
-
对于端口的访问也并不全是开放的,由FLAGS的IOPL进行限制,当CPL高于IOPL的时候,全都可以访问。
-
但是小于IOPL的时候,需要读取当前TSS的IO映射区许可位,测试该许可位就可以知道能否读取该端口
-
某些指令是特权指令,只有0级才可以访问,但是某些除了0级以外,其他特权级也可能,至于是否可以访问,要根据其IOPL来决定。
流程
-
因此对于内核的流程来说,
-
首先制造门并且将门的描述符放置在GDT中同时回填到salt中
-
创建任务的TCB,用来记录各项信息,并将其放置到TCB链表中
-
给该任务的LDT分配内存并记录到TCB中
-
然后读取用户程序头部的信息并根据这些信息创建对应的描述符,放置到LDT中【进行段重定位】
-
然后进行过程重定位
-
再创建该任务所需要的额外的栈并放置到LDT中
-
最后该任务的LDT本身也是一个内存段,创建他对应的描述符到GDT中,将选择子登记到TCB中
-
最后创建用户的TSS,放置到GDT中,登记到TCB中
总结
- 对于内核程序来说,更改的其实没有。引导程序没有变化
- 当从16位转换到32位的时候,自动将CPL更换为0级,然后Jump的时候,跳转到内核0级的代码段和数据段
- TCB,TSS,LDT,门
一些疑惑
- 目前的一些疑惑
- 当从16位模式转换到32位模式的时候,处理器都进行了什么处理
- TSS寄存器为什么没有保存寄存器的状态
- 非依从是什么意思
- EFLAGS 的初始值是什么
转移控制权到任务
- 因为当前的特权级都是0,cpu本身是禁止代码段从高到低转换的
- 因此这里使用retf,假装例程调用完毕的返回
- 因为正常调用操作系统的例程将参数入栈,然后将当前CS和IP入栈。【如果涉及到特权级转换】因为要切换栈,所以临时保存当前SS和ESP,然后从TSS中读取对应特权级的栈选择子和栈指针放置到SS和ESP,然后将以前的SS和ESP入栈,再压入参数,CS和IP
- 所以使用retf的时候,需要依次压入CS,IP,然后调用reftf即可。
- 【奇怪的是,如果对于需要切换栈,还需要从中读取SS和ESP进行重置,这又是依靠谁来制作的呢?】
变与不变
- 既然可以从16位到32位,那么当然也可以从32位到64位
- 32位相比于16位来说,就是把原来可以直接得到的东西,放进了一个屋子里面,然后加上密码锁,你只有符合他的权限等级,屋子才会让你进。
- 我们以前访问东西,是直接用段地址+偏移,但是现在这些数据全部被封装在描述符中,我们通过选择子来获取描述符
- 所以描述符就是一层锁,你想获取或者使用这个描述符,就必须要高于或者等于它的权限,然后这个描述符才可以到你手里,放置在对应的高速缓存中
- 因此对于TSS,LDT,门 都加了一把锁,存放到GDT中,因为这些数据都属于操作系统用来管理程序使用的数据,其他用户程序不可以无缘无故的访问它。
- 比如TSS,如果我通过研究得知了操作系统的GDT位置,然后又知晓了TSS的存放位置,那我就可以通过TSS知道对应程序的栈信息等等。
安全步骤执行流程
- 操作系统可以将自己的任务管理程序划分为一个特权级为0的任务
- 首先TR存放TSS的选择子,LDTR存放当前LDT的选择子,GDTR存放的是当前GDT的基地址和界限
- 当用户程序运行的时候,例如我们将
mov DS ax
,如果该ax的倒数第三位是1 表明是LDT的数据, 处理器就从 LDTR的高速缓存中取来 LDT的描述符,其中含有基址和界限,然后加上其选择子乘以8获得其在该任务内存空间LDT中的地址,获得其描述符 - 另外还有,DS,CS所能放置的选择子其本身也是有对应限制的。比如CS只能读不能写,DS可以读可以写,如果将可读可写的描述符放置到CS中就会引发中断。
- 这个时候再检验特权级,即CRL是否高于ax中的RPL,如果高于,就放置到DS数据段寄存器当中。【当我们将数据放置到DS,ES,CS,SS的时候总会进行检验,一旦检验通过,我们就可以用这个DS随意取数据了】
- 当任务发生切换的时候,我们需要保证TR总是指向当前TSS的选择子,然后LDTR总是指向当前LDT选择子,而且TSS中存放的有LDT的选择子。而且要把上一个任务的所有状态保存到对应TSS中,因为还没发生切换,所以根据TR可以得到TSS的位置,然后将新的一些数据写入TSS进行保存,再从任务门中获取新的TSS的选择子,并恢复到各个寄存器中,然后将TR指向新的选择子,并从TSS中获得LDT的选择子,将LDT指向它。
中断向量表的变化
- 在16位模式下的时候,从0号地址开始存放的中断向量表,在32位模式下就不能使用了,因为这些中断向量表放置的是左移四位然后加上偏移地址的传统计算方式数据
- 在32位下还要继续使用中断,就使用中断门,当发生中断的时候,就根据其中断号,在GDT中获取其对应的描述符,然后转移到对应地址执行代码。就是门的使用方式。
- 除此之外还有陷阱门和任务门,任务门是当发生中断后,CPU察觉到这是一个任务门的描述符后,从中获取其要切换的TSS对应的选择子,然后从GDT中取得TSS描述符后进行任务切换。