X86寄存器 (笔记)

本文内容来自于互联网,详见参考文档。

1. 16位cpu

在这里插入图片描述

8086的可编程寄存器包括:

通用寄存器
数据寄存器(16位):AX 、BX、CX、 DX
指针寄存器(16位):SP、BP
变址寄存器(16位):SI、DI
控制寄存器(16位):IP、FLAGS
段寄存器(16位):CS、DS、ES、SS

1.1 段寄存器

16位CPU的段寄存器是16位,是根据内存分段管理模式而设置的。
保护模式下逻辑地址由段寄存器的值(也就是段选择符)和一个偏移量组合而成的。通过段选择符可以找到段描述符,进而找到段基址,线性地址=段基址+偏移量。

ES为附加段寄存器。CS为代码段寄存器。SS为堆栈段寄存器。DS为数据段寄存器。

2. 32位cpu

在这里插入图片描述

80386共提供7种类型的可编程寄存器,如下:

  • 通用寄存器(32位):EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI
  • 段寄存器(16位):CS、SS、DS、ES、FS、GS
  • 指令指针寄存器和标志寄存器(32位):EIP、EFLAGS
  • 系统表寄存器:48位:GDTR、IDTR, 16位:LDTR、TR
  • 控制寄存器(32位):CR0、CR1、CR2、CR3、CR4
  • 调试寄存器(32位):DR0、DR1、DR2、DR3、DR4、DR5、DR6、DR7
  • 测试寄存器(32位):TR0、TR1、TR2、TR3、TR4、TR5、TR6、TR7

2.1 通用寄存器

提供了8个32bit的通用寄存器:

在这里插入图片描述

2.2 系统表寄存器

系统表寄存器主要用于x86的段寻址模式。
在这里插入图片描述

2.2.1 全局描述符表GDT(Global Descriptor Table)

  • GDTR

在整个系统中,全局描述符表GDT只有一张(一个处理器对应一个GDT),GDT可以被放在内存的任何位置,但CPU必须知道GDT的入口,也就是基地址放在哪里,Intel的设计者门提供了一个寄存器GDTR用来存放GDT的入口地址,程序员将GDT设定在内存中某个位置之后,可以通过LGDT指令将GDT的入口地址装入此寄存器,从此以后,CPU就根据此寄存器中的内容作为GDT的入口来访问GDT了。GDTR中存放的是GDT在内存中的基地址和其表长界限。

在这里插入图片描述

基地址指定GDT表中字节0在线性地址空间中的地址,表长度指明GDT表的字节长度值。指令LGDT和SGDT分别用于加载和保存GDTR寄存器的内容。在机器刚加电或处理器复位后,基地址被默认地设置为0,而长度值被设置成0xFFFF。在保护模式初始化过程中必须给GDTR加载一个新值。

  • GDT Entry

GDT中存储的表项的含义:
在这里插入图片描述

在这里插入图片描述

// 全局描述符表结构 http://www.cnblogs.com/hicjiajia/archive/2012/05/25/2518684.html
// base: 基址(注意,base的byte是分散开的)
// limit: 寻址最大范围 tells the maximum addressable unit
// flags: 标志位 见上面的AC_AC等
// access: 访问权限
struct gdt_entry {
    uint16_t limit_low;       
    uint16_t base_low;
    uint8_t base_middle;
    uint8_t access;
    unsigned limit_high: 4;
    unsigned flags: 4;
    uint8_t base_high;
} __attribute__((packed));
  • Selector (段寄存器:CS、SS、DS、ES、FS、GS / LDT / TR)

由GDTR访问全局描述符表是通过“段选择子”(实模式下的段寄存器)来完成的。段选择子是一个16位的寄存器(同实模式下的段寄存器相同)

在这里插入图片描述

段选择子包括三部分:描述符索引(index)、TI请求特权级(RPL)。

