x86 - CPU架构/寄存器详解 (三) 保护模式

系列文章

x86 - CPU架构/寄存器详解 (一)x86、8086、i386、IA-32 是什么?
x86 - CPU架构/寄存器详解 (二) 实模式(8086模式)
x86 - CPU架构/寄存器详解 (三) 保护模式
x86 - 分段与分页详解
x86 - 特权级别 CPL / RPL / DPL / IOPL
x86 - 操作系统:中断、陷阱、异常、故障、终止
x86 - 描述符详解:存储/系统段描述符、门描述符

  80286 也是一款 16 位的处理器,大部分的寄存器都和 8086 处理器一样。因此,80286 和 8086 一样,因为段寄存器是 16 位的,而且只能使用 16 位的偏移地址,在实模式下只能使用 64KB 的段;尽管它有 24 根地址线,理论上可以访问 2^24,即 16MB 的内存,但依然只能分成多个段来进行。但是,80286 和 8086 不一样的地方在于,它第一次提出了保护模式的概念。在保护模式下,段寄存器中保存的不再是段地址,而是段选择子,真正的段地址位于段寄存器的描述符高速缓存中,是 24 位的。因此,运行在保护模式下的 80286 处理器可以访问全部 16MB 内存。
  不过,由于 80286 的通用寄存器是 16 位的,只能提供 16 位的偏移地址,因此,和 8086 一样,即使是运行在保护模式下,段的长度依然不能超过 64KB。对段长度的限制妨碍了 80286 处理器的应用,这就是 16 位保护模式很少为人所知的原因。

  1985 年的 80386 处理器是 Intel 公司的第一款 32 位产品,而且获得了极大成功,是后续所有 32 位产品的基础。和 8086/80286 不同,80386 处理器的寄存器是 32 位的,而且拥有 32 根地址线,可以访问 2^32,即 4GB 的内存。
  80386,以及所有后续的 32 位处理器,都兼容实模式,可以运行实模式下的 8086 程序。而且,在刚加电时,这些处理器都自动处于实模式下,此时,它相当于一个非常快速的 8086 处理器。只有在进行一番设置之后,才能运行在保护模式下。在保护模式下,所有的 32 位处理器都可以访问多达 4GB 的内存,它们可以工作在分段模型下,每个段的基地址是 32 位的,段内偏移量也是 32 位的,因此,段的长度不受限制。在最典型的情况下,可以将整个 4GB 内存定义成一个段来处理,这就是所谓的平坦模式。在平坦模式下,可以执行 4GB 范围内的控制转移,也可以使用 32 位的偏移量访问任何 4GB 范围内的任何位置。32 位保护模式兼容 80286 的 16 位保护模式。
  除了保护模式,32 位处理器还提供虚拟 8086 模式(V86 模式),在这种模式下,IA-32 处理器被模拟成多个 8086 处理器并行工作。V86 模式是保护模式的一种,可以在保护模式下执行多个 8086 程序。传统上,要执行 8086 程序,处理器必须工作在实模式下。在这种情况下,为 32 位保护模式写的程序就不能运行。但是,V86 模式提供了让它们在一起同时运行的条件。V86 模式曾经很有用,因为在那个时候,8086 程序很多,而 32 位应用程序很少,这个过渡期是必需的。现在,这种工作模式已经基本无用了。

CPU架构

在这里插入图片描述  保护模式下,寄存器进行了扩展和添加,包括以下几类:

类型
通用寄存器8个,分别为EAX,EBX,ECX,EDX,ESP,EBP,ESI,EDI
E 表示 Extend,对16位寄存器进行拓展
标志寄存器EFLAGS
控制寄存器5个,分别为CR0-CR4(Control Register,CR)
系统地址寄存器4个,GDTR、IDTR、LDTR和TR
GDTR(Global Descriptor Table):全局描述符表
IDTR(Interrupt Descriptor Table Register):中断描述符表寄存器
LDTR(Local Descriptor Table Register):局域描述图表寄存器
TR(Task Register):任务寄存器
调试寄存器8个,分别为DR0-DR7(Debug Register,DR)
16位段寄存器6个,分别为CS,DS,ES,FS,GS,SS
CS:Code Segment - 代码段
DS:Data Segment - 数据段
ES:Extra Segment - 附加段
FS、GS:排在DS、ES后的新增段寄存器,没有全称
SS:Stack Segment - 栈段
其他寄存器EIP(Extend Instruction Pointer)、TSC(Time Stamp Counter,时间戳计数器)等

通用寄存器

  为了在汇编语言程序中使用经过扩展(Extend)的寄存器,需要给它们命名,它们的名字分别是 EAX、EBX、ECX、EDX、ESI、EDI、ESP 和 EBP。可以在程序中使用这些寄存器,即使是在实模式下。不过,32 位通用寄存器的高 16 位是不可独立使用的,但低 16 位保持同 16 位处理器  尽管这8个通用寄存器大多时候是通用的,可以用作任何用途,但是在某些情况下,他们也有隐含的用法。比如ECX、ESI和EDI在串循环操作中分别用作计数器、源和目标。EBP和ESP主要用来维护栈,ESP通常指向栈的顶部,EBP指向当前栈帧的起始地址。
在这里插入图片描述

