OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)

        这个专栏最近由于一些事情更新很慢,今天先更一篇笔记。

实模式

        这里的“实”,英文里对应real。real有真实的意思,实际上X86的实模式也代表了两方面的“真实”:运行真实的指令,执行指令真实的功能;访问内存的地址是真实的,对应的就是物理地址,不是“虚”的(开启MMU后的虚拟地址)。

实模式下访问寄存器

        实模式下,X86 CPU的寄存器如下图,每个寄存器都是16位宽。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第1张图片

 实模式下访问内存

         CPU要访问内存(从内存上取指令或数据),需要向总线上发送要访问的内存地址,实模式下,这个地址是怎么计算出来的呢?来看下面这张图:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第2张图片

        对于指令地址,CPU会通过CS和IP寄存器的值组合而来,值为CS所存储的地址左移4位加IP的值:(CS << 4) + IP。

        对于数据地址,CPU会通过DS,ES,SS加上AX,BX,CX,DX,EX,DI,SI,BP,SP寄存器组合而来,DS一般用来存放数据段内容比如C语言全局变量;SS则是栈的基地址,SS搭配SP来使用,一般用来访问C语言临时变量和函数栈信息。地址计算规则和指令地址计算规则类似。

实模式中断

        中断分为两种:硬件中断和软件中断。

        硬件中断: 当一个外设比如网卡收到了数据包后,通过中断线通知中断控制器,中断控制器向CPU发送一个信号,CPU收到信号后,会中止当前正在执行的任务切换到中断处理模式。待中断处理完成后切换回之前被打断的任务。就好像你正在写作业的时候,你妈给你拿了一个水果让你吃完再写,你会暂停写作业,吃完水果后再接着上次作业的地方继续开始写作业。

        软件中断:程序要求CPU执行INT指令,CPU会取出指令后的常数(软件中断号)并且换到对应的软件中断处理函数。

        对于上面描述的场景,CPU是怎么知道中断来了该去哪里执行呢?比如来了网卡中断该怎么处理,来了INT指令中断该区哪里处理。这时我们就需要在内存中放一个中断向量表。这个表的地址和长度由CPU的特定寄存器IDTR给出。实模式下,一个表项由“代码段地址” + “段内偏移”组成:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第3张图片

         可以看出,CPU会根据IDTR相关信息,结合中断表基地址,中断表表项长度,以及中断号,计算出对应中断表表项的地址,然后将表项中的代码段基地址值装入CS,代码段内偏移转入IP,然后从指定的CS + IP处取值执行中断处理程序。

保护模式

        实模式下,寄存器都是16位的,以CS为例,实际最大能寻址的范围是CS左移4位,总共20位地址,因此可以访问1MB空间的内存。

        当内存超过1MB时,实模式的第一个问题就来了,保护模式需要解决这个问题。

        实模式的第二个问题是CPU不加区分执行指令,对所访问的内存地址也没有任何限制。保护模式对这个问题也进行了解决。

保护模式寄存器

        保护模式下,所有通用寄存器位32位,也可以单独使用低16位,低16位又可以拆分成两个8位寄存器。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第4张图片

 保护模式特权等级

         保护模式下对指令(如in,out,cli)和资源(如寄存器,I/O端口,内存地址)等进行了权限区分。

        权限分为4级,R0-R3,每种权限执行的指令数量不同,R0权限最高,可以执行所有指令,R1,R2,R3权限逐级递减。内存访问的权限通过后面要说的段描述符和特权级别配合实现,对于R0来说权限最大,可以访问所有资源。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第5张图片

保护模式段描述符

        从前面保护模式的寄存器描述中可以看到,段寄存器仍然是16位的。保护模式下,可访问地址空间已经扩展到了32位,16位段寄存器显然没有办法放下32位地址。因此我们来看一下保护模式下,X86是如何处理这个问题的。

        首先,我们要了解一个叫做段描述符的东西,这个东西是X86规定的一个64位、8字节大小的数据结构,格式如下:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第6张图片

         可以看出,段描述符里包含了32位段基地址、20位段界限(需要注意这个值所代表的具体长度受到‘G‘这个bit的控制)的字段,另外还有一些bit用来做段的访问权限(读、写、可执行)和类型(系统段,代码段,数据段)等。这里简单列出一张表做关键字段说明:

关键字段 说明
G 粒度位,用于解释段界限的含义。当 G 位是“ 0”时,段界限以字节为单位。此时,段的扩展范围是从 1 字节到 1 兆字节( 1B~1MB),因为描述符中的界限值是 20 位的。相反,如果该位是“ 1”,那么,段界限是以 4KB 为单位的。这样,段的扩展范围是从 4KB到 4GB
S 指定描述符的类型( Descriptor Type)。当该位是“ 0”时,表示是一个系统段;为“ 1”时,表示是一个代码段或者数据段(堆栈段也是特殊的数据段)。
DPL 描述符的特权级( Descriptor Privilege Level, DPL)。这两位用于指定段的特权级。共有 4 种处理器支持的特权级别,分别是 0、 1、 2、 3,其中 0 是最高特权级别, 3 是最低特权级别。
P 段存在位( Segment Present)。 P 位用于指示描述符所对应的段是否存在。一般来说,描述符所指的段都在内存里,但在虚拟内存实现中,可能出现内存紧张时将相关空间转移到了外存中,此时P=0。处理器在尝试访问P=0的段时会产生异常。
D/B “默认的操作数大小”( Default Operation Size)或者“默认的堆栈指针大小”,又或者“上部边界”标志。主要是为了在32位处理器上兼容运行16位的程序。
L  64 位代码段标志,保留此位给 64 位处理器使用。
TYPE

描述符的子类型。

对数据段来说,这4个bit为:X,E,W,A

对代码段来说,这4个bit为:X,C,R,A

X代表是否可以执行( eXecutable)。

E代表扩展方向,E=0 向上(高地址)扩展,比如数据段;E=1向下(低地址)扩展,比如堆栈段。

W代表段是否可写, W=0表示不可写,W=1表示可写。

C代表特权等级是否是一致的(conforming)。C=0表示非一致代码段,这种代码段可以被相同特权级别的代码段所调用,或通过们调用;C=1表示一致代码段,允许低特权级别的程序执行这个代码段。

R代表是否可读,R=0表示不可读,R=1表示可读。

A表示已访问位,如果这个段最近被访问过,则处理器会将其改为1。

        将多个段描述符放到一起就形成了一张表,叫做全局段描述符表。这张表的基地址和长度由GDTR寄存器来确定。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第7张图片

         因此,保护模式下,段寄存器里存放的不再是段基地址,而是一个段选择子(可以理解为段描述符索引,但实际上还包含权限,描述符表索引等),用来找到内存中的段描述符

        访问一个内存地址时,CPU会先将GDTR寄存器的值和段寄存器的值相结合,找到内存中的段描述符,再通过段描述符里的段基地址和权限等信息来找到实际内存地址和访问权限。

        X86处理器有三种描述符表:全局描述符表(GDT),局部描述符表(LDT)和中断描述符表(IDT)。

        GDT是全局的,一个系统中通常只有一个GDT,供系统中的所有程序和任务使用。LDT与任务有关,每个任务可以有一个LDT,也可也让多个任务共享一个LDT。IDT的数量是和处理器的数量相关的,系统通常会为每个CPU建立一个IDT。

        GDTR在32位模式下,长度是48位,高32位是段全局描述符表基地址,低16位是表大小边界值。

        举个例子,假设GDTR = 0x800000003FF, 则可以得出,段全局描述符表基地址为0x80000000,表的长度为0x3FF + 1 = 0x400(1024个字节+1是因为0x0 - 0x3FF总共有0x400个字节),因此可以知道,一个GDTR所指向的空间可以存放1024 / 8 = 128个描述符。

