INTEL指令集手册笔记-x86体系结构概览
写在最开始
本文是基于IA-32架构软件开发者手册(第三卷)的阅读笔记。作为软件开发者,这或许是我们所必须掌握的知识中最接近底层的一部分。在这里,我们将会看到计算机是如何工作的,以及我们的程序是如何被计算机执行的。
这些内容十分的抽象、晦涩、冗杂,包含了大量的细节和术语。因此,我将会尽量使用简单的语言来描述这些内容。
大多数时候,我使用无序列表来展示一些重要的概念,以期尽量精简的展示系统的结构。在开头和结尾,总是会有概括性的文字对于这部分内容进行概述和总结。对于特别重要或特别复杂的部分,我才会使用成段的文字展开讨论。我从不罗列内容,例如EFLAGS每个位置的含义,而只用文字解释图片中没有的信息。不过,我仍然会列出重要的几点用于强化记忆。
为了更好的理解这部分知识,我们首先需要对一些关键的术语(主要是中文术语)进行辨析,理清这些内容,对于我们理解之后的内容有很大帮助。下面的内容假设读者具有一定的汇编和体系结构方面的知识(比如学习过CSAPP)。
关于“模式”
在Intel处理器中,存在许多的“模式”。我们将会看到,这些模式对应不同的寻址方式和内存布局。文中出现的模式大概有以下几种:
实模式:最早的模式,也是(目前)所有处理器启动时的默认模式。这种情况下,表示为
CS:IP
的地址值为CS<<4 + IP
。至多管理1MB的内存。这是8086 16位体系结构的无奈之举,用这种方式配合20位地址总线,编程空间得以从令人难以忍受的64KB扩展到1MB。
IA32(保护模式):从80386开始启用的模式,这种情况下,可以用平坦分段管理至多4GB的内存
IA32e(长模式):支持64位的体系结构。提供至多48位的地址空间,并提供兼容32位OS和软件的“兼容模式”。不再使用段寄存器,段选择子固定为GDT,几乎完全采用页式地址管理。
值得注意的是,IA32e与IA64并不相同。前者是我们熟知的x86-64(有时也称为x64)体系结构的一部分,是目前主流的64位体系结构。而IA64则是一种激进的不兼容IA32的64位架构,由Intel与HP合作开发,目前已经被抛弃。
关于“地址空间”
我们知道,在计算机界为人津津乐道的(八股文)术语之一就是地址空间,逻辑地址、线性地址、虚拟地址、物理地址成为无数求职者和学子的噩梦。
物理地址(Physical Address,PA):这个术语常常表示数据在物理内存条中的位置。在现代系统中,物理地址是由MMU(内存管理单元)通过页表变换得到的。本文中,物理地址并不会频繁的出现,然而,页表基址寄存器(CR3)存储的就是页表的物理地址基址。否则,默认情况下的“地址基址”往往是线性地址。
线性地址(linear address):这个术语通常用来描述理想状态下的连续地址空间。线性地址就是经过段式变换后得到的地址,也就是CPU实际使用的地址。
维基百科:线性地址
逻辑地址(logical address):这个术语通常用来描述“分段模式下”的
段基址:偏移量
形式的地址。不过,它的本意实际上是“编程时使用的地址”。从这个角度来说,逻辑地址其实等同于线性地址。维基百科:地址空间
虚拟地址(Virtual Address,VA):这个术语常常表示进程所看到的内存空间。由于保护模式和长模式的现代系统总是使用页表基址提供给进程一个完整的连续地址空间,虚拟地址也就等同于线性地址。
对于实模式而言,很难谈论虚拟地址,有人认为虚拟地址等于段偏移量,有人认为虚拟地址等于逻辑地址,但这其实没有意义。因为实(地址)模式不存在什么虚拟的地址
MSDB: 虚拟地址
综上所述,如果面试的时候被问到,或许可以回答“逻辑地址经过段式变换得到线性地址,如果是启动分页的保护模式或长模式,那么线性地址就是虚拟地址,虚拟地址经过页式变换得到物理地址;如果是实模式,那么没有虚拟地址,线性地址就是物理地址”。
体系结构概览
本章中,我们将关注系统的寄存器结构以及操作这些寄存器的系统指令。理解寻址、内存、中断处理、任务管理的机制以及其中的重要数据结构。并且看到计算机是如何从实模式切换到保护模式的。
系统级寄存器和数据结构
下图展示了保护模式和长模式下的系统寄存器和数据结构。其中:
- 左上角的部分展示了寄存器,包括标志寄存器、控制寄存器、任务寄存器和其他通用寄存器
- TODO:
- 底部展示了页表将线性地址映射到物理地址的方式