名称作用
EAX累加器(Accumulator),EAX是很多加法乘法的缺省寄存器,存放函数的返回值,用累加器进行的操作可能需要更少时间,在80386及其以上的微处理器中可以用来存放存储单元的偏移地址。AX寄存器是算术运算的主要寄存器。
EBX基地址寄存器(Base Register),主要用于在内存寻址时存放基地址。
ECX计数寄存器(Count Register)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;是重复(REP)前缀指令和LOOP指令的内定计数器。ECX寄存器也可以保存访问数据所在存储器单元的偏移地址。
EDX数据寄存器(Data Register),它的低16位即是DX,而DX又可分为高8位DH和低8位DL。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址;且总是被用来放整数除法产生的余数。
ESI/EDI分别叫做源/目标索引寄存器(Source/Destination Index Register)。它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串。此外,它们又作为通用寄存器可以进行任意的常规的操作,如加减移位或普通的内存间接寻址。
EBP/BSP分别是基址针寄存器(Base Pointer Register)/ 堆栈指针寄存器(Stack Pointer Register),其内存分别放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶/底部。主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。
指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。并且规定:BP为基指针(Base Pointer)寄存器,用它可直接存取堆栈中的数据;SP为堆栈指针(Stack Pointer)寄存器,用它只可访问栈顶。在32位平台上,ESP每次减少4字节。EBP最经常被用作高级语言函数调用的"框架指针"(frame pointer),EBP 构成了函数的一个框架,在C++反汇编中EBP通常是局部变量、传进来的参数。这里要注意在intel系统中栈是向下生长的(栈越扩大其值越小,堆恰好相反)。在通常情况下ESP是可变的,随着栈的生长而逐渐变小,而ESB寄存器是固定的,只有当函数的调用后,发生入栈操作而改变,在函数执行结束之后需要还原。

  关于堆栈操作的一些小知识:
  • SP(Stack Pointer),栈指针,在 32 位系统中,ESP(Extended SP) 寄存器存放的就是栈指针。在 64 位系统中,表现为 RSP 寄存器。SP 永远指向系统栈最上面一个栈帧的栈顶。所以 SP 是栈顶指针
  • BP(Base Pointer),基指指针,在 32 位系统中,EBP(Extended BP)寄存器存放的就是基指指针。在 64 位系统中,表现为 RBP 寄存器。BP 指向栈帧的底部,一般称之为栈底指针

  SP,指针即地址,存放栈顶指针,目的就是,下一次对栈操作的时候,系统可以及时找到栈的当前位置。 举个例子来说,push 压入一个操作数,会在 sp + 4 的地址的内存空间,存入一个字长的操作数;
   ebp 的作用之一是找到函数的形参,当然栈中的局部变量也是通过 ebp 来定位的,子程序就是通过 ebp + 偏移量 调用主程序传递来的参数的,在没有调用其他函数的时候,函数内的 ebp 一般保持不变。

状态寄存器

  EFLAGS:主要用于提供程序的状态及进行相应的控制。32位的EFLAGS寄存器包含一组状态标志、系统标志以及一个控制标志。在x86处理器初始化之后,EFLAGS寄存器的状态值为0000 0002H。第1、3、5、15以及22到31位均被保留,这个寄存器中的有些标志通过使用特殊的通用指令可以直接被修改,但并没有指令能够检查或者修改整个寄存器。通过使用LAHF/SAHF/PUSHF/POPF/POPFD等指令,可以将EFLAGS寄存器的标志位成组移到程序栈或EAX寄存器,或者从这些设施中将操作后的结果保存到EFLAGS寄存器中。在EFLAGS寄存器的内容被传送到栈或是EAX寄存器后,可以通过位操作指令(BT,BTS, BTR, BTC)检查或修改这些标志位。当调用中断或异常处理程序时,处理器将在程序栈上自动保存EFLAGS的状态值。若在中断或异常处理时发生任务切换,那么EFLAGS寄存器的状态将被保存在TSS中,注意是将要被挂起的本次任务的状态。
在这里插入图片描述

状态标志(Status Flags)

  EFLAGS寄存器的状态标志(0、2、4、6、7以及11位)是指示算术指令(如ADD, SUB, MUL以及DIV指令)的结果,这些状态标志的作用如下:

标志位英文名称用途
CF(bit 0)[Carry flag]若算术操作产生的结果在最高有效位(most-significant bit)发生进位或借位则将其置1,反之清零。这个标志指示无符号整型运算的溢出状态,这个标志同样在多倍精度运算(multiple-precision arithmetic)中使用。
PF(bit 2)[Parity flag]如果结果的最低有效字节(least-significant byte)包含偶数个1位则该位置1,否则清零。
AF(bit 4)[Adjust flag]如果算术操作在结果的第3位发生进位或借位则将该标志置1,否则清零。这个标志在BCD(binary-code decimal)算术运算中被使用。
ZF(bit 6)[Zero flag]若结果为0则将其置1,反之清零。
SF(bit 7)[Sign flag]该标志被设置为有符号整型的最高有效位。(0指示结果为正,反之则为负)
OF(bit 11)[Overflow flag]如果整型结果是较大的正数或较小的负数,并且无法匹配目的操作数时将该位置1,反之清零。这个标志为带符号整型运算指示溢出状态。

  在这些状态标志中,只有CF标志能够通过使用STC, CLC以及CMC指令被直接修改,或者通过位指令(BT, BTS, BTR以及BTC)将指定的位拷贝至CF标志中。
  这些状态标志允许单个的算术操作产生三种不同数据类型的结果:无符号整型,有符号整型以及BCD整型。如果把该结果当做无符号整型,那么CF标志指示越界(out-of-range)状态——即进位或借位,如果被当做有符号整型,则OF标志指示进位或借位,若作为BCD数,那么AF标志指示进位或借位。SF标志指示有符号整数的符号位,ZF指示结果为零。此外在执行多倍精度算术运算时,CF标志用来将一次运算过程中带进位的加法(ADC)或带借位的减法(SBB)产生的进位或借位传递到下一次运算过程中。

