保护模式
在保护模式下,依旧采用段:偏移的形式来进行寻址。只不过这时候的段值并非真实的地址了,而是一个索引,这个索引指向一个表,表里定义了起始地址、属性等信息。
换句话说,对内存进行操作的时候,需要找到操作对象所对应的一个索引,通过索引找到操作的起始地址,再加上偏移量找到具体地址。
这种寻址方式安全的地方就在于,对内存的操作必须通过一段规范的索引表单,从而避免了对内存的随意访问修改。
再者,由于索引的存在,在每次使用索引的时候会有相应的使用记录,这样的记录确保了访问的安全性,这也是保护模式中“保护”二字的重要体现。
注意一个段描述符只能用来定义一个内存段。
加载 gdt 这种操作模式好多地方都是通用的,cpu 会有很多与操作系统相互打配合的地方,这个就是其中之一。
配合关系就是 cpu 定义好一个数据结构,再给你一个寄存器。
操作系统一般负责做三件事情
- 负责在内存中某位置按照这个数据结构写一堆数据;
- 把在内存的哪个位置这个信息(起始地址),存在 cpu 预留的寄存器里。一般会有一条专门的指令,比如 lgdt
- 操作系统将 cpu 某寄存器中的某标志位置1
然后就开启了这个功能,段描述符表、页表、TSS亦是如此。
其实实模式下就已经是段选择子的工作模式了,只不过对应寄存器的值设置得跟凑巧,以至于表现起来跟使用了段寄存器模式一样。
X86寄存器表示图
在x86下.我们可以看如下寄存器表示图.
寄存器名称 | 段选择子(Select) | 段属性(Attributes) | 段基址(Base) | 段长(Limit) |
---|---|---|---|---|
ES(附加扩展段) | 0x0023 | 可读,可写 | 0x0000000 | 0xFFFFFFFF |
CS(代码段) | 0x001B | 可读,可执行 | 0x00000000 | 0xFFFFFFFF |
SS(堆栈段) | 0x0023 | 可读,可写 | 0x00000000 | 0xFFFFFFFF |
DS(数据段) | 0x0023 | 可读,可写 | 0x00000000 | 0xFFFFFFFF |
FS(分段机制) | 0x003B | 可读,可写 | 0x7FFDF000 | 0xFFF |
GS | 未使用 | 未使用 | 未使用 | 未使用 |
可以发现CS SS DS ES的段基址均为0x00000000,长度均为0xFFFFFFFF,8个F表示4G。
可见偏移部分也可以实现4G的地址索引,地址索引只用偏移部分就可以了。
96位的段寄存器中有16位可见部分是段选择子,处理器就是通过它去段描述符中加载不可见的80位加入到段寄存器中。所以段选择子并不直接指向段,而是指向定义段的段描述符。
处理器根据段选择子中的Index来获得需要的段描述符的地址,以此来加载隐藏的80位。
处理器不使用GDT的第一个条目。指向GDT的此条目的段选择器(即索引为0且TI标志设置为0的段选择器)被用作“空段选择器”。当段寄存器(除了CS或SS寄存器)加载了空选择器时,处理器不会产生异常。但是,当使用持有空选择器的段寄存器来访问内存时,它确实会产生一个异常。空选择器可用于初始化未使用的段寄存器。加载带有空段选择器的CS或SS寄存器会导致生成通用保护异常。
[SECTION .gdt]
; GDT
; 段基址, 段界限, 属性
LABEL_GDT: Descriptor 0, 0, 0 ; 空描述符
LABEL_DESC_NORMAL: Descriptor 0, 0ffffh, DA_DRW ; Normal 描述符
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G
LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW|DA_LIMIT_4K ; 0~4G
LABEL_DESC_CODE32: Descriptor 0, SegCode32Len-1, DA_CR|DA_32 ; 非一致代码段, 32
LABEL_DESC_CODE16: Descriptor 0, 0ffffh, DA_C ; 非一致代码段, 16
LABEL_DESC_DATA: Descriptor 0, DataLen-1, DA_DRW ; Data
LABEL_DESC_STACK: Descriptor 0, TopOfStack, DA_DRWA|DA_32 ; Stack, 32 位
LABEL_DESC_VIDEO: Descriptor 0B8000h, 0ffffh, DA_DRW ; 显存首地址
; GDT 结束
段的基址均为0,结合分页机制,就可以实现用纯偏移进行虚拟内存的索引。
处理器要根据段选择子找到需要的段描述符,以此填充段选择子的隐藏的80位的内容,此时的段选择子就代表了一个段。但是,在一个多任务中通常会同时存在着很多个任务,每个任务会涉及多个段,每个段都需要一个段描述符,因此系统中会有很多段描述符,为了方便管理,系统用线性表来存放段描述符。根据用途不同,IA-32处理器有3中描述符表:全局描述符表(GDT),局部描述符表(LDT)和中断描述符表(IDT)。
GDT是全局的,一个系统中通常只有一个GDT,供系统中的所有程序和任务使用。
LDT与任务有关,每个任务可以有一个LDT,也可也让多个任务共享一个LDT。
IDT的数量是和处理器的数量相关的,系统通常会为每个CPU建立一个IDT。
GDTR和IDTR寄存器分别用来标识GDT和IDT的基地址和边界。这两个寄存器的格式是相同的,在32位模式下,长度是48位,高32位是基地址,低16位是边界。
LGDT和SGDT指令分别用来读取和设置GDTR寄存器。
LIDT和SIDT指令分别用来读取和设置IDTR寄存器。
操作系统在启动初期会建立GDT和IDT并初始化GDTR和IDTR寄存器。
任务间保护
任务间保护主要是靠虚拟内存映射机制来实现的,即在保护模式下,每个人物都被置于一个虚拟内存空间中,操作系统决定何时以及如何把这些虚拟内存映射到物理内存。
举例来说,在X86下,每个任务都被赋予4GB的虚拟内存空间,可以用地址0~0xFFFFFFFF来访问这个空间中的任意地址。
尽管不同任务可以访问相同的地址(比如0x00401010),但因为这个地址仅仅是本任务空间中的虚拟地址,不同任务处于不同的虚拟空间中,不同任务的虚拟地址可以被映射到不同的物理地址,这样就可以很容易防止一个任务内的代码直接访问另一个任务的数据。
任务内保护
任务内保护主要用于操作系统。
操作系统的代码和数据通常被映射到系统中每个任务的内存空间中,并且对于所有任务其地址是一样的。例如,在Windows系统中,操作系统的代码和数据通常被映射到每个进程的高2GB空间中。这意味着操作系统的空间对于应用程序是“可触及的”,应用程序中的指针可以指向操作系统所使用的内存。
任务内保护的核心思想是权限控制,即为代码和数据根据其重要性指定特权级别,高特权级的代码可以执行和访问低特权级的代码和数据,而低特权级的代码不可以直接访问和执行高特权级的代码和数据。
高特权级通常被赋予重要的数据和可信任的代码,比如操作系统的数据和代码。
低特权级通常被赋予不重要的数据和不信任的代码,比如应用程序。
这样,操作系统可以直接访问应用程序的代码和数据,而应用程序虽然可以指向操作系统的空间,但是不能直接访问,一旦访问就会被系统发现并禁止。
事实上,应用程序只能通过操作系统公开的接口(API)来使用操作系统的服务,即所谓的系统调用。系统调用相当于在系统代码和用户代码直接开了一扇有人看守的小门。
GDT是一块内存,是CPU设计中要求操作系统提供的一款内存。这块内存是操作系统在启动时填充的。
GDT和LDT在4G内存里面应该放在哪里?为何不会被其他程序覆盖?
一致代码段与非一致代码段
一致代码段
一致代码段:简单理解就是操作系统拿出来被共享的代码段,可以被低特权级的用户程序直接调用访问的代码段,这些代码段,通常是不去访问受保护的资源和某些类型异常处理。
一致代码段访问限制:
特权级高的程序不允许访问特权级第的数据:即内核态不允许调用用户态的数据。
特权级低的程序可以访问到特权级高的程序,但是特权级不会改变,即不会从用户态切换到内核态。
非一致代码段
非一致代码段:为了避免低特权级的访问而被操作系统保护起来的系统代码
非一致代码段访问限制:
只允许同特权级访问。
绝对禁止不同特权级直接访问:内核态不去用户态,用户态也不使用内核态。
现代操作系统是围绕保护模式的“分段+分页+特权”三个特征开展的。