他的index(描述符索引)部分表示所需要的段的描述符在描述符表的位置,由这个位置再根据在GDTR中存储的描述符表基址就可以找到相应的描述符。然后用描述符表中的段基址加上逻辑地址(SEL:OFFSET)的OFFSET就可以转换成线性地址。
段选择子中的TI值只有一位0或1,0代表选择子是在GDT选择,1代表选择子是在LDT选择。
请求特权级(RPL)则代表选择子的特权级,共有4个特权级(0级、1级、2级、3级)。

关于特权级的说明:任务中的每一个段都有一个特定的级别。每当一个程序试图访问某一个段时,就将该程序所拥有的特权级与要访问的特权级进行比较,以决定能否访问该段。系统约定,CPU只能访问同一特权级或级别较低特权级的段。

例如给出逻辑地址:21h:12345678h转换为线性地址

a. 选择子SEL=21h=0000000000100 0 01b 他代表的意思是:选择子的index=4即100b选择GDT中的第4个描述符;TI=0代表选择子是在GDT选择;左后的01b代表特权级RPL=1

b. OFFSET=12345678h若此时GDT第四个描述符中描述的段基址(Base)为11111111h,则线性地址=11111111h+12345678h=23456789h
  • GDT 访问实例

在这里插入图片描述

当TI=0时表示段描述符在GDT中,如上图所示:

①先从GDTR寄存器中获得GDT基址。

②然后再GDT中以段选择器高13位位置索引值得到段描述符。

③段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

2.2.2 局部描述符表LDT(Local Descriptor Table)

  • LDT

局部描述符表可以有若干张,每个任务可以有一张。我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。如图:

在这里插入图片描述

  • LDTR

LDT和GDT从本质上说是相同的,只是LDT嵌套在GDT之中。LDTR记录局部描述符表的起始位置,与GDTR不同,LDTR的内容是一个段选择子。

由于LDT本身同样是一段内存,也是一个段,所以它也有个描述符描述它,这个描述符就存储在GDT中,对应这个表述符也会有一个选择子,LDTR装载的就是这样一个选择子。LDTR可以在程序中随时改变,通过使用lldt指令。如上图,如果装载的是Selector 2则LDTR指向的是表LDT2。

举个例子:如果我们想在表LDT2中选择第三个描述符所描述的段的地址12345678h。

1. 首先需要装载LDTR使它指向LDT2 使用指令lldt将Select2装载到LDTR

2. 通过逻辑地址(SEL:OFFSET)访问时SEL的index=3代表选择第三个描述符;TI=1代表选择子是在LDT选择,此时LDTR指向的是LDT2,所以是在LDT2中选择,此时的SEL值为1Ch(二进制为11 1 00b)。OFFSET=12345678h。逻辑地址为1C:12345678h

3. 由SEL选择出描述符,由描述符中的基址(Base)加上OFFSET可得到线性地址,例如基址是11111111h,则线性地址=11111111h+12345678h=23456789h

4. 此时若再想访问LDT1中的第三个描述符,只要使用lldt指令将选择子Selector 1装入再执行2、3两步就可以了(因为此时LDTR又指向了LDT1)

由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。

当进行任务切换时,处理器会把新任务LDT的段选择符和段描述符自动地加载进LDTR中。在机器加电或处理器复位后,段选择符和基地址被默认地设置为0,而段长度被设置成0xFFFF。

  • LDT 访问实例

在这里插入图片描述

当TI=1时表示段描述符在LDT中,如上图所示:

①还是先从GDTR寄存器中获得GDT基址。

②从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。

③以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。

④用段选择器高13位位置索引值从LDT段中得到段描述符。

⑤段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址yyyyyyyy才得到最后的线性地址。

2.2.3 任务状态段TSS(Task State Segment)

  • TSS

任务状态段(Task State Segment)是保存一个任务重要信息的特殊段。

在这里插入图片描述

TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。

任务状态段TSS的基本格式如下图所示。

在这里插入图片描述