控制标志(DF flag)
标志位英文名称用途
DF(bit10)[Direction flag]控制串指令(MOVS, CMPS, SCAS, LODS以及STOS)。设置DF标志使得串指令自动递减(从高地址向低地址方向处理字符串),清除该标志则使得串指令自动递增。STD以及CLD指令分别用于设置以及清除DF标志。
系统标志以及IOPL域(System Flags and IOPL Field)

  EFLAGS寄存器中的这部分标志用于控制操作系统或是执行操作,它们不允许被应用程序所修改。这些标志的作用如下:

标志位英文名称用途
TF(bit 8)[Trap flag]将该位设置为1以允许单步调试模式,清零则禁用该模式。
IF(bit 9)[Interrupt enable flag]该标志用于控制处理器对可屏蔽中断请求(maskable interrupt requests)的响应。置1以响应可屏蔽中断,反之则禁止可屏蔽中断。
IOPL(bits 12 and 13)[I/O privilege level field]指示当前运行任务的I/O特权级(I/O privilege level),正在运行任务的当前特权级(CPL)必须小于或等于I/O特权级才能允许访问I/O地址空间。这个域只能在CPL为0时才能通过POPF以及IRET指令修改。
NT(bit 14)[Nested task flag]这个标志控制中断链和被调用任务。若当前任务与前一个执行任务相关则置1,反之则清零。
RF(bit 16)[Resume flag]控制处理器对调试异常的响应。
VM(bit 17)[Virtual-8086 mode flag]置1以允许虚拟8086模式,清除则返回保护模式。
AC(bit 18)[Alignment check flag]该标志以及在CR0寄存器中的AM位置1时将允许内存引用的对齐检查,以上两个标志中至少有一个被清零则禁用对齐检查。
VIF(bit 19)[Virtual interrupt flag]该标志是IF标志的虚拟镜像(Virtual image),与VIP标志结合起来使用。使用这个标志以及VIP标志,并设置CR4控制寄存器中的VME标志就可以允许虚拟模式扩展(virtual mode extensions)
VIP(bit 20)[Virtual interrupt pending flag]该位置1以指示一个中断正在被挂起,当没有中断挂起时该位清零。与VIF标志结合使用。
ID(bit 21)[Identification flag]程序能够设置或清除这个标志指示了处理器对CPUID指令的支持。

系统地址寄存器

  全局描述符表GDT、局部描述符表LDT和中断描述符表IDT等都是保护方式下非常重要的特殊段,它们包含有为段机制所用的重要表格。为了方便快速地定位这些段,处理器采用一些特殊的寄存器保存这些段的基地址和段界限。我们把这些特殊的寄存器称为系统地址寄存器。
  关于表项结构,我会在后面的文章中详细介绍。
在这里插入图片描述

GDTR

  全局描述符表寄存器(Global Descriptor Table Register),指向全局描述符表 GDT
在这里插入图片描述

  GDTR长48位,其中高32位为基地址,低16位为界限。由于GDT 不能有GDT本身之内的描述符进行描述定义,所以处理器采用GDTR为GDT这一特殊的系统段提供一个伪描述符,GDTR给定了GDT。GDTR中的段界限以字节为单位。由于段选择子中只有13位作为描述符索引,而每个描述符长8个字节,所以用16位的界限足够。通常,对于含有N个描述符的描述符表的段界限设为8*N-1。
  理论上,全局描述符表可以位于内存中的任何地方。但是,如图 11-2 所示,由于在进入保护模式之后,处理器立即要按新的内存访问模式工作,所以,必须在进入保护模式之前定义 GDT。但是,由于在实模式下只能访问 1MB 的内存,故 GDT 通常都定义在 1MB 以下的内存范围中。当然,允许在进入保护模式之后换个位置重新定义 GDT。
  处理器并不使用 GDT中的第一个描述符。把这个"空描述符"加载到段寄存器中并不会产生一个异常。但是,如果使用这些加载了空描述符的段选择符来访问内存就会引发一般保护性异常。通过使用这个段选择符初始化段寄存器,就会引发异常。

