x86指令集架构
以下笔记来自阅读:x86 Instruction Set Architecture - MindShare
本文以Intel处理器作为阐述目标。AMD处理器差异点可以参考: Agner Fog Microarchitecture
在“指令集架构 "一文中,我们粗略介绍了指令集架构的分类、寻址、操作、编码等7个方面的内容。下面我们对服务器及PC中使用最广泛的x86指令集架构做详细介绍。
基本概念
两种x86架构
x86指令集架构实际上包含两种架构:
IA32 架构 | 不能执行64位代码 |
Intel 64 架构 | 可以执行64位代码、其是 IA32 架构的超集 |
其对应的处理器也分为两类:
IA32 处理器 | 只实现了IA-32 ISA,支持执行16位或者32位指令 |
Intel 64 处理器 | 实现了Intel 64 ISA: 若执行于 IA-32 模式,支持执行16位或者32位代码; 若执行于 IA-32e 模式,支持执行16位、32位和64位代码 |
逻辑处理器
物理处理器、核心、逻辑处理器的关系见下图:
IA 指令和微操作
x86
ISA是复杂指令集(CISC)架构,其指令集复杂、指令长度可变(1到18个字节)。而精简指令集(RISC)架构则使用定长、简单的指令集,并具有如下的优势:
- 简化了处理器的指令取出逻辑
- 简化了指令流的解析逻辑
- 简化了指令解码器的设计
- 简化了指令分发相关的逻辑
- 标准化了处理器执行单元间的内部总线宽度
为了在保证兼容性的情况下尽可能的提高处理器速度,Die 内的指令集翻译被引入到了处理器中。x86指令首先从内存读取到处理器缓存中,之后一组译码器将每个x86指令翻译成一组简单的、定长指令(微操作)。之后这些微操作执行后将完成原来x86指令同样的功能。Intel在1995发布的 Pentium Pro 首次使用了指令集翻译,可以说,从 Pentium Pro 之后,所有的Intel处理器实际上是RISC机器。
特权级
当处于IA-32
保护模式、或者IA-32e的的兼容或64比特模式下时,x86处理器支持特权级。每时每刻、处理器都在从内存区域中获取执行代码,这些内存区域的特性由一个特殊的描述符定义(代码段描述符)。处理器根据代码段描述符的2比特
DPL(Descriptor Privilege
Level)字段确定当前执行程序的特权级(CPL Current Privilege
Level )。共有4个特权级:
- 0:
最高特权级。一般只有操作系统内核运行于特权级0,并可以执行任何操作。
- 1:
一般由高特权级的设备驱动和操作系统服务使用。调试程序也可以使用该特权级避免低特权级设备驱动和程序的干扰。
- 2: 一般由低特权级设备驱动使用。
- 3:
是最低的特权级,一般由应用程序使用,这阻止了应用程序对操作系统、驱动等的破坏性操作。
模式和模式切换
x86各子模式和模式切换参考下图:
模式 | 子模式 | 说明 |
---|---|---|
IA-32 模式 | 实模式 | - 逻辑处理器在硬重置之后将总是从实模式开始执行。 - 在实模式,逻辑处理器会模拟 Intel 8088/8086 处理器的行为。 - 内存地址划分为64KB大小的段: CS:代码段包含需要执行的代码。 SS:栈段定义了可以临时保存或恢复寄存器信息的内存区域 DS:数据段定义了当前执行程序操作的默认数据内存区域 ES,FS和GS:可用来定义当前执行程序操作的额外数据内存区域 - 没有机制可以阻止对特定区域内存的未授权访问。 - 当前执行程序可访问任何I/O端口、因此可以不通过操作系统或者其他程序直接更改I/O设备状态。 - 没有特权级的概念。对当前执行程序的指令和寄存器使用、包括内存访问区域没有任何限制。 - 软件被限制只能访问前1MB内存空间。 |
- | 保护模式 | 保护模式实现了多任务处理,在特定时间内允许逻辑处理器执行操作系统调度的任务、而其他任务则需要暂停。保护模式提供了解决多任务环境问题的诸多方案: - 全面的内存保护 - 允许操作系统内核优雅访问I/O端口。 - 阻止对操作系统服务和资源的未授权访问。 - 拦截未授权的开/关外部中断处理操作。 - 拦截未授权的通过软中断对BIOS和MSDOS服务的访问。 为了很好的处理上述的问题,x86架构引入了新的功能: - 操作系统调度器为程序赋予特权级的能力,以此来限制程序对逻辑处理器功能的使用。 - 支持16位、286、以及32位代码。 - 增加了段描述符结构,允许操作系统内存管理为内存段指定如下描述信息: 32位基地址。 32位段长度。 为访问该段,需要的最低特权级。 读写访问权限。 - 基于硬件的任务切换机制 - 虚拟地址到物理地址转换服务。 - 中断描述符 |
- | 虚拟8086模式(VM86) | 由于8086代码在保护模式下可能引起破坏性行为,操作系统任务调度首先切换逻辑处理器至虚拟8080模式,再初始化或者恢复实模式程序的运行。当处于虚拟8086模式时,逻辑处理器将激活执行如下指令的硬件: - 它暂停违法程序的运行。 - 调用特定的操作系统程序,虚拟机监视器(VMM)。 - VMM检查违法指令并执行如下操作: 允许指令运行。 以软件的形式模拟指令,防止对整体软件环境的方式进行破坏。 禁止操作并关闭违法任务。 当在虚拟8086模式运行时,逻辑处理器仿佛运行在实模式下。(但是打开了如上的拦截逻辑) |
- | 系统管理模式(SMM) | SMM用来处理特定设计过的系统事件(比如平台相关的电源或者温度管理事件)。逻辑处理器当收到系统管理中断(SMI)时,进入SMM模式。当收到SMI,逻辑处理器: - 自动保存当前的机器状态(比如逻辑处理器的整组寄存器状态)。 - 进入SMM。 - 执行事件处理程序。 - 恢复寄存器组到之前的状态。 - 恢复被中断程序的执行。 |
IA-33e 模式 | 64位模式 | 假设逻辑处理器: - 正运行于IA-32e模式, - 正在从16位或者32位代码段获取/执行代码,因此处于IA32-e的兼容子模式。 - 一条指令导致了向另一个代码段的跳转,该代码段的描述符设置了L位(长模式比特位;长模式是AMD对IA-33e模式的命名)。 - 因此,逻辑处理器自动从兼容模式切换至64比特模式。 逻辑处理器保持在64位模式,直到: - 一个SMI导致临时切换至SMM模式以处理平台事件。 - 一条指令导致了向另一个代码段的跳转,该代码段的段描述符L位清零。因此,逻辑处理器自动从64位模式切换到了兼容模式。 当处于64位模式时,以下描述都是正确的: - 逻辑处理器支持相当大的逻辑和物理地址空间: ·理论上支持64位虚拟地址空间。当前Inel处理器支持48位虚拟地址空间。 ·理论上支持52位物理地址空间。目前Intel处理器实现了40位物理空间,部分AMD处理器实现了41或者48位物理地址空间。 - 允许软件使用更多高位宽寄存器: ·通用寄存器从32位扩展至64位。 ·控制和调试寄存器从32位扩展至64位。 ·8个额外的64位通用寄存器(R8-R15)。 ·8个额外的64位控制寄存器(CR8-CR15)。 ·8个额外的调试寄存器(DR8-DR15)。 ·8个额外的128位 SIMD寄存器(XMM8-XMM15)。 - 关闭当前操作系统不使用的老旧机制(分段、基于硬件的任务切换辅助逻辑等)。 - 硬件增强的平台内存模式。 |
- | 兼容模式 | 子模式:兼容模式包含两个子模式: - 16位兼容子模式。当逻辑处理器运行位于286兼容的16位代码段的代码时,其运行于16位兼容子模式。 - 32位兼容子模式。当逻辑处理器运行位于32位代码段的代码时,其运行于32位兼容子模式。 进入的时机:逻辑处理器在如下场景下进入IA-32e兼容模式: - 初始时进入。由保护模式切换至IA-32e模式时进入。 - 任务切换时。逻辑处理器正处于64位模式,一条指令触发了向另一代码段位置的跳转,且该代码段清除了段描述符的L位。 与保护模式有差异:当位于兼容模式时,逻辑处理仿佛处于保护模式,但是有如下的差异: - 不支持执行VM86任务。 - 虽然限制了32位虚拟地址空间,但是并没有对32位物理空间的限制。 - 中断和异常时间将导致逻辑处理器切换至64位模式(处理程序必须位于64位代码段)。当中断处理程序处理完,逻辑处理器返回兼容模式恢复执行被中断的任务。 - 控制寄存器是64位宽。 - GDT中TSS描述符是16字节,而不是8字节。 - GDT中的LDT描述符是16字节而不是8字节。 - 调用门(Call Gate)描述符在GDT和LDT中都是16字节,而不是8字节。 |
模式切换:IA-32 到 IA-32e
想要进入IA-32e模式,只能先从旧的保护模式切换到IA-32e兼容模式。切换通过如下的步骤完成:
1a | 如果在保护模式开启了分页,在准备切换到IA-32e之前,需要关闭。 |
1b | IA-32e 操作系统内核使用的数据结构和保护模式下使用的有差别,软件需要在内存中创建对应的数据结构,这包含IA-32e使用的地址转换表。 |
2a | 设置CR4[PAE]为1(IA-32e使用一种重新设计的PAE模式用于地址转换) |
2b | 置位EPER寄存器中的LME(Long Mode Enable)比特位(AMD称IA-32e为长模式,这里Intel使用了和AM同样的命名)。需要注意的是,虽然LME = 1,此时还没有进入IA-32e模式 |
2c | 将CR3指向IA-32e模式地址转换表的最高层(页表中的PML4)。以为CR3在保护模式只有32位,因此开始的PML4物理地址需要在4GB以下 |
3 | 设置CR0[PG] = 1,开启分页,同时也开启了IA-32e模式。逻辑处理器,此时进入IA-32e兼容子模式,并设置EFER[LMA] = 1(Long Mode Active),表明目前IA-32e模式已经开启。 |
4 | 软件现在将逻辑处理器的寄存器指向1b中创建的内核数据结构: - GDTR:加载全局描述符表的基地址和长度到GDTR。 - LDRT:加载局部描述符表的基地址和长度到LDTR。 - IDTR:加载中断描述符表的基地址和长度到IDTR。 - TR:加载内核TSS(Task State Segment)结构的地址和长度到TR |
一个简化的转化图如下:
IA32e子模式选取
当逻辑处理器处于保护模式或者IA-32e模式,CS
寄存器的内容定义了当前程序的:
- 代码段的内存基地址。
- 代码段的大小。
- 代码段的特权级。
- 代码段的类型:
16位,286代码段。
32位,386代码段。
64位代码段。
- 其他代码段特性:
仅包含代码或者包含代码和只读数据。
是否为 conforming 代码段(参考段级别保护机制)
当逻辑处理器在IA-32e模式下,不同类型的代码段定义了当前逻辑处理器处于哪个子模式。CS
寄存器的 L 和 D 比特位决定了当前的代码段类型:
- L=0,
D=0:逻辑处理器正在从16位,286代码段执行代码,因此处于IA-32e的16位兼容子模式。
- L=0,
D=1:逻辑处理器正在从32位,386代码段执行代码,因此处于IA-32e的32位兼容子模式。
- L=1,
D=0:逻辑处理器正在从64位代码段执行代码,因此处于IA-32e的64位子模式。
- L=1, D=1:保留。
当切换到IA-32e模式的时刻,逻辑处理器处于兼容子模式。 需要注意的是,此时逻辑处理器仍在从保护模式代码段获取代码,因此,其正运行在IA-32e的16位或者32位兼容子模式下。
重置之后的状态
IA-32 寄存器如下图所示:
硬重置
下表列出了硬重启之后的寄存器和资源状态:
寄存器/资源 | 影响 |
---|---|
指令流水线 | 目前没有从内存中读取的指令 |
ROB(Reorder Buffer) | 由于并没有从内存中读取指令,ROB为空,且指令分发逻辑也处于空闲 |
BTB(Branch Target Buffer) Cache | BTB记录u了分支执行的历史,当前为空 |
CR0 寄存器 | 重置后内容为
60000010h: - CR0[PE]=0,关闭保护模式。 - CR0[PG]=0,关闭分页。 - CR0[CD&NW]=11b,关闭缓存逻辑。 - CR0[EM&MP]=11b,表明当前不存在x87 FPU。 - CR0[TS]=0,表明没有发生任务切换。 - CR0[ET]=1,表明内置x87 FPU与387 FPU兼容。 - CR0[NE]=0,FP异常发生时,逻辑处理器不再抛出 16 异常。逻辑处理器转而使用DOS兼容的方式触发信号。 - CR0[WP]=0,目前由于关闭了分页,该配置无效。当分页开启时,CR0[WP]=1会阻止 0 特权级软件写入只读内存页。 - CR0[AM]=0,逻辑处理器当检测到对多个字节的不对齐访问时,不会产生对齐检查异常。 |
CR4 寄存器 | 新特性的控制寄存器,重置后内容为 00000000h :
- VME:禁用VM86模式。 - PVI:禁用保护模式下的虚拟中断特性。 - TSD:不限制RDTSC调用方的特权级。 - DE:禁用调试扩展。 - PSE:禁用内存页大小扩展。 - PAE:禁用物理地址扩展。 - MCE:禁用机器检查扩展。 - PGE:禁用全局页功能。 - PCE:禁用性能计数器。 - VMXE:禁用虚拟化功能。 - SMXE:禁用 Safer Mode。 |
Eflags 寄存器 | 重置后,内容为
00000002h: - Eflags[TF]=0,关闭单步调试模式。 - Eflags[IF]=0,关闭对外部中断的响应。 - Eflags[DF]=0,字节串指令从开始地址,以递增方式访问。 - Eflags[VM]=0,禁用VM86模式。 - Eflags[ID]=0,没有任何对外部的影响。 - Eflags剩下的比特位是状态比特位,反映当前指令执行状态 |
MTRR 寄存器组 | MTRRdefType
寄存器内容为0: - 所有内存均设置为不可缓存(UC) - 禁用固定区域的MTRR寄存器。 - 禁用可变区域的MTRR寄存器。 |
缓存(Cache) | 所有逻辑处理器的缓存都为空。另外缓存被禁用,所有的内存也标记为不可缓存。 |
CS和EIP寄存器 | CS=F000h:因此,代码段在地址 000F000h
开始。实际上它开始于
FFFFFF0000h。CS寄存器的不可见部分(CS缓存寄存器,参考:Program-Invisible
Registers)则加载了如下的代码段描述信息: - 段基地址为 FFFF0000h 。 - 段大小为0FFFFh (64KB) - P=1,段在内存中存在。 - RW=1,可读写段。 - A=1,段被访问过。 EIP=0000FFF0h。第一个指令指向代码段 0000FFF0h 的位置,代码段位于 FFFF0000h(即指向 FFFFFFF0h,4GB - 16) |
DS、ES、FS和GS寄存器 | 所有的数据段寄存器的内容都为
0000h。数据段寄存器的不可见部分有如下的描述: - 段基地址为 00000000h 。 - 段大小为0FFFFh (64KB) - P=1,段在内存中存在。 - RW=1,可读写段。 - A=1,段被访问过。 |
SS和ESP寄存器 | SS=0000h。SS的不可见部分,对栈段有如下描述: - 段基地址为 00000000h 。 - 段大小为0FFFFh (64KB) - P=1,段在内存中存在。 - RW=1,可读写段。 - A=1,段被访问过。 - 这是一个向上生长的栈段。 ESP=00000000h。 |
EBX,ECX,EBP,ESI,EDI | 这些通用寄存器内容都为 00000000h |
EAX | - 如果 BIST(内置自测)没有运行或运行后没有错误,EAX内容为
00000000h。 - 如果 BIST 运行遇到错误,EAX的内容将是一个非零的处理器相关的错误码。 |
EDX | 在重置触发后,代码获取之前,逻辑处理器自动的将处理器的 type, family, model, stepping 信息储存到EDX中。 |
IDTR | IDTR=0000h, 中断表有如下特性: - 段基地址为 00000000h 。 - 段大小为0FFFFh (64KB) - P=1,段在内存中存在。 - RW=1,可读写段。 实模式中断表在物理地址 00000000h |
GDTR | 48位的全局描述符寄存器在重置之后: - 基地址为 00000000h - 大小为FFFFh(64KB) 由于GDTR和GDT只能在保护模式使用,因此当前阶段没有作用 |
LDTR | 16位的局部描述符寄存器在重置之后: - 基地址为 00000000h - 大小为FFFFh(64KB) 由于LDTR和LDT只能在保护模式使用,因此当前阶段没有作用 |
TR寄存器 | 16位的任务寄存器在重置之后: - 基地址为 00000000h - 大小为FFFFh(64KB) 由于任务寄存器只能在保护模式使用,因此当前阶段没有作用 |
CR2和CR3寄存器 | CR2(缺页地址寄存器)和CR3(页目录基地址寄存器)的内容都为00000000h。由于CR2和CR3都只在保护模式使用,因此当前阶段没有作用 |
TLBs | 重置后所有TLB项均标记为失效。由于TLB只在保护模式下并开启分页的场景有用,当前阶段没有作用 |
XCR0(XFEM)寄存器 | 只有比特1也就是SSE比特位当前实现了,该比特位置为0 |
调试寄存器 | DR7(调试控制寄存器)内容为 00000400h,关闭逻辑处理器的断点逻辑。因此DR0-DR6 不用关心。 |
LAPIC寄存器 | 在BIST运行之后(如果BIST执行),高级可编程中断控制器就被赋予了唯一的APIC ID。该控制器随后和其他逻辑处理器的LAPIC进行协商,最后选择负责引导(bootstrap)的逻辑处理器。所有对外部中断的响应都会关闭,当前LAPIC处于虚拟线模式(Virtual Wire or 8259A mode)。 |
SSE 寄存器组 | XMM0-7 数据寄存器都为0。MXCSR 寄存器的内容为
1F80h: - 禁用 Flush-to-Zero 模式。 - 禁用 Denormals As Zero 模式。 - 取整控制标志位 =00b,取整到最近的偶数。 - 禁用所有的SSE异常。 - 清楚所有的SSE错误状态位。 |
x87 寄存器组 | - 控制寄存器的内容是 0040h: 禁用所有的x87 FPU异常。 取整到最近的偶数。 精度为单精度。 - 状态寄存器是 0000h。 清空所有的状态位。 栈顶指针指向数据寄存器0。 - 数据寄存器都为0。 - 数据操作数段:偏移寄存器=0:0。 - 指令段:偏移寄存器=0:0 |
MCA 寄存器组 | 没有记录的硬件错误 |
性能监控寄存器组 | 所有逻辑寄存器的性能监控计数器都已经关闭,因此没有任何类型的事件可以记录 |
温度监控 | 逻辑处理器的温度监控和控制功能都已经关闭 |
调试控制寄存器 | 调试功能都已关闭 |
VMX能力 | CR4[VMXe]=0,关闭了逻辑处理器的虚拟化相关功能 |
TSC | 64位 TSC MSR寄存器置为0, 在重置之后,寄存器开始记录逻辑处理器的时钟周期 |
总结来说:
- 逻辑处理器处于实模式。
- 缓存已经清空,并且缓存已经禁止。
- 所有的CR4个功能标志位都已清空,禁用386之后的大部分新功能。 -
关闭对外部中断的响应。 - 没有从内存中获取的指令。
- x87 FPU已禁用。
- 所有的x87 FPU和SSE异常都已经禁用。
- 机器检查(Machine Check)和对齐检查(Alignment Check)异常都已经禁用。
- 第一条指令会从FFFFFFF0h获取并执行。
软重置
正如上文的描述,一次硬重置将完全重新初始化逻辑处理器和其对应的LAPIC并清理它的缓存。而一次软重置(也称为 INIT),有同样的效果,但是软重置将保留缓存、MSR、MTRR和x87 FPU状态等内容(BTB和TLB的内容会被清空)。
软重置通过如下两种方式发送到逻辑处理器:
- 可以命令芯片组为处理器设置 INIT# 信号。
- 逻辑处理器上运行的软件(通常是内核代码)可以命令LAPIC发送 INIT
IPI 到一个或者多个逻辑处理器的LAPIC。
选择引导CPU(BSP,Bootstrap CPU)
在重置发起之后,启动ROM中的代码被获取之前。所有系统中的逻辑处理器必须协商并决定谁来充当引导处理器(BSP)。BSP负责发现并配置系统拓扑,也负责启动操作系统。除BSP外的其他逻辑处理器称为应用处理器(AP,
Application
Processor),AP处于休眠状态,直到BSP向其发送了启动中断消息(SIPI,
Startup Interrupt
Message)。BSP的选取方式是和处理器设计强相关的:
- 基于前端总线的系统。其选取流程如下图所示:
- 基于Intel
QPI的系统。处理器之间使用QPI通信的系统使用不同的BSP选取方式。在重置开始之后,从引导ROM读取初始代码之前,每个物理处理器需要执行内部微码,用以:
1) 配置内部内存控制器和事务路由设备,确定到芯片组、引导ROM、以及系统其他部分的路径。
2) 确定可用的逻辑处理器数目。配置QPI路径以便逻辑处理器间互相通信。
3) 选取需要充当BSP的逻辑处理器。
AP的发现和配置
除了BSP外的其他逻辑处理器叫做应用处理器(AP)。当BSP从引导ROM获取代码进行系统拓扑发现和配置、引导操作系统的时候,应用处理器保持在休眠和低功耗状态。BSP的任务之一就是发现并初步配置AP。引导ROM通过如下过程完成这一任务:引导ROM将配置程序加载到内存,通过LAPIC向系统所有AP的LAPIC发送一个包含该程序内存地址的特殊消息。AP接收到这条消息(SIPI,Startup Inter-Processor Interrupt)后退出休眠状态,开始执行引导ROM放到内存里的配置程序。
初始阶段的内存读取
x86
处理器总是从实模式开始执行。在实模式下,内存地址由16位段寄存器构成的20位段基地址(段寄存器左移4位)和16位偏移相加构成。偏移包含:
- 16位IP(指令指针)寄存器,用于构成代码的内存地址。
- 16位SP(栈指针)寄存器,用于构成需要读写的栈内存地址。
- 为了数据访问构建的访存指令。
由于偏移是16位,所有段的大小就被限制在了64KB。
当重置触发后,CS寄存器的内容为 F000h、EIP寄存器包含的偏移地址为 0000FFF0h。自然而然的,第一个执行的指令地址应该位于 0000FFFF0h。然后在重置上电之后,逻辑处理器第一次访存构建内存地址的方式和一般的实模式访问不同。段基地址为 FFFF0000h ,而不是 F0000h。当EIP偏移为 00000FF0h时,加到段基地址,得到的地址是 FFFFFFF0h。逻辑处理器在读取初始指令时使用该地址。内存指令的地址持续使用这种构建方式,直到程序加载任何值到CS段寄存器(即使加载的是 F000h,也就是其目前的值)。也就是说,直到执行一个 far jump (或者 far call)。一般,在power-on-restart地址(FFFFFFF0h)上发现的第一条指令是到ROM上 Power-On Self-Test(POST)程序的far jump。从这个时刻开始直到进入保护模式,地址将按照常规的方式构建,因此也限制在前1MB的内存空间。