从图中可见,TSS的基本格式由104字节组成。这104字节的基本格式是不可改变的,但在此之外系统软件还可定义若干附加信息。基本的104字节可分为链接字段区域内层堆栈指针区域地址映射寄存器区域寄存器保存区域其它字段等五个区域。

1.寄存器保存区域:

寄存器保存区域位于TSS内偏移20H至5FH处(32 - 95),用于保存通用寄存器、段寄存器、指令指针和标志寄存器。当TSS对应的任务正在执行时,保存区域是未定义的;在当前任务被切换出时,这些寄存器的当前值就保存在该区域。当下次切换回原任务时,再从保存区域恢复出这些寄存器的值,从而,使处理器恢复成该任务换出前的状态,最终使任务能够恢复执行。

2.高特权级堆栈指针区域:

为了避免高特权级的程序由于栈空间不足而崩溃以及不同特权级间栈数据的交叉引用。处理器在特权级发生变化时,当前任务的堆栈也必须发生变化。具体来说就是一个任务在每个不同的特权级下,都应该拥有一个独立的堆栈。特权级一共有4个等级,同时由于特权级的限制,高特权级的任务一般无法将控制转移至低特权级的段。
特权级为0的内核任务不需要额外的栈,使用自己固有的内核栈即可。特权级为1的任务需要定义一个额外的栈,用于将控制转移至特权级0时使用。依此类推,特权级为2的任务需要定义两个额外的栈,特权级为3的应用程序应该准备3个额外的堆栈以便在不同的特权级间进行切换。
这在TSS中有所体现,任务切换时也必须把这些额外的栈选择子/栈顶指针记录下来,其中SS0、SS1、SS2分别是任务在特权级0、1、2时的栈段选择子,ESP0、ESP1、ESP2分别是任务在特权级0、1、2时的栈顶指针寄存器的值。至于当前任务固有的栈,则位于上述的寄存器保存区域中的SS、ESP中。

3.地址映射寄存器区域

TSS的地址映射寄存器区域由位于偏移1CH处的双字字段(CR3)和位于偏移60H处的字字段(LDTR)组成。在任务切换时,处理器自动从要执行任务的TSS中取出这两个字段,分别装入到寄存器CR3和LDTR。这样就改变了虚拟地址空间到物理地址空间的映射。
但是,在任务切换时,处理器并不把换出任务但是的寄存器CR3和LDTR的内容保存到TSS中的地址映射寄存器区域。事实上,处理器也从来不向该区域自动写入。因此,如果程序改变了LDTR或CR3,那么必须把新值人为地保存到TSS中的地址映射寄存器区域相应字段中。可以通过别名技术实现此功能。

4.任务链接字段

80386允许在任务之间进行嵌套。在中断等情况下,可以暂时打断当前任务(如应用程序)并切换至更紧急的中断处理程序(如操作系统内核内存缺页中断处理程序)。
为了能够在任务切换时,新任务执行完成后再切换回之前的老任务。TSS中引入了任务链接字段,用于在嵌套任务切换时,记录当前任务的前一个任务的TSS段选择子。嵌套任务的切换工作原理,将在下文进行详细介绍。

5.其它字段

I/O映射位图基地址
前面的博客在I/O特权级保护中提到了I/O映射位图。和LDT一样,每个任务都拥有自己的I/O映射位图,TSS中的I/O映射位图基地址用于指向当前任务的I/O映射位图,其中的值代表着I/O许可位图相对于当前TSS内存段起始位置的偏移量。这也是为什么TSS段最小是104字节的原因,因为在104字节的基础结构之后,还允许拓展的存放一个I/O映射位图数据。
在寻找TSS时,如果发现该偏移量已经超过了TSS段描述符中的段界限,CPU不会报错,而是认为当前任务不存在I/O映射位图。这种情况下,如果任务的当前特权级CPL低于IOPL的话,将无权访问任何外设端口。