LDTR

  局部描述符表寄存器(Local Descriptor Table Register),指向局部描述符表 LDT

  首先,在多任务系统中,应用程序的数量是不确定的,应用程序也会执行结束。如果把所有应用程序的段描述符都放在 GDT 中,对于操作系统来说,管理这个数据太复杂;
  其次,当引入特权级别之后,如果应用程序的段描述符放在 GDT 中,那么就意味着应用程序需要有权限来访问 GDT,而 x86 系统中只有一个 GDT(所以叫做 Global Description Table),只能被操作系统访问;
  因此,操作系统需要为每一个应用程序,单独申请一块空间,用作这个程序自己的段描述附表,称作:LDT(Local Description Table)。

  局部描述符表寄存器LDTR规定当前任务使用的局部描述符表LDT。LDTR类似于段寄存器,由程序员可见的16位的寄存器和程序员不可见的高速缓冲寄存器组成。实际上,每个任务的局部描述符表LDT作为系统的一个特殊段,由一个描述符描述。而用于描述符LDT的描述符存放在GDT中。在初始化或任务切换过程中,把描述符对应任务LDT的描述符的选择子装入LDTR,处理器根据装入LDTR可见部分的选择子,从GDT中取出对应的描述符,并把LDT的基地址、界限和属性等信息保存到LDTR的不可见的高速缓冲寄存器中。随后对LDT的访问,就可根据保存在高速缓冲寄存器中的有关信息进行合法性检查。
  LDTR寄存器包含当前任务的LDT的选择子。所以,装入到LDTR的选择子必须确定一个位于GDT中的类型为LDT的系统段描述符,也即选择子中的TI位必须是0,而且描述符中的类型字段所表示的类型必须为LDT。可以用一个空选择子装入LDTR,这表示当前任务没有LDT。在这种情况下,所有装入到段寄存器的选择子都必须指示GDT中的描述符,也即当前任务涉及的段均由GDT中的描述符来描述。如果再把一个TI位为1的选择子装入到段寄存器,将引起异常。
  当任务切换时,LDT会更换成新任务的LDT,GDT不会改变,因为GDT所映射的一半虚拟地址空间是系统中所有任务公有的,LDT所映射的另一半则在任务切换时被改变。系统中所有任务共享的段有GDT来映射。系统中每个应用程序对应一个任务,并且每个任务都有自己的LDT,当它需要引用自身的LDT时,它需要通过lldt指令将其LDT的段描述符装入此寄存器。lldt指令与lgdt指令不同的时,lgdt指令的操作数是一个32-bit的内存地址,这个内存地址处存放的是一个32-bit GDT的入口地址,以及16-bit的GDT Limit。而lldt指令的操作数是一个16-bit的选择子。
在这里插入图片描述
  至此,我们可以这样理解GDT和LDT:GDT为一级描述符表,LDT为二级描述符表。由于每个进程都有自己的一套程序段、数据段、堆栈段,有了局部描述符表则可以将每个进程的程序段、数据段、堆栈段封装在一起,只要改变LDTR就可以实现对不同进程的段进行访问。

IDTR

  中断描述符表寄存器(Interruptions Descriptor Table Register),指向IDT
  中断描述符表寄存器IDTR指向中断描述符表IDT。IDTR长48 位,其中32位的基地址规定IDT的基地址,16位的界限规定IDT的段界限。由于80386只支持256个中断异常,所以IDT表最大长度是2K,以字节位单位的段界限为7FFH。IDTR 指示IDT的方式与GDTR指示GDT的方式相同。
  在实模式下,位于内存最底端的1KB内存,是中断向量表(IVT,Interrupt Vector Table),定义了256种中断的入口地址,包括16位段地址和16位段内偏移量。当中断发生时,处理器要么自发产生一个中断向量,要么从 int n 指令中得到中断向量,或者从外部的中断控制器接受一个中断向量。然后,它将该向量作为索引访问中断向量表。具体的做法是,将中断向量乘以4,作为表内偏移量访问中断向量表,从中取得中断处理过程的段地址和偏移地址,并转到那里执行;
  在保护模式下,处理器对中断的管理是相似的,但并非使用传统的中断向量表来保存中断处理过程的地址,而是中断描述符表(IDT),表里保存的是和中断处理过程有关的描述符(8个字节),包括中断门、陷阱门和任务门:
在这里插入图片描述
在这里插入图片描述  事实上,调用门、任务门、中断门和陷阱门的描述符都十分相似,从大的方面来说,因为都用于实施控制转移,因此都包括16位的目标代码段选择子,以及32位的段内偏移量。此外,中断门和陷阱门描述符只允许存放在IDT内,任务门可以位于GDT、LDT和IDT中。
  和实模式下的中断向量表(IVT)不同,保护模式下的IDT不要求必须位于内存的最低端。和GDT一样,IDTR指向IDT,因为整个系统只需要一个IDT,所以GDTR和IDTR都直接存储目标地址,不需要选择器部分。

初始化:

  • 内核在启用中断机制之前,必须把IDT表的起始地址载入IDTR寄存器,并初始化表中的每一个表项。
  • IDT被初始化两次。第一次是在BIOS程序中,此时CPU还运行在实模式下,IDT被初始化并由bootloader程序使用。一旦操作系统启动,IDT会被搬运到RAM的受保护区域并被第二次初始化。
  • 在系统的初始化阶段,内核用来设置IDTR寄存器,专用汇编指令是lidt。

