上期文章 是关于Linux启动的,在启动的时候会切换CPU模式,从实模式到保护模式,有小伙伴咨询到我说想详细了解下这两种模式。
那么,本期来跟你们唠嗑一下CPU工作模式,一起彻底理解CPU工作模式。
前置知识
- 寄存器就是用来存取数据的,在CPU运算的时候主要靠寄存器来进行数据存取。
- 寄存器的位数就代表这CPU能运行的位数,比如32位机器,就是说寄存器的位数有32位,一般32位的话,就是EAX 这种,包含高16位和低16位,,低16位有包含高八位AH和低八位AL。
进入正题。
实模式部分
CPU的工作模式,简单概括就是CPU在工作的时候是如何寻址的,那么在实模式下,我们很容易猜测到是寻找实实在在的内存地址,没错,实模式工作时,是寻找真实的物理地址,而且没有加任何限制,下面我们先来看看历史起源。
历史起源
CPU的实模式工作模式就起源于8086处理器,这也是第一款支持内存分段的处理器,此时的CPU就工作在实模式下,8086这款处理的寄存器位数只有16位,寄存器只有16位的话,那么它最大的表示范围为:2^16 = 64 K,也就是说此时寄存器能够存储的最大的值就是64K - 1,大于这个值就放不下了,但8086处理器的地址总线有20根,也就是20位,20位的寻址空间可达到 2^32B = 1024K = 1M,所以地址总线能传这么大的数,但是寄存器却存不下,
为了解决这一不匹配带来的问题,必须要采用一种方式给它俩匹配上,而这种方式,就是下面寻址原理提到的方式。
寻址原理
在上一期关于Linux启动的文章中,我们知道内存地址的表示是由代码段寄存器CS和指令寄存器IP完成的,前者代表段基地址,后者代表段内偏移地址。他们都是16位的,为了和20位地址总线匹配,会进行如下:先将cs寄存器的值左移四位,然后再与ip寄存器的值相加,这么以转换,就得到了一个20位的数据大小了,如下所示:
真实物理地址 = 段基址 <<<< 4 + 段内偏移地址
我们现在假设 段基址的值也就是cs寄存器的值 = 0xFFFF , 段内偏移地址也就是 ip寄存器的值为 0x0000, 那么最终的真实物理地址为:
0xffff <<<< 4 + 0x0000 = 0xffff0 + 0x0000 = 0xffff0
最终结果就是 0xffff0 了
通过原理的介绍以及例子的讲解,我们弄清楚了实模式下的CPU寻址原理,也就是(cs:ip)
其实还有一点没有跟你提到,我们前面所描述的寻址原理,是通过段基地址和段内偏移地址找到的内存地址,这就是在操作系统进行内存管理的第一大跨越,著名的内存分段模型。
8086CPU的问题
通过上面的描述,细心的你,肯定发了问题。
你会发现用到的既然都是真实的物理地址,是不是随意一个能表示的地址都可以访问?
这就出现了两个程序之间的地址空间没有界限,就可以相互随意干扰,所以就得给内存地址加上保护机制。
你还会发现代码段寄存器和指令寄存器所能表示的最大寻址地址为0xFFFF:0xFFFF = 0x10FFEF
这个地址已经比1M要大了,那超过1M怎么办呢,地址总线就那么20根,这个时候8086CPU采取的办法就是高位被抛弃,也就是我们现在说的数据溢出(Overflow)了。
为了解决这些问题保护模式机制就诞生了。
保护模式部分
保护模式出现的原因就是为了解决实模式下出现的问题,保护,顾名思义:就是保护进程地址空间,程序A的地址空间,不能随意被程序B访问。
保护模式下的概念稍多,我们先来看看几个概念。
几个概念
执行权限(CPU特权级)
既然是需要保护内存地址空间,那么肯定有些指令得有执行的权限控制,CPU就实现了特权级,如图
从里往外权限依次降低,可以看到内核态具有最高权限即Ring 0 拥有最高权限,可以访问低特权级的资源,而反过来却不行。
段描述符
这里32位CPU中的寄存器相比于16位发生了改变,原来16位寄存器可以存放下段基地址和段内偏移地址,而变到32位,还需要存储其他信息,肯定放不下了。放不下,就需要想办法去解决,这个时候就去找内存借用空间,然后把描述一个段的信息封装成特定格式的描述符,存放在内存中,这个描述符就被称为段描述符,它包含两个双字,也就是64位8字节数据,里面包含了段基地址、段长度、权限、类型等等信息,具体就来看图吧!
那么段描述符肯定不止一个,多个段描述符在内存中,就形成了一个表,这个表叫做全局段描述符表(GDT,Global Descriptor Table),这个表的基地址是由GDTR寄存器指向。这个寄存器在保护模式初始化的时候会加载一个值,这个值就指向了GDT,这样要寻址的时候就会先来到这里,而在寄存器中,不在是存放段基址,而是一个指向段描述符表的索引。所以这里就从根本上改变了寻址方式了。
段选择子
上面提到的指向段描述符表的索引就属于段选择子的一部分,具体结构如下图:
- 索引:表示在段描述附表中的索引位置
- TL:Table Indicator 表示是去GDT表查找还是LDT(局部描述符表)
- RPL:请求特权级,以什么样的特权级访问段信息。
段选择子又属于属于段寄存器的一部分,它是一个16位的整数(这是16位可见部分)记录了GDT表的段描述符的具体索引位置,以及请求特权级和描述符表类型以及段描述符表索引。
寻址原理
保护模式的寻址原理,就没有实模式那么简单了,首先在保护模式下,cs寄存器的低16位存储的是上面提到的段选择子的信息,在寻址的时候,会根据GDTR寄存器找到基址,然后根据cs寄存器里面的段选择子信息,进行偏移读取,读取完了之后,其实这个时候已经找到了程序执行的地址,接下来就会根据段描述符进行对段寄存器的填充,主要是填充不可见的那一部分,段寄存器的大致分布如下。
struct Segment
{
WORD selector; //16位段选择子
WORD attribute; //16位表示的段属性
DWORD base; //32位表示的基址始
DWORD limit; //32位表示
}
我们前面提到了,段选择子是段寄存器的一部分,如上面代码 selector 就是表示选择子,而下面的三个属性是不可见的。attribute 属性标注了段的属性,是可读还是可写还是可执行的,base属性表示了段从哪里开始,limit和base就可以确定一个段的大小。
除了段选择子可见外,后面的三个部分对程序员是不可见的,那么我们如何得知这三部分的存在呢?这里需要做一些相关的实验室,后续见~
填充完了之后,段寄存器中有内存地址信息了。
弄清楚了保护模式的寻址原理了,是不是还想知道,实模式下,究竟如何切到到保护模式呢?
实模式切换到保护模式
x86的CPU在每次加电或者reset的时候,都是先进入到的是实模式,如果要切换到保护模式得需要程序引导,这个引导,我们上期提到过了,在启动的时候是存在GRUB中的,根据上文,我们回忆一下,切到保护模式的寻址方式发生了根本改变,那么是不是先得准备好保护模式所需要的寻址资料呢。
屏蔽中断
其实在准备寻址资料之前还需要一步叫屏蔽中断,保护模式下的中断和实模式下的可不一样,所以为了安全第一步就得屏蔽实模式下的中断,指令如下
cli
准备全局段描述符
然后得准备好全局段描述符 GDT用来存放实模式下的CS和IP的段地址信息;
接下来得有一个指向GDT的寄存器,它叫 GDTR寄存器 初始化这个值使其指向GDT基地址。
设置CR0
CR0也是CPU的寄存器之一,它可以控制CPU的重要特性,其中有一个位置就是代表了是否进入保护模式,是它的最低位——保护允许位,将这个位设置成1,就代表要开启保护模式。
长跳转
设置完CR0之后,就开始进入保护模式了,这个时候得需要长跳转指令,如下,
jmp 08h:_32bits_mode
为什么是长跳转?
长跳转的一个作用是可以清空CPU的指令流水线,在切换到保护模式前,有很多实模式下的指令进入到了指令流水线中,而这个时候CPU又切换了工作模式,就得把之前的指令都清空。
长跳转到08h这个地址执行,这个时候cs的值也就被重新赋值了。
初始化相关寄存器
长跳转完之后,最后就是进行相关寄存器的初始化,比如ax、bx、sp等,初始化完之后就按照保护模式的寻址方式进行寻址。
到此,CPU进入了保护模式运行,完了模式切换。
最后
回顾上面的内容,主要讲解了实模式的一些历史起源、寻址原理,以及保护模式下的几个概念和寻址方式,最后对两种模式的切换步骤做了描述,你可能有疑问了,是不是还有64位的工作模式,没错,是还有64位的工作模式——长模式,它的寻址原理和32位差不多,也是会有一个段描述符来存储信息,只是在校验方面有些不同,弄懂保护模式就很好理解长模式了。
更多内容你可以关注公众号: 「ConeZhang」