在TSS内偏移64H处的字是为任务提供的特别属性。在80386中,只定义了一种属性,即调试陷阱。该属性是字的最低位,用T表示。该字的其它位置被保留,必须被置为0。在发生任务切换时,如果进入任务的T位为1,那么在任务切换完成之后,新任务的第一条指令执行之前产生调试陷阱。

  • GDT Entry ( TSS Descriptor)
    在这里插入图片描述
    TSS作为段描述符的一种,和LDT段描述符,数据段/代码段描述符的格式差不多,最大的区别在于TYPE字段。TSS的TYPE字段为10B1,其中高2位,和最低位是固定不变的,而B位是忙(Busy)位的意思,B位为0时代表任务不处于忙状态,B位为1时代表任务处于忙状态。

当任务刚刚被初始化时,B位应该被操作系统设置为0。当任务处于执行状态时,或者因为中断等原因被暂时切换为挂起状态时,B位会被CPU固件设置为1。

  • TR (任务寄存器)

TSS对应的段描述符必须预先加载进全局描述符表GDT中。

80386也提供了另一个专用的寄存器指向当前任务的TSS段,这个专用的寄存器被称为任务寄存器(Task Resigter TR)。TR寄存器是16位的,其中装载的是所指向的TSS段的段选择子。任务切换时,将TR中的段选择子指向新任务的TSS段即可。

  • 80386任务切换的方式

80386允许在4种情况下进行任务切换:

1、当前任务执行了一条引用TSS描述符的JMP或者CALL指令

使用jmp far或者call far时,如果给出的段选择子指向的是普通的代码段,那么CPU认为是普通的跳转或是过程调用,依然是同一个任务,不会发生不同任务上下文的切换。  
使用jmp far或者call far给出的段选择子指向的是一个TSS系统段时,CPU将进行任务切换。

2、当前任务执行了一条引用任务门的JMP或者CALL指令

使用jmp far或者call far时,如果给出的段描述符指向的是一个任务门时,CPU将进行任务切换。

3、引用中断描述符表(IDT)中的任务门中断或异常

一般的中断处理可以使用中断门或者陷阱门进行处理,此时CPU认为这是在同一个任务内的控制转移。但是如果中断号对应的是任务门,则CPU认为这是一次任务切换。

4、当嵌套任务标志NT置位时,当前任务执行了一条IRET指令

中断发生时,可以是同一个任务内进行常规的中断处理,也可以进行任务切换,这两者都可以通过IRET中断返回指令进行返回。同一任务内的中断返回会跳转回中断触发时的代码段,当前任务继续。使用任务切换进行的任务处理将会在返回时,切换回中断发生前的任务。  
IRET指令返回时,用于区分上述两种类型的方式是判断标志寄存器EFLAGS的NT位(第14位)的值,NT的意思是Nested Task,NT位是嵌套位置标识。NT位=1时,代表当前执行的任务是嵌套于另一个任务中的,并且能够在任务返回时通过TSS中的任务链接字段返回到前一个任务。

2.2.4 中断描述符表IDT(Interrupt Descriptor Table)

  • IDTR (中断描述符表寄存器)

中断处理过程是由CPU直接调用的,CPU有专门的寄存器IDTR来保存IDT在内存中的位置。

在这里插入图片描述

IDTR有48位,前32为是IDT在内存中的位置(线性地址),本文写为IDTR.base,后16为是IDT的大小,本文写为IDT.limit。程序可以使用LIDT和SIDT指令来读写IDTR。

  • IDT Entry

中断描述符IDT表示一个系统表,它与中断或异常向量相联系。每一个中断或异常向量在这个系统表中有对应的中断或异常处理程序入口地址。中断描述符的每一项对应一个中断或异常向量,每个向量由8个字节组成。因此,最多需要256*8=2048字节来存放IDT。

IDT是一个最大为256项的表,每个表项为8字节。称为中断门。CPU通过IDT.base+n*8来寻找门。根据中断号对应的异常类型不同(Faults/Traps/Aborts)8个字节的意义也不同。