异常及中断处理:
  当CPU执行完当前指令后,需要判断是否发生了异常或者中断。如果确实发生,则:

  1. 确定所发生的异常或者中断在IDT表中的id(0-255)
  2. 通过IDTR寄存器加载IDT表的基地址,并读取对应id的表项

  CPU通过相应表项的门描述符的段选择子,跳转到异常或者中断处理程序中执行。

但要注意下面两点:

  • 权限检查:检查CPU的当前权限CPL与IDT表中相应表项的DPL,权限低的代码可以访问权限级别高的代码
  • 检查是否发生了特权级的变化。若中断发生时CPU运行在用户空间 ,而中断处理程序运行在内核态,特权级发生了变化,会引起堆栈的更换,即从用户堆栈切换到内核堆栈。而当中断发生在内核态时,即CPU 在内核中运行时,则不会更换堆栈。
TR

  任务寄存器(Task Register)
  任务寄存器拥有当前任务的TSS(Task Status Segment)的段选择符和段描述符(32位基地址,16位段界限和描述符参数)。任务寄存器具有可见部分(软件可以读和写)和不可见部分(只能被处理器访问,软件不能读写),可见部分中的段选择符指向GDT中TSS描述符,不可见部分缓存TSS的描述符,当把任务状态段的选择子装入到TR可见部分时,处理器自动把选择子所索引的描述符中的段基地址等信息保存到不可见的高速缓冲寄存器中。在此之后,对当前任务状态段的访问可快速方便地进行。
  指令LTR(加载任务寄存器)和STR(保存任务寄存器)加载和保存任务寄存器的可见部分。LTR指令让任务寄存器加载TSS描述符的段选择符,该指令只能运行在特权级0,该指令通常用来系统初始化时初始化任务寄存器。装入到TR的选择子不能为空,必须索引位于GDT中的描述符,且描述符的类型必须是TSS。指令STR可以将任务寄存器的可见部分保存到通用寄存器或内存中。该指令可以运行在任何特权级。
  TSS描述符仅可能存放在GDT中,不能存放在LDT或IDT中。
在这里插入图片描述
  TSS在任务切换过程中起着重要作用,通过它实现任务的挂起和恢复。所谓任务切换是指,挂起当前正在执行的任务,恢复或启动另一任务的执行。在任务切换过程中,首先,处理器中各寄存器的当前值被自动保存到TR(任务寄存器)所指定的TSS中;然后,下一任务的TSS的选择子被装入TR;最后,从TR所指定的TSS中取出各寄存器的值送到处理器的各寄存器中。由此可见,通过在TSS中保存任务现场各寄存器状态的完整映象,实现任务的切换。

  注意:x86CPU的构想是每一个任务对应一个TSS,然后由TR寄存器指向当前的任务,执行任务切换时,修改TR寄存器的指向即可,这是硬件层面的多任务切换机制。这个构想其实还是很不错的,然而现实却打了脸,包括Linux和Windows在内的主流操作系统都没有使用这个机制来进行线程切换,而是自己使用软件来实现多线程切换。
  所以,绝大多数情况下,TR寄存器都是指向固定的,即便线程切换了,TR寄存器仍然不会变化。虽然操作系统不依靠TSS来实现多任务切换,但这并不意味着CPU提供的TSS操作系统一点也没有使用。还是存在一些特殊情况,如一些异常处理会使用到TSS来执行处理。

  当段选择子的TI置零或置一时,分别使用 GDT 与 LDT 进行段寻址:

TI=0,GTDTI=1,LDT
当TI=0时表示段描述符在GDT中:
  ① 先从GDTR寄存器中获得GDT基址。
  ② 然后再GDT中以段选择器高13位位置索引值得到段描述符。
  ③ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到段的起始地址(基址),再以基址加上偏移地址才得到最后的线性地址。
当TI=1时表示段描述符在LDT中:
  ① 还是先从GDTR寄存器中获得GDT基址。
  ② 从LDTR寄存器中获取LDT所在段的位置索引(LDTR高13位)。
  ③ 以这个位置索引在GDT中得到LDT段描述符从而得到LDT段基址。
  ④ 用段选择器高13位位置索引值从LDT段中得到段描述符。
  ⑤ 段描述符符包含段的基址、限长、优先级等各种属性,这就得到了段的起始地址(基址),再以基址加上偏移地址才得到最后的线性地址。

段寄存器

  段寄存器是根据内存分段的管理模式而设置的。内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址,x86的段寄存器有6个----CS/DS/ES/SS/FS/GS,均为16位。

寄存器作用
CS(Code Segment)CS段寄存器包含代码段的段选择符,代码段保存正在执行的指令。处理器从代码段读取指令时,使用有CS寄存器中的段选择符与EIP寄存器联合构成的逻辑地址。EIP保存要执行的下一条指令在代码段中的偏移量。CS寄存器不能有应用程序显式地的加载。相反,可以通过某些指令或处理器内部操作隐式地加载。这些指令/内部操作,例如过程调用,中断处理,或者任务切换,用于改变程序的执行流,从而导致更新CS寄存器。
DS(Data Segment)
ES(Extra Segment)
FS/GS
这四个寄存器指向四个数据段。多个数据段的存在允许高效地且安全地访问不同的数据结构类型。例如,可以创建如下的四个数据段:第一个数据段保存当前程序模块的数据结构,第二个数据段保存更高级别程序模块导出的数据,第三个数据段保存动态创建的数据结构,最后一个数据段保存另一个程序共享出来的数据。
要想访问更多的数据段,应用程序必须按需将数据段对应的段选择符加载到 DS/ES/FS/GS 寄存器中的其中一个当中。
SS(Stack Segment)S寄存器包含栈段的段选择符,这里栈段用于存储程序/任务/当前正在执行的处理器程序的栈帧。所有的栈操作都使用SS栈段寄存器来定位栈段。与CS代码段寄存器不同,SS寄存器可以显式地加载,这样就允许应用程序建立多个栈段,并在这些段间切换。