保护模式段选择子

        CS,DS,ES,SS,FS,GS这些寄存器,里面的格式如下:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第8张图片

        其中影子寄存器是硬件管理,软件不可用,其作用是作为段描述符的高速缓存,用于提升段描述符的访问速度,总共64位、8字节。

        X86保护模式下有CPL(当前权限级别),段选择子的RPL(请求权限级别),段描述符字段里还有一个DPL(描述符权限级别)。关于X86保护模式下段机制,可以参考下面两篇文章(写得很详细):

[原创]保护模式学习笔记之段机制-软件逆向-看雪论坛-安全社区|安全招聘|bbs.pediy.com[原创]保护模式学习笔记之段机制https://bbs.pediy.com/thread-270476.htm
操作系统篇-调用门与特权级(CPL、DPL和RPL) - 卫卐 - 博客园内存的保护,“保护”这两个字的含义何在呢?不同权级之间是如何相互访问,如何通讯,如何保护,这些东西都跟调用门和不同代码段的特权级紧密相关。本文主要探讨的就是,保护模式下的调用门与特权级。https://www.cnblogs.com/chenwb89/p/operating_system_004.html

保护模式下使用平坦内存模型(Flat Memory)

        分段模式的缺点比较多,现代OS都会使用分页模型。但由于X86 CPU无法绕开分段机制,因此需要在遵循分段模型的前提下,巧妙地让分段模型成为一种“虚设”,变成平坦内存模型。具体做法如下:

        由于32位 CPU能访问的地址空间是4GB,我们将所有段的基地址设置为0,段的界限设置为0xFFFFF,段访问的粒度G设置为1(4KB)。这样设置后,所有的段都指向了同一个(0xFFFFF + 1) * 4KB = 4GB大小的空间(地址范围0x00000000 - 0xFFFFFFFF)。

 想要更深入了解分段机制,请参考这位大神的文章,写的很清晰:

操作系统篇-分段机制与GDT|LDT - 卫卐 - 博客园保护模式寻址入门,了解保护模式分段机制,全局描述符GDT与局部描述符LDT的区别,分析描述符的结构、类型。https://www.cnblogs.com/chenwb89/p/operating_system_003.html

保护模式中断处理

        实模式下中断处理不需要检查权限,因此CPU可以直接将中断向量表中的值放到CS:IP寄存器。但保护模式下,中断要进行权限检查,加上特权等级也可能涉及切换,因此需要扩展中断向量表的信息。保护模式下每个中断用一个中断门描述符来表示,简称中断门,其格式如下:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第9张图片

         和实模式一样,内存里也需要有一张中断向量表,由IDTR寄存器记录表的基地址,区别只是表项变成了中断门描述符。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第10张图片

         中断产生后,CPU会:

  1.         先判断中断号是否在合理范围(不能超过中断门描述符表中表项个数),X86最大支持256个中断源。
  2.         检查描述符类型(中断门还是陷阱门)、描述符在不在内存中。
  3.         检查中断门描述符中的段选择子所指向的段描述符。最后做权限检查,若CPL小于等于中断门的DPL,并且CPL大于等于中断门中的段选择子指向的段描述符的DPL,则通过检查。
  4.         如果CPL等于中断门段选择子指向的段描述符的DPL,视为同级别权限,不切换栈,否则会切换栈。如果涉及栈切换,还会从TSS中加载具体权限对应的SS,ESP等。
  5.         将中断门描述符中目标代码的段选择子放到CS寄存器,目标代码段偏移加载到EIP寄存器。

切换到保护模式

        X86 CPU在每一次上电或复位后,默认进入实模式,要进入保护模式,需要通过软件来实现。步骤如下:

        1. 准备好全局段描述符表

GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
GDT_END:
GDT_PTR:
GDTLEN  dw GDT_END-GDT_START-1
GDTBASE  dd GDT_START

        2. 设置GDTR寄存器,将全局段描述符表地址写入其中