在这里插入图片描述

其中的关键字段含义如下:

Segment Selector,中断服务程序锁对应的段寄存器。
Offset,中断服务程序在段中的偏移。
DPL,描述符权限等级。

以上为Intel对中断描述符的分类:

任务门(task gate) 
    当中断信号发生时,必须取代当前进程的那个进程的TSS选择符存放在任务门中。

中断门(interruptgate) 
    包含段选择符和中断或异常处理程序的段内偏移量.当控制权转移到一个适当的段时,处理器 清IF标志,从而关闭将来会发生的可屏蔽中断。

陷阱门(Trap gate)
    与中断门相似,只是控制权传递到一个适当的段时处理器不修改IF标志。

Linux采用了更细的分类方法

中断门(interrupt gate)
    用户态的进程不能访问的一个lntel中断门(门的DPL字段为0)。所有的Linux中断处理程序都通过中断门激活,并全部限制在内核态。

系统门(syslem gate)
    用户态的进程可以访问的一个Intel陷阱门(门的DPL字段为到.通过系统门来激活三个Linux异常处理程序,它们的向量是4,5及128,因此,在用户态下.可以发布into、 bound及int $Ox80三条汇编语言指令。

系统中断门(system interrupt gate)
    能够被用户态进程访问的Intel中断门(门的DPL字段为3). 与向量3相关的异常处理程序是由系统中断门激活的,因此,在用户态可以使用汇编语言指令int3。

陷阱门(Irapgate)
    用户态的进程不能访问的一个Inte)陷阱门(f]的DPL字段为0). 大部分Linux异常处理程序都通过陷阱门来激活。

任务门(task gate)
    不能被用户态进程访问的Intel任务门(门的DPL字段为0).Linux对"Doublefault"异常的处理程序是由任务门激活的。

2.3 段寄存器

80386有6个16位段寄存器,分别是:CS(代码段寄存器),DS(数据段寄存器),SS(堆栈段寄存器),ES、FS 、GS(附加数据段寄存器)。前4个段寄存器的名称与8086相同,在实地址方式下使用方式也和8086相同。80386又增加了FS与GS,主要为了减轻对DS段和ES段的压力。除CS支持代码段,SS支持堆栈段外,程序员可以利用其它的所有段寄存器支持数据段。

段寄存器中存放的不再是某个段的基地址,而是某个段的选择符(Selector)。因为16 位的寄存器无法存放32位的段基地址,段基地址只好存放在段的描述符(Descriptor)中。

每个段寄存器对应这一个64位段描述符寄存器,这在8086中是没有的,它的作用是快速访问段描述符。当段寄存器载入段选择符时,64位的段描述符寄存器就载入GDT中的对应的段描述符。

2.4 指令指针寄存器和标志寄存器/控制寄存器/调试寄存器/测试寄存器

具体参考X86系列CPU标准寄存器

3. 64位cpu

在这里插入图片描述

3.1 通用寄存器

X86-64中,所有寄存器都是64位,相对32位的x86来说,标识符发生了变化,比如:从原来的%ebp变成了%rbp。为了向后兼容性,%ebp依然可以使用,不过指向了%rbp的低32位。

X86-64寄存器的变化,不仅体现在位数上,更加体现在寄存器数量上。新增加寄存器%r8到%r15。加上x86的原有8个,一共16个寄存器。

%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。

在这里插入图片描述

GCC就可以更多的使用寄存器,替换之前的存储器堆栈使用,从而大大提升性能:

%rax 作为函数返回值使用。
%rsp 栈指针寄存器,指向栈顶
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则(Callee Save),简单说就是随便用,调用子函数之前要备份它,以防他被修改
%r10,%r11 用作数据存储,遵循调用者使用规则(Caller Save),简单说就是使用之前要先保存原值

3.2 Caller/Callee Save

