OS-1
CPU三种工作模式
实模式
特点
- 运行真实的指令
- 发往内存的地址是真实的
实模式寄存器
下面是x86 CPU实模式下的寄存器,均为16位
寄存器 | 描述 |
---|---|
AX, BX, CX, DX, DI, SI, BP | 通用寄存器 |
IP | 指向下一条指令的地址 |
SP | 栈指针寄存器,始终指向栈顶 |
CS, DS, ES, SS | 段寄存器,存放一个内存段的基地址 |
FLAGS | 存放CPU运算产生的标志位 |
实模式访问内存
代码段由CS和IP确定,栈段由SS和SP确定
示例汇编代码:
1 | data SEGMENT ;定义一个数据段存放Hello World! |
实模式中断
- 硬件中断:中断控制器直接给CPU发射电子信号,CPU作出应答,随后中断控制器发送中断号
- 软件中断:CPU执行了INT指令
为了实现中断机制,我们需要构建中断向量表。(IDTR是CPU的特定寄存器)
保护模式
保护模式寄存器
寄存器 | 描述 |
---|---|
EAX, EBX, ECX, EDX, EDI, ESI, EBP | 32位通用寄存器(名字在实模式基础上加了E) |
EIP | 32位程序指针 |
ESP | 32位栈指针 |
CS, DS, ES, SS, FS, GS | 16位段寄存器,里面存放内存段的描述符索引 |
EFLAGS | 32位CPU标志寄存器 |
CR0, CR1, CR2, CR3 | 32位CPU控制寄存器,控制CPU功能 |
保护模式特权级
特权级分为4级,从R0-R3,R0 可以执行所有指令,R1、R2、R3 依次递减,它们只能执行上一级指令数量的子集。
保护模式段描述符
目前为止,内存还是分段模型,要对内存进行保护,就可以转换成对段的保护。
为了实现对段的保护,我们需要段描述符记录段的信息,其中包括该段的权限级别,从而确定能否被访问
又由于CPU拓展后,地址均为32位,段长度最大为(1 << 20)
因此我们要记录一个段的信息,至少需要32+20(记录该段长度)=52位,再加上些标识位,凑到64位:
多个段描述符形成全局的段描述符表,该表的基地址和长度记录在CPU的GDTR寄存器中
段寄存器中不再存放段基地址,而是具体段描述符的索引,访问一个内存地址时,段寄存器中的索引首先会结合 GDTR 寄存器找到内存中的段描述符,再根据其中的段信息判断能不能访问成功。
保护模式段选择子
CS-GS这些段寄存器有16位,实际上它们还均附有影子寄存器(64位,用于段描述符的高速缓存),但这些影子寄存器位于硬件,对程序员不可见也不可操作。
低三位之所以能放 TI 和 RPL,是因为段描述符 8 字节对齐,每个索引低 3 位都为 0
TI: 我们不用关注 LDT,只需要使用 GDT 全局描述符表,所以 TI 永远设为 0。
RPL: 通常情况下,CS 和 SS 中 RPL 就组成了 CPL(当前权限级别),所以常常是 RPL=CPL,进而 CPL 就表示发起访问者要以什么权限去访问目标段,当 CPL 大于目标段 DPL 时,则 CPU 禁止访问,只有 CPL 小于等于目标段 DPL 时才能访问。
保护模式平坦模型
分段模型有很多缺陷,这在后面讲内存管理时有详细介绍,其实现代操作系统都会使用分页模型(这点在后面讲 MMU 那节课再探讨)。
但是 x86 CPU 并不能直接使用分页模型,而是要在分段模型的前提下,根据需要决定是否要开启分页。因为这是硬件的规定,程序员是无法改变的。但是我们可以简化设计,来使分段成为一种“虚设”,这就是保护模式的平坦模型。
由于CPU为32位,我们最大有1<<32的地址,而段长度最大为20位,粒度最大为2^12,因此一个段最长也为1<<32。
我们可以设置所有的段均从0开始,长度均为1<<20,粒度均为1<<12,那么所有的段均为1<<32地址空间
下面是当前该OS的段描述符表
1 | GDT_START: |
G=1,则段长度等于 0xfffff 个 4KB(G位为1,粒度为4KB,即最大的1<<12)
DPL=0,这说明需要最高权限即 CPL=0 才能访问。
保护模式中断
实模式下中断无需权限检查,而保护模式需要权限检查以及权限切换,因此同样的,我们需要中断描述符描述每个中断向量的信息。
我们将一个中断的中断描述符称为中断门描述符:
同理,我们通过IDTR+中断号获取对应的中断门描述符:
产生中断后,CPU依次进行下列操作:
- 检查中断号是否大于最后一个中断门描述符(x86 CPU最多支持0-255)
- 检查描述符类型
- 检查描述符段选择子指向的段选择符
- 权限检查:如果 CPL 小于等于中断门的 DPL,并且 CPL 大于等于中断门中的段选择子所指向的段描述符的 DPL,就指向段描述符的 DPL。
- 加载描述符中的段选择子到CS, 代码段偏移到EIP
CPL 小于等于中断门的 DPL,说明有权限能执行中断。 CPL 大于等于所指向的段描述符的 DPL,如果不提升 CPL,就会导致没有权限调到该内存,所以指向段描述符的 DPL。
切换到保护模式
x86 CPU在加电和reset后,会进入实模式,因此我们需要写代码使CPU切换到保护模式。
第一步:准备全局段描述符表(代码同上):
1 | GDT_START: |
第二步:将GDTR寄存器指向全局段描述符表,
1 | lgdt [GDT_PTR] |
第三步:设置CR0寄存器,开启保护模式
1 | ;开启 PE |
bts指令的第一个操作数称为位基址,亦称位串;第二个操作数为位偏移值(bit offset)。
bts就是根据位偏移值从位串中取出一位放入CF中,然后将位串中的该位置成1。
第四步:进行长跳转,加载CS段寄存器,即段选择子
1 | jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移 |
我们无法直接或间接 mov 一个数据到 CS 寄存器中,因为刚刚开启保护模式时,CS 的影子寄存器还是实模式下的值,所以需要告诉 CPU 加载新的段信息。接下来,CPU 发现了 CRO 寄存器第 0 位的值是 1,就会按 GDTR 的指示找到全局描述符表,然后根据索引值 8,把新的段描述符信息加载到 CS 影子寄存器,当然这里的前提是进行一系列合法的检查。
到此为止,CPU 真正进入了保护模式,CPU 也有了 32 位的处理能力。
长模式
特点
长模式又名 AMD64,因为这个标准是 AMD 公司最早定义的,它使 CPU 在现有的基础上有了 64 位的处理能力,既能完成 64 位的数据运算,也能寻址 64 位的地址空间。这在大型计算机上犹为重要,因为它们的物理内存通常有几百 GB。
长模式寄存器
长模式相比于保护模式,增加了一些通用寄存器,并扩展通用寄存器的位宽,所有的通用寄存器都是 64 位,还可以单独使用低 32 位。