lgdt [GDT_PTR]

        3. 配置CR0寄存器,打开保护模式(设置CR0.PE bit为1)


;开启 PE
mov eax, cr0
bts eax, 0                      ; CR0.PE =1
mov cr0, eax         

        4. 通过jmp指令进行长跳转,加载CS段寄存器(X86下无法直接修改CS寄存器的值)


jmp dword 0x8 :_32bits_mode ;_32bits_mode为32位代码标号即段偏移

长模式(AMD64)

        长模式(AMD64)最早是AMD定义的,它将CPU处理能力带到了64位时代。

长模式寄存器

        长模式下,增加了一些通用寄存器,扩展了通用寄存器位宽到64位,可以单独使用低32位,低32位又可以拆分为低16位寄存器使用,低16位还可以拆分为两个8位寄存器使用。

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第11张图片

长模式段描述符

        长模式和保护模式下大部分特性是一致的,本节关注两种模式的差异。

        首先来看长模式下的段描述符,如下图

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第12张图片

        AMD64长模式下,运行在64-bit模式下的软件会自动使用平坦内存模型;64-bit模型下,段基址被看作为0,段大小被忽略,这使得有使用效地址可以访问处理器支持的所有虚拟地址空间。

         长模式下,CPU不再检查段基址和段界限,只对DPL进行检查。

长模式中断处理

        长模式下中断门描述符格式如下:

OS实战笔记(3) -- X86 CPU三种工作模式(实模式,保护模式,长模式)_第13张图片

         长模式为了支持64位寻址,中断门描述符在原有基础上增加了8字节,用来存储目标段偏移的高32位。目标代码段选择自对应代码段描述符必须为64位的代码段。

        长模式也存在中断门描述符表,和保护模式差异是每个表项长度是16字节,最多支持256个中断源,权限检查和保护模式一致。

切换到长模式

        可以从实模式直接切换到长模式,也可以从保护模式切换到长模式。切换步骤如下:

        1. 准备长模式全局段描述符表:


ex64_GDT:
null_dsc:  dq 0
;第一个段描述符CPU硬件规定必须为0
c64_dsc:dq 0x0020980000000000  ;64位代码段
d64_dsc:dq 0x0000920000000000  ;64位数据段
eGdtLen   equ $ - null_dsc  ;GDT长度
eGdtPtr:dw eGdtLen - 1  ;GDT界限
     dq ex64_GDT

        2. 准备长模式的MMU页表,为开启分页模式做准备,长模式下必须要开启分页。因为长模式下内存地址空间的保护工作交给了MMU完成。页表的地址由CR3寄存器保存。


mov eax, cr4
bts eax, 5   ;CR4.PAE = 1
mov cr4, eax ;开启 PAE
mov eax, PAGE_TLB_BADR ;页表物理地址
mov cr3, eax

        3. 设置GDTR寄存器,指向全局段描述符表。


lgdt [eGdtPtr]

        4. 开启长模式,同事要开启保护模式和分页模式。长模式下使用rdmsr,wrmsr指令进行MSR寄存器读写。其中IA32_EFER寄存器的第8位LME表示是否要开启长模式。


;开启 64位长模式
mov ecx, IA32_EFER
rdmsr
bts eax, 8  ;IA32_EFER.LME =1
wrmsr
;开启 保护模式和分页模式
mov eax, cr0
bts eax, 0    ;CR0.PE =1
bts eax, 31
mov cr0, eax 

        5. 通过jmp指令进行跳转,加载CS段寄存器


jmp 08:entry64 ;entry64为程序标号即64位偏移地址

       更多AMD64参考资料,可以看这里

AMD64卷2-系统编程-xlpang-ChinaUnix博客卷2 系统编程前言        本卷用于系统软件设计人员编写操作系统、加载器、链接器、设备驱动和系统工具等。阅读前请先了解卷1(应用编程)。  &nbhttp://blog.chinaunix.net/uid-7295895-id-3011309.html

你可能感兴趣的:(OS实战笔记,其他)