“Caller Save” 和 “Callee Save” 寄存器的区分,即寄存器的值是由“调用者保存” 还是由 “被调用者保存”。当产生函数调用时,子函数内通常也会使用到通用寄存器,那么这些寄存器中之前保存的调用者(父函数)的值就会被覆盖。为了避免数据覆盖而导致从子函数返回时寄存器中的数据不可恢复,CPU 体系结构中就规定了通用寄存器的保存方式。

  • 如果一个寄存器被标识为”Caller Save”, 那么在进行子函数调用前,就需要由调用者提前保存好这些寄存器的值,保存方法通常是把寄存器的值压入堆栈中,调用者保存完成后,在被调用者(子函数)中就可以随意覆盖这些寄存器的值了。
  • 如果一个寄存被标识为“Callee Save”,那么在函数调用时,调用者就不必保存这些寄存器的值而直接进行子函数调用,进入子函数后,子函数在覆盖这些寄存器之前,需要先保存这些寄存器的值,即这些寄存器的值是由被调用者来保存和恢复的。

3.3 x86-64函数调用及栈帧原理

在frame point模式下,x86使用bp寄存器来保存帧指针。

  • 函数的调用

子函数调用时,调用者与被调用者的栈帧结构如下图所示:

在这里插入图片描述

在子函数调用时,执行的操作有:

父函数将调用参数从后向前压栈 -> 
↓
将返回地址压栈保存 -> 
↓
跳转到子函数起始地址执行 -> 
↓
子函数将父函数栈帧起始地址(%rpb) 压栈 -> 
↓
将 %rbp 的值设置为当前 %rsp 的值,即将 %rbp 指向子函数栈帧的起始地址。

对应的伪指令如下:

...   # 参数压栈
call FUNC  # 将返回地址压栈,并跳转到子函数 FUNC 处执行
...  # 函数调用的返回位置

FUNC:  # 子函数入口
pushq %rbp  # 保存旧的帧指针,相当于创建新的栈帧
movq  %rsp, %rbp  # 让 %rbp 指向新栈帧的起始位置
subq  $N, %rsp  # 在新栈帧中预留一些空位,供子程序使用,用 (%rsp+K) 或 (%rbp-K) 的形式引用空位
  • 函数的返回

函数返回时,我们只需要得到函数的返回值(保存在 %rax 中),之后就需要将栈的结构恢复到函数调用之差的状态,并跳转到父函数的返回地址处继续执行。由于函数调用时已经保存了返回地址和父函数栈帧的起始地址,要恢复到子函数调用之前的父栈帧,我们只需要执行以下两条指令:

movq %rbp, %rsp    # 使 %rsp 和 %rbp 指向同一位置,即子栈帧的起始处
popq %rbp # 将栈中保存的父栈帧的 %rbp 的值赋值给 %rbp,并且 %rsp 上移一个位置指向父栈帧的结尾处

为了便于栈帧恢复,x86-64 架构中提供了 leave 指令来实现上述两条命令的功能。执行 leave 后,前面图中函数调用的栈帧结构如下:

在这里插入图片描述

可以看出,调用 leave 后,%rsp 指向的正好是返回地址,x86-64 提供的 ret 指令,其作用就是从当前 %rsp 指向的位置(即栈顶)弹出数据,并跳转到此数据代表的地址处,在leave 执行后,%rsp 指向的正好是返回地址,因而 ret 的作用就是把 %rsp 上移一个位置,并跳转到返回地址执行。可以看出,leave 指令用于恢复父函数的栈帧,ret 用于跳转到返回地址处,leave 和ret 配合共同完成了子函数的返回。当执行完成 ret 后,%rsp 指向的是父栈帧的结尾处,父栈帧尾部存储的调用参数由编译器自动释放。

参考文档:

1.X86系列CPU标准寄存器
2.GDT,LDT,GDTR,LDTR 详解
3.全局描述符表GDT
4.TR and tss
5.80386任务切换机制

posted @ 2020-07-10 19:16  pwl999  阅读(312)  评论(0编辑  收藏  举报