Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用

李倩 发表于 2018-04-13 15:16:19 收藏 已收藏
赞(0) •  评论(0

Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用

李倩 发表于 2018-04-13 15:16:19

这里我们主要介绍Intel 80x86系列CPU保护模式下最核心的部件中几个寄存器的作用,这些寄存器在Linux内核运行时起着至关重要的作用。至于其他那些各式各样的硬件设备,我们在讲解设备驱动时会针对具体的驱动程序来介绍的。首先,大家先看看CPU的主要架构:

EU(通用寄存器、运算器和控制器)执行部件:完成指令所要求的功能。

SU(段寄存器、段转换器)分段部件:完成执行单元的地址请求, 将虚地址转换为线性地址。

PU(TLB、页转换器)分页部件:将线性地址转换为物理地址。

BIU(总线接口)接口部件:完成指令预取请求和执行单元的数据存取请求,数据存取请求优先于指令预取请求。

IPU(控制逻辑和预取队列)预取部件:16字节指令预取队列, 提出预取请求。

IDU(指令译码、6字节指令队列)译码部件:完成指令译码功能。

FPU(片内集成了浮点协处理器):专用于浮点运算的处理部件。

下面,我们针对EU、SU和PU模块做做详细说明,其他模块就暂时不介绍了。

1 EU模块

EU模块是CPU中最核心,最重要的部件。现在的奔腾CPU已经发展了若干年了,但其中最起作用的还是加法单元ALU,一组通用寄存器组、一个标志和控制逻辑。如图:

首先,8个32位通用寄存器按使用情况分为三种:指针寄存器、变址寄存器、数据寄存器。

[1] 指针寄存器:主要提供全部或部分偏移量

ESP:专门存放堆栈段中栈顶单元的偏移量。

EBP:存放堆栈段中某个单元的全部/部分偏移量,也可存放32位或16位操作数或运算结果。

[2] 变址寄存器

ESI/EDI:存放主存操作数的全部/部分偏移量,也可存放16位操作数和结果,在多数情况功能可以互换。 但在串操作指令中作用不能互换,源操作数必须用ESI提供偏移量,目的操作数必须用EDI提供偏移量。

[3] 数据寄存器

◆ 数据寄存器既可以作为4个32位的寄存器,也可以作为8个16位的寄存器 ,还可以作为16个8位的寄存器。

◆ 在程序中,数据寄存器用来存放操作数、运算结果或其他信息。

◆ 数据寄存器在许多指令中要求指明使用,但也有隐含或特定使用,详细情况请查阅相关资料。

其次,4个控制寄存器CR0~CR3

[1] CR0:由80286的MSW寄存器演变而来,并增加了2位,Linux最看重他的PG位——PG=0,允许分页;PG=1,不允许分页。

[2] CR1:未使用

[3] CR2:页故障地址寄存器, 存放出现故障的页的32位线性地址

[4] CR3:页目录基地址寄存器, 存放页目录表的基地址。

最后来看看标志寄存器FR

FR用来记录程序执行时的状态,即两个操作数通过ALU后的状态:

[1] 进位标志位CF(Carry Flag)

[2] 奇偶标志位PF(Parity Flag)

[3] 辅助进位标志位AF(Auxiliary Carry Flag)

[4] 零值标志位ZF(Zero Flag)

[5] 符号标志位SF(Sign Flag)

[6] 溢出标志位OF(Overflow Flag)

[7] 单步标志位TF(Trace Flag)

[8] 中断标志位IF(Interrupt-enable Flag)

[9] 方向标志位DF(Direction Flag)

2 SU模块

下面着重看看SU部件。这个部件也被Linux用到了,但Linux用它的目的并不是遵循Intel手册对地址进行虚拟化,而是利用它来做用户态和内核态的切换。而对地址的虚拟化,则是通过PU单元,也就是分页机制来实现的。

首先来看看SU模块的架构图:

处理器提供了6个段寄存器,段寄存器的唯一的目的是存放选择子(16位)。这些段寄存器称为cs, ss, ds, es, fs和gs。尽管只有6个段寄存器,但程序可以把同一个段寄存器用于不同的目的,方法是先将其值保存在存储器中,用完后再恢复。

6个寄存器中3个有专门的用途:

cs——代码段寄存器,指向包含程序指令的段。

ss——栈段寄存器,指向包含当前程序栈的段。

ds——数据段寄存器,指向包含静态数据或者全局数据的段。

其它三个段寄存器作一般用途,可以指向任意的数据段。

每个段由一个8字节的描述子(Segment Descriptor)表示,它描述了段的特征。描述子放在全局描述符表(Global Descriptor Table, GDT)或局部描述符表(Local Descriptor Table, LDT)中,这些表位于内存中,如图所示。如果是多CPU,则每个CPU定义一个GDT,而每个进程除了存放在GDT中的段之外如果还需要创建附加的段,就可以有自己的LDT。GDT在主存中的基地址和大小存放在gdtr处理器寄存器中,当前正被使用的LDT地址和大小放在ldtr处理器寄存器中。

虚拟地址由16位选择子和32位偏移量组成,段寄存器仅仅存放选择子。CPU的分段单元(SU)执行以下操作:

[1] 先检查选择子的TI字段,以决定描述子对应的描述子保存在哪一个描述符表中。TI字段指明描述子是在GDT中(在这种情况下,分段单元从gdtr寄存器中得到GDT的线性基地址)还是在激活的LDT中(在这种情况下,分段单元从ldtr寄存器中得到LDT的线性基地址)。

[2] 从选择子的index字段计算描述子的地址,index字段的值乘以8(一个描述子的大小,其实就是屏蔽掉末尾那三位指示特权级的CPL和指示TI的字段),这个结果与gdtr或ldtr寄存器中的内容相加。

[3] 将对应的描述子从内存拷贝到CPU的隐Cache中,这样,只有在选择子改变的情况下才会修改Cache中的内容。

[4] 把逻辑地址的偏移量与隐Cache中描述子Base字段的值相加就得到了线性地址。

请注意,多亏了与段寄存器相关的不可编程的隐Cache,只有当段寄存器的内容被改变时才需要执行前三个操作。

LDT在Linux中使用得很少,我们就不细说他了,它跟我们下面讲的IDT差不多。

中断描述符表(Interrupt Descriptor Table,IDT)是一个系统表,它与每一个中断或异常向量相联系,每一个向量在表中有相应的中断或异常处理程序的入口地址。内核在允许中断发生前,必须适当地初始化IDT。

IDT的格式与这GDT和LDT的格式非常相似,表中的每一项对应一个中断或异常向量,每个向量由8个字节组成。因此,最多需要256×8=2048字节来存放IDT(Linux有256个中断向量)。

idtr寄存器使IDT可以位于内存的任何地方,它指定IDT的线性基地址及其大小(最大长度)。在允许中断之前,必须用lidt汇编指令初始化idtr。

IDT包含三种类型的描述符,下图显示了每种描述符中的64位的含义。尤