控制寄存器

  X86提供了控制寄存器,来决定CPU的操作模式和当前执行的任务的属性。80386有4个控制寄存器(Control Register):CR0、CR1、CR2、CR3;均为32位,用于控制和确定处理器的操作模式以及当前执行任务的特性,保存全局性和任务无关的机器状态。后来的32位CPU增加了CR4,而x64 又增加了CR8。这几个寄存器是与分页机制密切相关的,因此,在进程管理及虚拟内存管理中会涉及到这几个寄存器。对控制寄存器的读写是通过mov指令来实现。
在这里插入图片描述

CR0

  包含6个预定义标志,分为协处理器控制位和保护控制位两类:

  1. CR0中协处理器控制位:
   CR0的4个位:扩展类型位ET、任务切换位TS、仿真位EM 和数学存在位MP 用于控制80x86浮点(数学)协处理器的操作。CR0的ET位(标志)用于选择与协处理器进行通信所使用的协议,即指明系统中使用的是80387还是80287协处理器。TS、MP和EM位用于确定浮点指令或WAIT指令是否应该产生一个设备不存在(Device Not Available,DNA)异常。这个异常可用来仅为使用浮点运算的任务保存和恢复浮点寄存器。对于没有使用浮点运算的任务,这样做可以加快它们之间的切换操作。
   (1)ET:CR0的位4是扩展类型(Extension Type)标志。当该标志为1时,表示指明系统中有80387协处理器,并使用32位协处理器协议。ET=0指明使用80287协处理器。如果仿真位EM=1,则该位将被忽略。在处理器复位操作时,ET位会被初始化指明系统中使用的协处理器类型。如果系统中有80387,则ET被设置成1,否则若有一个80287或者没有协处理器,则ET被设置成0。
   (2)TS:CR0的位3是任务已切换(Task Switched)标志。该标志用于推迟保存任务切换时的协处理器内容,直到新任务开始实际执行协处理器指令。处理器在每次任务切换时都会设置该标志,并且在执行协处理器指令时测试该标志。如果设置了TS标志并且CR0的EM标志为0,那么在执行任何协处理器指令之前会产生一个设备不存在异常。如果设置了TS标志但没有设置CR0的MP和EM标志,那么在执行协处理器指令WAIT/FWAIT之前不会产生设备不存在异常。如果设置了EM标志,那么TS标志对协处理器指令的执行无影响。在任务切换时,处理器并不自动保存协处理器的上下文,而是会设置TS标志。这个标志会使得处理器在执行新任务指令流的任何时候遇到一条协处理器指令时产生设备不存在异常。设备不存在异常的处理程序可使用CLTS指令清除TS标志,并且保存协处理器的上下文。如果任务从没有使用过协处理器,那么相应协处理器上下文就不用保存。
   (3)EM:CR0的位2是仿真(EMulation)标志。当该位设置时,表示处理器没有内部或外部协处理器,执行协处理器指令时会引起设备不存在异常;当清除时,表示系统有协处理器。设置这个标志可以迫使所有浮点指令使用软件来模拟。
   (4)MP:CR0的位1是监控协处理器(Monitor coProcessor或Math Present)标志。用于控制WAIT/FWAIT指令与TS标志的交互作用。如果MP=1、TS=1,那么执行WAIT指令将产生一个设备不存在异常;如果MP=0,则TS标志不会影响WAIT的执行。

  2. CR0中保护控制位:
   (1)PE:CR0的位0是启用保护(Protection Enable)标志。当设置该位时即开启了保护模式;当复位时即进入实地址模式。这个标志仅开启段级保护,而并没有启用分页机制。若要启用分页机制,那么PE和PG标志都要置位。
   (2)PG:CR0的位31是分页(Paging)标志。当设置该位时即开启了分页机制;当复位时则禁止分页机制,此时所有线性地址等同于物理地址。在开启这个标志之前必须已经或者同时开启PE标志。即若要启用分页机制,那么PE和PG标志都要置位。
   由于只有在保护方式下才可启用分页机制,所以尽管两个位分别为0和1共可以有四种组合,但只有三种组合方式有效。PE=0且PG=1是无效组合,因此,用PG为1且PE为0的值装入CR0寄存器将引起通用保护异常。需要注意的是,PG位的改变将使系统启用或禁用分页机制,因而只有当所执行的程序的代码和至少有一部分数据在线性地址空间和物理地址空间具有相同的地址的情况下,才能改变PG位。

  此外还有:
   CD:Cache 缺失设置位
   NW:直写无效(直写:高速缓存中的数据始终保持与主存储器中数据匹配 )
   AM: 对齐功能屏蔽(与EFLAGS寄存器中 AC标志位一同使用,当两个都为1时开启内存对齐)
   WP:(Write Protection)写保护