左:保护模式(IA32)下的系统寄存器和数据结构
右:长模式(IA32e)下的系统寄存器和数据结构
1. 全局描述符表(GDT)和局部描述符表(LDT)
在保护模式下,所有访存都是通过描述符表——GDT和LDT完成的。
描述符表(Descriptor Table,DT):用于内存、中断、任务管理的重要数据结构。
段描述符(Segment descriptors):描述符表的表项。定义了段的起始地址、访问权限(读写执行)和使用信息。
段选择子(Segment Selector):用来在GDT或LDT中定位段描述符。包含相对于DT基址的偏移量,一个全局/局部标志位,以及特权级。
描述附表寄存器(Descriptor Table Register,DTR):用来存储DT的线性地址基址。
当前特权级(Current privilege level,CPL):当前正在执行的代码所在的特权级。通常,特权级别从0~3,0表示内核态(完全权限),3表示用户态(最低权限)。
通过段选择子查询DT可以访问代码、数据、栈等段,其中的权限标志会阻止不合法的访问。
描述符机制是段式地址管理在现代系统中的实现,这种机制方便了内存的管理,并且提供了访问权限的控制。实模式下,总是直接访问物理地址,而保护模式下则至少要经过描述符的转换。从这个角度上来说,“实”指的是直接访问物理地址,“保护”指的是通过描述符表进行访问权限的保护。
长模式下描述符扩展为16字节,兼容模式下不进行这种扩展
2.系统段,段描述符和门
系统段:TSS(Task-state Segment)和LDT,称为系统段。其余的则是运行时环境包含的代码、数据、栈这等段。GDT不视作系统段因为其不通过段选择子访问。
门(Gate):特殊的段选择子,通过门进行的调用可以进行特权级别的转换。包含过程调用、中断、陷阱、任务等。
- 调用门:可以执行更高特权级别的代码,还可用于16-32位代码段的相互访问。
门的使用方式和段选择子类似,通过门选择子查询GDT或LDT中的门描述符以获取基址,然后结合偏移量访问对应的数据。不过,门提供了特权级别的转换甚至是字长模式的转换,因而是一种特殊的段描述符。
系统段,称其为“系统”,是相对于“应用”而言的。系统段的存在是为了支持操作系统的运行,包括任务上下文保存、地址管理、中断处理等。
长模式下,调用门可以用于64位和32位模式的代码段的相互访问。任务切换门被设置为NULL,其余门(包括TSS选择子和LDT选择子)被扩展为16字节。不过,页表中的特权控制很大程度上代替了门的特权控制功能。如同之前一样,段描述符和门都是在保护中重要,而在长模式中不再重要的概念。
3.任务状态段(Task-State Segment)和任务门(Task Gates)
任务状态段包含了一个任务所需的上下文:
- 通用寄存器
- 条件码:EFLAGS
- 程序计数器:EIP
- 堆栈指针(3个特权级各一个)
- 页表基址寄存器(CR3)
- 局部描述符表选择子
每次任务切换,操作系统都要保存状态,通过GDT选择新TSS并加载状态。
任务状态段要么通过TSSs(任务状态段选择子)访问,要么通过任务门访问,后者可以提供特权控制。
长模式下,不再能通过硬件切换任务,因此任务门不再有效。不过TSS仍然保留,且TSSs设置为指向TSS基址。
任务状态段的存在是为了支持任务切换,而任务切换是为了支持多道程序。一个TSS保存的内容就是一个完整的上下文,包括当前的数据、状态、执行位置等基础信息,还包含其地址空间信息(页表基址寄存器和LDT选择子)。
4.中断和异常处理
外部中断(指外设引发的异步中断),软件中断和异常都通过中断描述符表(IDT)进行管理。
- 中断描述符:IDT中的表项,都是门描述符,包含了中断处理程序的地址、特权级别等信息,也可以是一个任务门。
- 中断描述符表寄存器(IDTR):用来存储IDT的线性地址基址。
- 中断向量:中断描述符的索引,通过中断向量查询IDT中的门描述符。
外设、处理器、软件都可以引发中断。软件中断使用INT系列或BOUND指令。三种形式的中断例如:键盘中断、浮点异常、系统调用。
中断和陷阱会的处理等效于通过调用门调用处理函数,而指向任务门的中断向量会通过任务切换调用相应的处理函数。
5.内存管理
处理器支持物理地址或虚拟地址两种模式。分页启动时,所有的访存都是通过页表完成的。
- 分页(Paging):一种地址管理方式,将线性地址按照一定大小(通常是4K)映射到物理地址。这个映射往往是乱序的。
- 页表(Page Table):管理线性地址到物理地址的映射关系的数据结构。
- 页帧(Page Frame):物理内存中的页大小的连续区域。
- 页表基址寄存器:用来存储页表的地址基址,是一个控制寄存器(Control Register,CR),即CR3。
- 页表条目:包含页或下一级页表的基址,以及访问权限等信息。
长模式下的页表分为4级,每级512个条目,每个条目8字节,每个页表恰好占用4K。页表基址寄存器指向最顶级页表的物理地址基址。
6.系统寄存器
系统寄存器保存了对于操作系统运行至关重要的信息,通常涉及底层的数据结构基址和当前的系统配置。
- 条件码寄存器(EFLAGS):包含了一些标志位,用来表示上一条指令的执行结果。例如进位标志、溢出标志等。还包含模式切换中断处理等标志
- 控制寄存器(CR):包含了一些控制系统行为的标志位。例如分页开关。
- 任务寄存器(TR):用来存储TSS选择子,用于任务切换。
- 描述符表寄存器(DTR):LDTR,IDRT,GDTR指向这些描述符表的基址。
- 调试寄存器(DR)、模型指定的寄存器(MSR)等
除EFLAGS外,大多数操作系统中,系统寄存器仅限RING0使用(最高特权)。这进一步体现了其作为“系统”的能力。
长模式中,大多数寄存器都被扩展为64位,此外增加了可以读写任务优先级寄存器(TPR)的CR8。兼容模式下的DR0-DR3的地址匹配粒度仍然为64位。长模式增加了一些MSR用于支持长模式的系统指令。
7.其他资源
其他资源包括操作系统指令,性能监控计数器,内部高速缓存和缓冲区等。
综述
这一部分,我们迅速的浏览了一下处理器体系结构的组成部分,然而对于其中的细节我们还一无所知。之后,我们将会在内存管理、任务切换、中断处理等上下文中看到这些数据结构是如何具体被操作系统使用的。
值得强调的是,目前为止我们接触的大部分内容都是32位世界的产物,他们包含了一个计算机从1MB的拘谨空间到4GB的广袤世界所必须的诸多基础设施,然而他们中的很多,在今天看来,是一种过分“间接”的设计。随着硬件制造能力的提升,许多曾经的设计成为了工程上的包袱。因而,在64位世界中的内容与32位世界中的内容有很大的不同。
不过,这部分内容仍然是我们了解计算机硬件体系结构的基础,其中提到的基础概念和基本思想(描述符表、分段、任务切换、中断处理)等内容将在操作系统中起到至关重要的作用。并且,他还提供了系统引导阶段必不可少的知识准备,我们将在那里看到上述所有这些设施是如何被初始化的。
运行模式
在最开头,我已经简要的论述了Intel处理器支持的几种模式。除了实模式、保护模式、长模式,还有其他几种模式:
- 系统管理模式(System Management Mode,SMM):电源管理或原始设备制造商(Origin Equipment Manufacturer, OEM)使用的模式
- 虚拟8086模式(Virtual-8086 Mode):用于执行一些16位代码。之所以称其为虚拟,是因为8086原则上是采用实地址系统,为了在现代机器上运行这些代码,需要进行相应的配置。
当然,长模式还有一个兼容子模式和一个纯粹的64位模式。相对来说,实模式和保护模式的切换是我们最关心的,这是因为(读书笔记要求上是这么说的)它不仅是操作系统系统运行时进行的第一个模式切换,更是之后所有模式的基础。通常来讲,系统上电时默认处于实模式,BIOS会将引导扇区的文件读入内存然后加载操作系统。系统引导首先做的事情就是准备切换到保护模式。
从实模式切换到保护模式
Intel手册在后面的章节详细的描述了实模式到保护模式的切换过程。这里,我们将暂时把参考书目切换为《X86汇编语言:从实模式到保护模式》,并简要概括这个过程。
准备好全局描述符表的内容之后:
- 1.关闭中段并清空段寄存器
- 2.使能A20地址线,允许访问1MB以上的内存
- 3.加载全局描述符表(GDT)
- 4.设置CR0,使能保护模式,这是我们第一次使用32位寄存器(通常用%eax做源操作数)
- 5.使用长跳转指令,该条指令应当紧跟先前的指令以清空之前的指令流水线,之后的指令将会以保护模式取指执行。
- 6.设置段寄存器,使得段寄存器指向GDT中的段描述符
- 7.设置栈区,通常把栈区设置在引导位置之下,即0x0000~0x7c00
从实模式切换到实模式
从保护模式切换到实模式,要进行类似的过程, 以维护正确的寻址方式和寄存器指向。
- 关闭中断
- 如果启用分页,跳转到一个直接映射的程序段(线性地址等于物理地址),并且确保GDT和LDT也在直接映射的程序段内,清空CR0的PG位以关闭分页,然后将0H移动到CR3以刷新快表(TLB)
- 将段寄存器指向合适的选择子以便在实模式下可用
- 清空CR0的PE,切换到实模式
- 出于和之前一样的原因,执行一个长跳转指令
- 加载段寄存器的值以便在实模式中使用
x86系统指令寄存器
系统指令寄存器,如同前面所说的,是保存着操作系统运行所需的关键内容的寄存器。事实上,基本上也只有操作系统有修改甚至访问这些寄存器的权限。读写对应寄存器的指令通常都是特权指令,只有在RING0才能执行。
在操作系统层面,比较重要的几组寄存器包含标志寄存器、内存控制寄存器、控制寄存器。
1.标志寄存器
标志寄存器包含了上一条算数运算造成的溢出、补码溢出、0、奇偶位等内容。应用程序可以通过一些用户指令来访问相应的字段。不过,标志寄存器也保存着当前系统相关的信息,例如调试模式、中断控制等、IO特权级别等信息。这些信息只能通过系统指令来访问。
2.内存管理寄存器
这些寄存器用于保存系统全局或某个任务的寻址信息,因此称之为“内存管理寄存器”,主要的内容就是之前提到的几个表结构的基址以及限长。由于TR和LDTR是特殊的系统段,还保存了他们的段选择子和段描述符属性。
这几个寄存器的内容如下所示,注意观察其中LDTR、TR和GDTR、IDTR的结构区别:
其中:
- 基址指的是表/段的首个字节(0字节)的线性地址,限长指的是表/段的最大长度
- 处理器上电或复位时会将他们的(段/表)基址设置为 0,(段/表)限长设置为 OFFFFH。(GDT和IDT不是段)
- TR和LDTR会在任务切换时自动加载,然而并不会自动进行保存。
- 每个寄存器都有对应的L/S指令用于加载(Load)或存储(Store)寄存器的值
- 通过专门的加载指令TR和LDTR加载这两个寄存器时,只需要指定一个选择子,段基址、段限长和段描述符属性会根据这个选择子从GDT中加载
接下来是具体内容:
全局描述符表寄存器 (GDTR) : 保存32/64位的GDT基址,16位的表限长。切换到保护模式时需要重新设置GDTR。
局部描述符表寄存器 (LDTR) : LDTR 寄存器保存一个16位的段选择子,32/64位的LDT段基址、LDT段限长和 LDT 的描述符属性。包含LDT的段也必须在GDT中有一个表项。(因为加载时要通过这个表项补全信息)
中断描述符表寄存器 (IDTR):IDTR 寄存器保存IDT 基址和16位的表限长。
任务寄存器(TR):任务寄存器保存一个16位的段选择子、基址、段限长以及当前任务的 TSS 的描述符属性。
3.控制寄存器
控制寄存器(CR0~4)确定处理器的运行模式和当前执行任务的特征。在保护模式和兼容模式下,这些寄存器都是 32 位的。
长模式下,寄存器扩展到64位,MOV CRn指令可用于修改值,bwlq等后缀被忽略,但还有一些特殊规定,例如CR0和CR4的高32位,CR3的40-51位必须被置为0.
此外,增加了CR8
- CR0:含有控制处理器操作模式和状态的系统控制标志
- CR1:保留
- CR2:含有导致页错误的线性地址
- CR3含有页目录表物理基址地址,因此该寄存器也被称为页目录基地址寄存器PDBR
系统指令
8条系统指令对应于四个内存管理寄存器的Load Store操作:
op | GDTR | LDTR | IDTR | TR |
---|---|---|---|---|
Load | LGDT | LLDT | LIDT | LTR |
Store | SGDT | SLDT | SIDT | STR |
注意,其中LLDT和LTR会自动加载段选择子对应的信息到这两个寄存器里