CPU工作模式
按照CPU功能升级迭代的顺序,CPU的一共有三种工作模式:实模式、保护模式、长模式。
在不同的工作模式下,CPU执行程序的方式不同,至于有什么不同以及为什么会造成不同,接着往下看。
实模式(Real Mode)
实模式,何为实?
一是指运行的指令是真实的,没有权限区分;
二是指运行的地址是真实的,与内存地址一一对应,并且可以操控任意位置的内存。
内存寻址
在理解实模式运行过程之前,先来看看在X86 CPU实模式下的寄存器有哪些:
上面这些寄存器有什么用?
当CPU运行某段指令时,需要先将指令从存储介质,如内存、硬盘中取出放到对应的寄存器中,CPU才可以执行这段指令。而取出指令或指令所需的数据时,也需要寄存器来确定内存地址,一般是**段基地址+偏移地址,**以8086CPU(16位寄存器,20位内存寻址)为例,访问内存地址数据如下:
这就是早期的分段式内存管理模式,代码段由CS+IP来确定,栈段由SS+SP来确定。
执行中断
所谓中断即是中断执行当前的指令,转而去执行指定的指令。中断分为硬件中断和软中断。
硬件中断是某个设备的中断控制器给CPU发送特定的电信号,CPU对其作出应答,并获取中断控制器发送的中断号。
**中断号是什么?**往下看。
软件中断就是CPU执行的指令为**INT 常数。**这个常数也是上文所说的中断号。
中断号可以看作是“excel表格”的行号,当CPU获取到中断号时,就会去加载一个特定“表格”指定行的内容,并根据其内容执行相应的指令。
上文所说的“excel表格”就是存放在内存中的中断向量表(在BIOS阶段加载到内存的),这个表的地址和长度由特定寄存器IDTR指向,表中一条记录由代码段地址和代码段内偏移地址组成,如下:
在实模式下,中断即是保存CS、IP(当前指令执行到哪了)及相关数据到栈中,然后根据中断号去中断向量表中装载新的内容到CS、IP寄存器中,从而实现中断响应。
保护模式
从前面的介绍了解到,实模式对于指令和地址不加区分,这其实会造成很大的隐患——如果某个程序恶意修改某个内存地址的内容从而造成系统崩溃怎么办?
但是凡事皆应该有限度,CPU执行代码也不例外。
保护模式就是对CPU执行指令的权限、内存地址加以限制,从而保证计算机运行稳定。
内存寻址
保护模式下对CPU指令和地址加以限制是通过一些额外的寄存器来实现的,:
**为了区分哪些指令(如 in、out、cli)和哪些资源(如寄存器、I/O 端口、内存地址)**可以被访问,CPU 实现了特权级。CPU特权级共分为4级,R0~R3,其中R0具有最高权限,可以执行所有指令,而后的R123依次递减,如下:
为了实现对内存段的保护,我们需要对各内存段进行区分,我们将其称为段描述符,并放置在内存中,一个段描述符64位,其中包含段基地址、段长度、段权限、类型、可读、可写等,其格式如下:
将多个这样的段描述符组合在一起便有了全局段描述符表GDT,该表的基地址和长度(表明该表的起始和界限)由CPU的GDTR寄存器指出,如下图所示。
实模式下进行内存访问时CS、DS、ES等段寄存器中存储的是段基地址,但是在保护模式下,进行内存访问时需要通过一个段选择子(Selector)来决定是否有权利访问及具体的访问位置。
段选择子主要有三个部分:**段描述符索引、TI、请求特权级,**如下图所示(暂不考虑前面64位的影子寄存器):
根据段描述符索引索引及GDT表中的全局描述符表基址就可以找到该内存的段描述符。
PS:这里的设计很好的节约了空间,由于每个段描述符是64位8字节,因此每个索引之间的差距就是8字节(1000 2000 3000``````),因此低三位可以用作他途(不用的话总为0,浪费了),存储TI和RPL。在实际计算的时候对索引左移三位就可以了。
TI表示该内存段描述符是在GDT中还是在LDT中,LDT可以看成是GDT的子集,是某个任务所需内存段描述符的集合表。LDT所在内存段地址也是通过GDT中的段选择子找到的,如果说GDT是一级查询表,LDT就是二级查询表。详情可见:LDT详解。
RPL表示请求访问内存的执行程序的当前权限级别(CPL),CPL是由CS和SS的RPL组成的,通常RPL=CPL。而后只有当RPL<=段描述符中的DPL时,才能够访问。
PS:特权级越低,特权越大(R0)。
影子寄存器又称为段描述符高速缓冲寄存器,是为了避免每次都访问GDT而采用硬件提速的方式来缓存对应的段描述符。
平坦模型与分页
内存分段模型相较于内存分页模型有很多缺陷(使用率、置换等),因此现代操作系统常用的是内存分页模型。不过X86 CPU需要在分段的前提下根据需要进行分页,而平坦模型则是通过让分段“虚假”,从而达到必定分页的目的:
可知,32位的寄存器最高寻址空间为4GB,因此一个段长度最大也只能为4GB,如果将所有段的基地址设为0,长度设为0xfffff,段长度的粒度设为4KB,则所有段都指向同一个地址空间:0~4GB-1(1M个4K大小的地址空间)。
执行中断
同内存访问一样,在保护模式下执行中断也需要进行权限判断,因此需要像段描述符一样扩展中断向量表中的信息,将扩展后的中断向量中的信息称为中断描述符(中断门),其格式如下:
将中断描述符组合放在内存中,即是中断门描述符表IDT,加载在相应的寄存器IDTR中,通过中断号来获取相应的中断门描述符流程如下:
产生中断时,CPU根据中断号去获取相应的中断门描述符,会进行如下判断:
- 中断号是否越界(如X86最大是256)
- 中断/陷阱门(中断门进入中断自动将IF设为0,从而防止嵌套中断//可以人为打开//,而陷阱门不会,因此陷阱门适合处理异常)
- 1表示在内存中
- 进行权限检查,这一步骤详细过程如下:
CPL<=中断门描述符的DPL(必须有足够高的权限才能进行中断),进入下一步;否则抛出一个保护异常;
CPL>=描述符中段选择子的DPL(特权级不能过高,不然还需要中断做什么,而且禁止进行高特权向低特权的转换),进入下一步;否则抛出一个保护异常;
如果CPL=段选择子指向的段描述符中的DPL,则表示同级权限,不用进行栈切换;否则,需要进行栈切换,需要从TSS中加载具体权限的各寄存器值。有关上述权限切换,可以参考:Linux权限切换。
PS:TR是任务寄存器,用于寻址任务状态段(Task State Segment,TSS),TSS存储当前执行任务的相关信息,也是64位8字节。
TR寄存器用于存放当前任务TSS段的16位段选择符、32位基地址、16位段长度和描述符属性值。它引用GDT表中的一个TSS类型的描述符。当执行任务切换时,处理器会把新任务的TSS的段选择符和段描述符自动加载进任务寄存器TR中。
在进行完上述流程之后,CPU才会加载目标中断代码进行中断服务程序的执行。
开启保护模式
X86 CPU加电后自动进入实模式,那么如何切换到保护模式呢?
- 准备全局段描述符GDT
GDT_START: //CPU硬件要求第一个段描述符为0 knull_dsc: dq 0 //dq表示4个字,8字节 kcode_dsc: dq 0x00cf9e000000ffff kdata_dsc: dq 0x00cf92000000ffff GDT_END: GDT_PTR: GDTLEN dw GDT_END-GDT_START-1 GDTBASE dd GDT_START
- 让GDTR指向GDT
lgdt [GDT_PTR]
- 设置CR0,开启保护模式
;开启 PE mov eax, cr0 bts eax, 0; 将CR0.PE =1,即打开保护模式 mov cr0, eax
- 长跳转,刷新CS段寄存器
jmp dword 0x8:_32bits_mode ;//_32bits_mode为32位代码标号即段偏移
(***)上述0x8:32位_段偏移,是一个长跳转,发现CR0的值为1,因此CPU处于保护模式,这时0x8即为上文中的段选择子:0000 0000 0000 1000,从GDTR中根据索引号8,去获取相应的段描述符,在加上32位_段偏移,最终实现了一次内存访问!
PS:虽然我们想让CS段寄存器更新,CS段寄存器无法通过mov等直接赋值的操作进行赋值,指令集没有设计这样的功能,只能通过跳转等方法来改变它。
PS:汇编中jmp是跳转指令,
jmp short 标号 段间跳转 -128-127
jmp far ptr 标号 超段转移 跳转包含目标地址
jmp reg 16位寄存器
jmp word ptr 内存单元地址 段内转移
jmp dword ptr 内存单元地址 ( 段间间接转移) 高字地址存放cs 低字节存放ip。
jmp 1000H:2000H 段间直接转移,段码和偏移量直接给出
小结
我们来看看在保护模式下进行一次段偏移的流程是如何进行的:
手写操作系统(5)——CPU工作模式与虚拟地址(下):https://developer.aliyun.com/article/1508587