CR1

  CR1是未定义的控制寄存器,供今后开发的处理器使用,在80386中不能使用CR1,否则会引起无效指令操作异常。

CR2

  是页故障线性地址寄存器,保存最后一次出现页故障(Page Fault)的全32位线性地址。在报告页异常时,处理器会把引起异常的线性地址存放在CR2中。因此操作系统中的页异常处理程序可以通过检查CR2的内容来确定线性地址空间中哪一个页面引发了异常。

CR3

  含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)。
  CR3用于保存页目录表的起始物理地址。由于目录是页对齐的,所以仅高20位有效,低12 位保留未用。向CR3中装入一个新值时,低12位必须为0;但从CR3中取值时,低12位被忽略。每当用MOV指令重置CR3的值时,会导致分页机制高速缓冲区的内容无效,用此方法,可以在启用分页机制之前,即把PG位置1之前,预先刷新分页机制的高速缓存。
  CR3寄存器即使在CR0寄存器的PG位或PE位为0时也可装入,如在实模式下也可设置CR3,以便进行分页机制的初始化。在任务切换时,CR3要被改变,但是如果新任务中CR3的值与原任务中CR3的值相同,那么处理器不刷新分页高速缓存,以便当任务共享也表时有较快的执行速度。
  此外还有两个控制位:

  • PCD:控制当前页目录表的缓冲,当设置清空时,缓冲置位时,缓冲无效,与CR0中的CD或PG一同使用
  • PWT:控制cache采取直写还是回写的策略,当设置清空时,回写有效,当置位时,直写有效
CR4

  包含处理器扩展功能的标志位,是一些结构的扩展,表明对于特定的处理器和操作系统执行支持。
  VME:虚地址模式.当清空时,无效.
  PVI:保护模式虚中断,当清空时,无效
  TSD:时间戳允许标志位.当清空时,允许RDTSC指令执行在任务特权级上.当置位时,只允许工作在特权级0.
  DE:调试扩展.置位,表明DR4与DR5将产生没有定义的异常
  PSE:当置位,使用4M的页面;清空,使用4K的页面
  PAE:置位,使用36物理内存的分页机制.清空时,使用32位分页机制
  MCE:置位,使用机器检查异常机制.
  PGE:置位,启动全局页面.当写CR3时,也不会被替换.
  PCE:置位,表明使RDPMC指令工作在任何保护级别.
  OSFXSR:置位.表明操作系统支持FXSAVE and FXRSTOR指令
  OSXMMEXCPT:置位.表明操作系统支持不可屏蔽的SIMD浮点异常.

调试寄存器

  调试寄存器主要作用是调试应用代码、系统代码、开发多任务操作系统.来监视代码的运行和处理器的性能,在x86架构CPU内部,提供了8个调试寄存器DR0~DR7:
在这里插入图片描述

寄存器作用
DR0~DR3这是四个用于存储地址的寄存器
DR4~DR5这两个有点特殊,受前面提到的CR4寄存器中的标志位DE位控制,如果CR4的DE位是1,则DR4、DR5是不可访问的,访问将触发异常。如果CR4的DE位是0,则DR4和DR5将会变成DR6和DR7的别名,相当于做了一个软链接。这样做是为了将DR4、DR5保留,以便将来扩展调试功能时使用。
DR6这个寄存器中存储了硬件断点触发后的一些状态信息:
  B0—B3:断点状态的监测;
  BD:调试寄存器访问监测、置位,表明在指令流中,下一条指令将访问其中的一个调试寄存器;
  BS:单步执行标志位;
  BT:任务转换标志位。
DR7调试控制寄存器,这里面记录了对DR0-DR3这四个寄存器中存储地址的中断方式(是对地址的读,还是写,还是执行)、数据长度(1/2/4 个字节)以及作用范围等信息:
  L0—L3:局部断点使能标志位;
  G0–G3:全局断点使能标志位;
  LE AND GE:置位,表明处理器可以监测导致数据断点的指令,推荐置位为1;
  GD:通用监测使能标志位,表明是否开启调试寄存器保护;
  LEN0 through LEN3:用来表明相应断点地址寄存器内存位置的大小;
  R/W0 through R/W3:相应断点的状态。

  通过调试器的接口设置硬件断点后,CPU在执行代码的过程中,如果满足条件,将自动进行中断。

其他寄存器

EIP

在这里插入图片描述
  在 32 位模式下,为了生成 32 位物理地址,处理器需要使用 32 位的指令指针寄存器。为此,32 位处理器扩展了 IP,使之达到 32 位,即 EIP。当它工作在 16 位模式下时,依然使用 16 位的 IP;工作在 32 位模式下时,使用的是全部的 32 位 EIP。 当一个程序开始运行时,系统把EIP清零,每取入一条指令,EPI自动增加取入CPU的字节数目。
  EIP主要用于存放当前代码段即将被执行的下一条指令的偏移,但其本质上并不能直接被指令直接访问,只由处理器内部使用。对 IP 和 EIP 的修改通常是用某些指令隐式进行的(控制转移指令、中断及异常),这此指令包括 JMP、CALL、RET 和 IRET 等等。
  读操作通过执行call 指令并取得栈中所存放的地址来实现,而写操作则通过修改程序栈中的返回指令指针并执行 RET/IRET 指令来完成,因此尽管这个寄存器相当重要,但其实并不是操作系统在实现过程中所需关注的焦点。

TSC

  (时间戳寄存器)每个时钟周期时其值加1,重启时清零。通过RDTSC指令读取TSC寄存器,只有当CR4寄存器的TSD位为0时,才可以在任何优先级下执行该指令,否则只能在特权级下执行该指令。

浮点寄存器

  由于在80486微处理器内部设有浮点运算器,因此在其内部有相应的寄存器,其中包括8个80位通用数据寄存器、1个48位指令指针寄存器、1个48位数据指针寄存器、1个16位控制字寄存器、1个16位状态字寄存器和1个16位标记字寄存器。

MSR

  从80486之后的x86架构CPU,内部增加了一组新的寄存器,统称为MSR(Model Specific Register)寄存器,中文直译是模型特定寄存器,意思是这些寄存器不像上面列出的寄存器是固定的,这些寄存器可能随着不同的版本有所变化。这些寄存器主要用来支持一些新的功能。
  随着 x86 CPU不断更新换代,MSR寄存器变的越来越多,但与此同时,有一部分MSR寄存器随着版本迭代,慢慢固化下来,成为了变化中那部分不变的,这部分MSR寄存器,Intel将其称为Architected MSR,这部分MSR寄存器,在命名上,统一加上了IA32的前缀。
  这里选取三个代表性的MSR简单介绍一下:
   • IA32_SYSENTER_CS
   • IA32_SYSENTER_ESP
   • IA32_SYSENTER_EIP
  这三个MSR寄存器是用来实现快速系统调用。在早期的x86架构CPU上,系统调用依赖于软中断实现,类似于前面调试用到的int 3指令,在Windows上,系统调用用到的是int 2e,在Linux上,用的是int 80。
  软中断毕竟还是比较慢的,因为执行软中断就需要内存查表,通过IDTR定位到IDT,再取出函数进行执行。系统调用是一个频繁触发的动作,如此这般势必对性能有所影响。在进入奔腾时代后,就加上了上面的三个MSR寄存器,分别存储了执行系统调用后,内核系统调用入口函数所需要的 段寄存器、堆栈栈顶、函数地址,不再需要内存查表。快速系统调用还提供了专门的CPU指令 sysenter/sysexit 用来发起系统调用和退出系统调用,在64位上,这一对指令升级为 syscall/sysret。

参考资料

  《x86汇编语言 从实模式到保护模式》
  https://blog.csdn.net/weixin_40913261/article/details/90762210
  https://blog.csdn.net/weixin_42109012/article/details/100148721
  https://blog.csdn.net/sky1679/article/details/89785382
  https://blog.csdn.net/song_lee/article/details/105297902
  https://www.cnblogs.com/wanghetao/archive/2011/10/28/2228130.html
  https://zhuanlan.zhihu.com/p/272135463
  https://www.cnblogs.com/nullecho/p/10266467.html
  https://www.cnblogs.com/kukudi/p/11416993.html

  • 6
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据您提供的Makefile,这个错误信息是由于在删除目标文件时出现问题引起的。这可能是由于您的Dev-C++环境配置不正确导致的。 在您的Makefile中,您使用了`$(DEL)`变量来执行删除操作。然而,在Windows环境下,删除操作通常使用`del`命令而不是`devcpp.exe INTERNAL_DEL`。 为了解决这个问题,您可以尝试将以下行: ``` DEL = C:\Program Files (x86)\Embarcadero\Dev-Cpp\devcpp.exe INTERNAL_DEL ``` 替换为: ``` DEL = del ``` 这将使用Windows的`del`命令来执行删除操作。 修改后的Makefile如下所示: ```makefile # Project: 项目1 # Makefile created by Embarcadero Dev-C++ 6.3 CPP = g++.exe CC = gcc.exe WINDRES = windres.exe OBJ = main.o LINKOBJ = main.o LIBS = -L"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib" -L"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/lib" -static-libgcc INCS = -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" CXXINCS = -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/x86_64-w64-mingw32/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include" -I"C:/Program Files (x86)/Embarcadero/Dev-Cpp/TDM-GCC-64/lib/gcc/x86_64-w64-mingw32/9.2.0/include/c++" BIN = 项目1.exe CXXFLAGS = $(CXXINCS) -std=c++11 CFLAGS = $(INCS) -std=c++11 DEL = del .PHONY: all all-before all-after clean clean-custom all: all-before $(BIN) all-after clean: clean-custom ${DEL} $(OBJ) $(BIN) $(BIN): $(OBJ) $(CPP) $(LINKOBJ) -o $(BIN) $(LIBS) main.o: main.cpp $(CPP) -c main.cpp -o main.o $(CXXFLAGS) ``` 请尝试使用修改后的Makefile重新编译您的项目,看看是否能够解决问题。如果还有其他错误信息,请提供详细的错误信息,以便我更好地帮助您解决问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值