XV6

本章主要给出 XV6 运行的硬件平台 X86 的基本概念,主要是 ISA 架构范畴内的硬件知识。 读者在后续阅读中,需要注意联系 XV6 运行中的各个阶段和 X86 硬件配置及状态的关系,从而知道相应代码的运行环境及需要解决的问题。

在讨论处理器架构的话题中,经常会涉及 ISA 架构和微架构两个术语,前者是指对处理器硬件的抽象,用于支撑上层软件编写,而后者则是对该抽象的内部实现,用于硬件体系结构设计和分析。我们分析 XV6 操作系统在 X86 处理器上运行,实际上只需要知道 X86 处理器的 ISA 架构即可。ISA 架构全程是 Instruction Set Architecture,涵盖了该处理器的寄存器组织、汇编指令格式和功能、存储器及 IO 地址组织、异常 / 终端机制等概念。我们先给出寄存器和内存组织的信息,其余的信息在后续讨论中根据需要在作必要的补充。

1. 寄存器

X86 平台上的 “操作数” 可以在内存单元、寄存器或者是 IO 端口中,其中寄存器位于处理器内部,我们首先来学习 x86 ISA 寄存器组织,寄存器命名及其用途。在刚启动的时候,x86 处理器都工作于最初的 8086 模式,此时 x86 中软件可见的主要寄存器有:

寄存器 描述
通用寄存器 8 个 32 位,存放正在处理的数据
段寄存器 6 个 16 位,处理内存访问
指令寄存器 1 个 32 位,指向要执行的下一条指令码
浮点寄存器 8 个 80 位,存放浮点数据
控制寄存器 5 个 32 位,确定处理器的操作模式
调试寄存器 8 个 32 位,在调试处理器时包含信息
标志寄存器 EFLAGS

我们学习的 XV6 是操作系统,因此它还需要更多的硬件知识,后面会逐步增加所需 x86 ISA 的背景知识。

浮点寄存器和调试寄存器没有在 XV6 中使用。

通常 X86 对操作系统隐藏了缓存,所以我们只需要考虑寄存器和主存两种存储器,不用担心主存的层次结构引发的差异。

1.1 通用寄存器

通用寄存器 描述
EAX 用于操作数和结果数据的累加器
EBX 指向数据内存段的数据的指针
ECX 字符串和循环操作的计数器
EDX IO 指针
EDI(destination) 用于字符串操作的目标的数据指针
ESI(source) 用于字符串操作的源的数据指针
ESP(stack) 堆栈指针
EBP(base) 堆栈数据指针

为了兼容原来 16 bit 架构,这些寄存器的低 16 bit 仍按照原来的命名方式,去掉前缀 E 并且对应于 x86 16 bit 架构中的相应寄存器 AXBXCXDXSPBPSIDI。同样出于兼容性的目的,前 4 个通用寄存器 EAXEBXECXEDX 的低 16 位还可以继续划分成高低各 8 bit,例如 AX 可以划分成各自可以独立访问的 AHAL

|<------------EAX---------------->|
|1 2 3 4 5 6 7 8 |1 2 3 4 5 6 7 8 |
|----------------|-------|--------|
|. . . . . . . . |. AH . | . AL . |
|----------------|-------|--------|
. . . . . . . . .|<------AX------>|

虽然称它们是通用寄存器,这只是表明它们可以作为通用目的、保存任意数据,而实际上软硬件在使用这 8 个寄存器时,还是有一些特殊的约定。具体而言,这些 “特定” 的功用说明如下:

  1. EAX 寄存器也称为累加器,用于协助执行一些常见的运算操作以及用于传递函数调用的返回值。在 x86 指令集中很多经过优化的指令会优先将数据写入或读出 EAX 寄存器,再对数据进行进一步运算操作。大多数运算如:加法、减法和比较运算都会借助使用 EAX 寄存器来达到指令优化的效果。还有一些特殊的指令如:乘法和除法则必须在 EAX 寄存器中进行。
  2. EDX 是一个数据寄存器。这个寄存器可以被认为是 EAX 寄存器的延伸部分,用于协助一些更为复杂的运算指令,如:乘法和除法,EDX 被用于存储这些指令操作的额外数据结果。
  3. ECX 被称为计数器,用于支持循环操作。需要特别注意的是 ECX 寄存器通常是反向计数的(递减 1),而非是正向计数。
  4. ESI 被成为源变址寄存器,这个寄存器存储这输入数据流的位置信息。EDI 寄存器则指向相关目的数据操作存放的位置,我们称其为目的变址寄存器。这 2 个寄存器主要涉及到数据处理的循环操作(例如字符串拷贝)。可以简记为 ESI 用于 “读”,EDI 用于 “写”。在数据操作中使用源变址寄存器和目的变址寄存器可以极大的提高程序运行效率。
  5. ESP 和 EBP 寄存器分别被成为栈指针和基址指针。这些寄存器用于控制函数调用和相关栈操作。当一个函数被调用时,调用参数连同函数的返回地址将先后被压入函数栈中。ESP 寄存器始终指向函数栈的最顶端,由此不难推出在调用函数过程中的某一时刻, ESP 指向了函数的返回地址。EBP 寄存器被用于指向函数栈帧的最低端。在某些情况 下,编译器为了指令优化的目的可能会避免将 EBP 寄存器作为栈帧指针。在这种情况下,被 “释放” 出来的 EBP 寄存器可以像其他寄存器一样另作他用。
  6. EBX 是唯一没有被指定特殊用途的寄存器,真正意义上的 “通用” 寄存器。

1.2 段寄存器

IA32 允许 3 种访问系统内存的方法:

  1. 平坦内存模式:所有指令、数据、堆栈包含在相同的地址空间内,通线性地址访问每个内存位置。
  2. 分段内存模式:划分为 3 个段,指令段,数据段,堆栈段;内存位置通过逻辑地址定义,包含段地址和偏移地址;处理器将逻辑地址转换为线性地址。
  3. 实地址模式:所有段寄存器指向 0 线性地址,所以指令,数据,堆栈元素搜通过线性地址直接访问。
段寄存器 描述
CS(code segment) 代码段基地址。处理器根据 CS 值和 EIP 中的偏移值取指令
DS(data segment) 数据段基地址
ES(extra segment) 附加段指针
FS 同 ES
GS 同 ES
SS(stack segment) 堆栈段基地址;包含传递给函数和过程的数据值

1.3 指令寄存器

EIP(extended instruction pointer)跟踪要执行的下一条指令码,记录偏移值或者线性地址;不可直接修改。

1.4 控制寄存器

控制寄存器 描述
CR0 控制处理器状态和操作模式的系统
CR1 当前没有使用
CR2 内存页面错误信息
CR3 内存页面目录信息
CR4 支持处理器特性和说明处理器特性的标志

X86-32 引入保护模式,因此需要相应的控制寄存器。控制寄存器并不是普通应用编程所能涉及的寄存器,而且必须在最高特权级才能修改设置它们,因此通常由操作系统内核代码对他们进行设置。

X86-32 的控制寄存器有 4 个:CR0、CR1、CR2、CR3。这些寄存器的用途现在还无法展开讨论,我们会在学习 X86 的保护模式及分页机制的时候加以说明。当设置了 CR0 的 PE 位置后,将启动保护模式,从而可以看到更多的硬件资源和相应的更多的寄存器。

CR3 只有高 20 位有效,存储页表首地址。

CR2 存储页错误线性地址。

CR1 保留。

CR0 比较复杂,放到后面讲。

1.5 标志寄存器

每个操作都要有一种机制确定操作是否成功。IA32 平台使用 1 个 32 位的 EFLAGS 包含一组状态,控制和系统标志。

2. 标志寄存器

该寄存器比较复杂,单独拿出来讲。每个操作都要有一种机制确定操作是否成功,IA32 平台使用 1 个 32 位的 EFLAGS 包含一组状态,控制和系统标志。

2.1 状态标志

标志 名称 描述
CF 0 进位标志 无符号整数运算最高有效位产生进位或借位,置 1
PF 2 奇偶校验标志 奇校验位,使得寄存器和此位 1 的数目为奇数个
AF 4 辅助进位标志 用于 BCD 码的运算
ZF 6 零标志 操作结果为 0,置 1
SF 7 符号标志 设置为结果的最高有效位
OF 11 溢出标志 带符号数运算溢出,置 1

在每次算术逻辑运算后将设置相关的运算结果标志位。x86 的结果标志为说明如下:

  1. 进位标志 CF(Carry Flag)

    进位标志 CF 主要用来反映运算是否产生进位或借位。如果运算结果的最高位产生了一个进位或借位,那么其值为 1,否则其值为 0。使用该标志位的情况有:多字(字节)数的加减运算、无符号数的大小比较运算、移位操作、字(字节)之间移位、专门改变 CF 值的指令等。

  2. 奇偶标志 PF(Parity Flag)

    奇偶标志 PF 用于反映运算结果中 1 的个数的奇偶性。如果 1 的个数为偶数,则 PF 的值为 1,否则其值为 0。利用 PF 可进行奇偶校验检查,或产生奇偶校验位。在数据传送过程 中,为了提供传送的可靠性,如果采用奇偶校验的方法,就可使用该标志位。

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

    在发生下列情况时,辅助进位标志 AF 的值被置为 1,否则其值为 0:

    • 在字操作时,发生低字节向高字节进位或借位时。
    • 在字节操作时,发生低 4 位向高 4 位进位或借位时。
  4. 零标志 ZF(Zero Flag)

    零标志 ZF 用来反映运算结果是否为 0。如果运算结果为 0,则其值为 1,否则其值为 0。 在判断运算结果是否为 0 时,可使用此标志位。

  5. 符号标志 SF(Sign Flag)

    符号标志 SF 用来反映运算结果的符号位,它与运算结果的最高位相同。在微机系统中, 有符号数采用补码表示法,所以 SF 也就反映运算结果的正负号。运算结果为正数时,SF 的值 为 0,否则其值为 1。

  6. 溢出标志 OF(Overflow Flag)

    溢出标志 OF 用于反映有符号数加减运算所得结果是否溢出。如果运算结果超过当前运算位数所能表示的范围,则称为溢出,OF 的值被置为 1,否则,OF 的值被清为 0。“溢出” 和 “进位” 是两个不同含义的概念,不要混淆。如果不太清楚的话,请查阅 “计算机组成原理” 等相关课程中的内容。

对以上 6 个运算结果标志位,在一般编程情况下,标志位 CF、ZF、SF 和 OF 的使用频率较高,而标志位 PF 和 AF 的使用频率较低。

2.2 控制标志

控制处理器的特定行为。

当前只定义了一个控制标志:DF 标志(direction flag),位于第 10 位。控制处理器处理字符串的方式。

DF 位置 1:字符串指令自动低递减内存地址,到达字符串的下一个字节。

DF 位置 0:字符串指令自动低递增内存地址一到达字符串的下一个字节。

2.3 系统标志

控制操作系统级别的操作,应用程序不应该试图修改系统标志。

标志 名称 描述
TF 8 陷阱标志 置 1 时,启用单步模式,每次执行一条指令,等待下一条指令执行的信号。调试汇编程序时极有用
IF 9 中断使能标志 控制处理器如何响应外部源接受到的信号
IOPL 12~13 IO 特权级别标志 表明当前正在运行的任务的 IO 权限级别
NT 14 嵌套任务标志 控制当前执行的任务是否链接到前一个执行的任务。用于链接被中断和被调用的任务
RF 16 恢复标志 控制处理器在调试模式如何响应异常
VM 17 虚拟 8086 状态标志 表明处理器在虚拟 8086 状态执行,而不是保护模式或实模式
AC 18 对准检查标志 和 CR0 的 AM 位一起用于启用内存引用的对准检查
VIF 19 虚拟中断标志 处理器在虚拟模式中操作时,起到 IF 标志位的作用
VIP 20 虚拟中断挂起标志 处理器在虚拟模式中操作时,标志一个中断正被挂起
ID 21 识别标志 表示处理器是否支持 CPUID 指令

光是阅读表格,也许并不能很好的理解其功能作用。但读者无需担心, 一些复杂的概念在后续相应的讨论中理解其外延内容后,才能更好的理解。

状态控制标志位是用来控制 CPU 操作的,它们要通过专门的指令才能使之发生改变。

  1. 追踪标志 TF(Trap Flag)

    当追踪标志 TF 被置为 1 时,CPU 进入单步执行方式,即每执行一条指令,产生一个单步中断请求。这种方式主要用于程序的调试。指令系统中没有专门的指令来改变标志位 TF 的值, 但程序员可用 popf 来改变其值。

  2. 中断允许标志 IF(Interrupt-enable Flag)

    中断允许标志 IF 是用来决定 CPU 是否响应 CPU 外部的可屏蔽中断发出的中断请求。但不 管该标志为何值,CPU 都必须响应 CPU 外部的不可屏蔽中断所发出的中断请求,以及 CPU 内 部产生的中断请求。具体规定如下:

    1. 当 IF=1 时,CPU 可以响应 CPU 外部的可屏蔽中断发出的中断请求。
    2. 当 IF=0 时,CPU 不响应 CPU 外部的可屏蔽中断发出的中断请求。

    CPU 的指令系统中有专门的指令来改变标志位 IF 的值,sticli 分别用于开启和关闭中断 使能。XV6 启动代码中会关闭中断,然后在合适的时候再打开。

  3. 方向标志 DF(Direction Flag)

    方向标志 DF 用来决定在串操作指令执行时有关指针寄存器发生调整的方向。具体规定在 “字符串操作指令” 中给出。在微机的指令系统中,还提供了专门的指令来改变标志位 DF 的值。


相对于 X86-16 架构,X86-32 增加了一些标志位,用于支持保护模式和虚拟 8086 模式等。

  1. I/O 特权标志 IOPL(IO Privilege Level)

    IO 特权标志用两位二进制位来表示,也称为 IO 特权级字段。该字段指定了要求执行 IO 指令的特权级。如果当前的特权级别在数值上小于等于 IOPL 的值,那么,该 IO 指令可执行, 否则将发生一个保护异常。

    XV6 的内核代码将运行于最高的 0 级,而用户代码则运行于最低的 3 级,读者可以在启动代码中关于段寄存器设置。

  2. 嵌套任务标志 NT(Nested Task)

    嵌套任务标志 NT 用来控制中断返回指令 IRET 的执行。具体规定如下:

    1. 当 NT=0,用堆栈中保存的值恢复 EFLAGS、CS 和 EIP,执行常规的中断返回操作;
    2. 当 NT=1,通过任务转换实现中断返回。
  3. 重启动标志 RF(Restart Flag)

    重启动标志 RF 用来控制是否接受调试故障。规定:RF=0 时,表示 “接受” 调试故障,否则拒绝之。在成功执行完一条指令后,处理机把 RF 置为 0,当接受到一个非调试故障时,处理机就把它置为 1。

  4. 虚拟 8086 方式标志 VM(Virtual 8086 Mode)如

    如果该标志的值为 1,则表示处理机处于虚拟的 8086 方式下的工作状态,否则,处理机处于一般保护方式下的工作状态。XV6 并不使用用这个模式,可以暂不关心该标志位。

3. 内存组织

X86-32 处理器出于兼容性的考虑,使得其内存的组织有些难以理解。X86-32 可以工作在实地址模式,也可以工作在基于段的保护模式,保护模式下还可以开启分页机制。不论是哪种模式,其地址的产生都和段寄存器相关。下面将段寄存器及其中英文名字一起列出,帮助读者记忆。

缩写 描述
CS 代码段寄存器(Code Segment Register)
DS 数据段寄存器(Data Segment Register)
ES 附加段寄存器(Extra Segment Register)
SS 堆栈段寄存器(Stack Segment Register)
FS 附加段寄存器(Extra Segment Register)
GS 附加段寄存器(Extra Segment Register)

段寄存器都是 16 位的,因此 X86-32 和 X86-16 在段寄存器的命名和寄存器位宽上是完全一样的,但其使用方法却并不相同。

3.1 段式内存管理

程序中的代码、数据、堆栈等呈现不同的属性,因此也要求它们安装不同的 “段” 分开管理,而不是简单的一视同仁,也就是说分段管理是程序的内在需求。但是在分段管理上,处理器硬件到底如何支持可以呈现出不同方式,甚至是软硬件协作完成。

实地址模式

X86-32 处理器在启动时处于实地址模式,此时程序发出的地址就是物理地址,物理地址由段寄存器的值和相应的偏移构成,将段寄存器的值左移 4 位加上偏移,此时只能访问 1 MB 的内存空间。

此时在一定程度上满足了分段管理的需求,形成了二维的地址空间,一个维度是段号 / 段名,另一个维度是段内偏移。但是 X86 在实模式下并没有访问权限等控制,因此在分段管理方面只提供了地址编码上的支持,使得各段内部可以从 0 地址组织其内部的代码或数据。

实地址模式(Real-address Mode),其实是 IA-32 架构或 Intel 64 架构提供的一种工作模式,该模式基本上提供了和 8086 上一样的执行环境。因此只能看到部分硬件资源,还带有一些扩展,基本能运行原本在 i8086 上运行的程序。

  1. 首先,是和 8086 上一样的执行环境,可寻址的内存空间,范围 [0, 1M]。因最初支持实地址模式的 8086 处理器只有 20 条地址线,所以其寻址范围最大只能去到 2^20
  2. 可用的通用寄存器是:AX, BX, CX, DX, SI, DI, BP, SP。这些寄存器负责临时存放运算结果,或临时存放运算需要的操作数,或临时存放操作数在内存中的地址,或辅助构筑栈(Stack)。
  3. 可用的段寄存器有:CS, DS, ES, SS。这些段寄存器负责存放段的基地址(准确点,实模式下的地址计算是通过将段寄存器里的数值左移 4 位得到的)。
  4. 可用 FLAGS 标记寄存器。
  5. IP 寄存器存放下一条要执行的指令在代码段里的偏移,它联合 CS 决定了下一条要执行的指令在内存里的地址(即 CS<<4+IP)。
  6. 专注浮点运算的寄存器。最初支持实地址模式的 8086 处理器,需要一个叫 8087 math 协处理器来执行浮点运算。
  7. 可寻址的 IO 空间。处理器的数据线和地址线除了可用于内存数据的传输和寻址外, 还可以用于与其它外部设备进行数据的传输和寻址外部设备。当然,也可以通过 Memory-mapped IO 来访问外部设备的数据。IO 空间的寻址范围为 [0,FFFFH]。处理器提供了专门的指令来访问 IO 空间里的数据。
  8. 中断的机制(中断向量表)。
  9. 支持 8086 上所有的指令集。

除了上述内容,X86-32 实地址模式模式并不完全等于 8086,相对于原来的 8086 硬件,还有扩展的部分:

  1. 利用操作数大小修饰符(Operand Size Override Prefix),可访问 32 位的操作数。在此情况下,可访问 32 位的寄存器 EAX, EBX, ECX, EDX, ESP, EDI, ESI。
  2. 可访问增加的两个寄存器 FS 和 GS。
  3. 可执行一些 8086 里没有的指令,这些指令是后面 IA-32 架构的处理器引入的。
  4. 利用地址大小修饰符(Address Prefix),可指定 32 位的地址偏移。

保护模式

XV6 只在启动的时候短暂工作于实地址模式,然后很快就设置 CR0 的 PE 位进入保护模式。在保护模式下,段寄存器的值不再直接作为地址使用,而是当作一个间接(索引)信息,经过段表的转换才能形成段的起始地址,再加上偏移量最终形成物理地址。在保护模式下,如果启用了分页机制,则可以形成段页式内存管理。

保护模式不仅支持了地址编码上的二维管理,而且提供能各段访问属性的控制,是否可读、是否可写、是否可以执行,以及特权级(优先级)的概念,使得低特权级的用户代码不能访问高特权级的操作系统内核代码和数据。在保护模式下,中断入口(及系统调用)的管理也不再是简单的一个地址,而是提供各种 “门” 的机制来加强访问控制。

整体上说,保护模式克服了实地址模式下用户代码可以任意改变内存空间任意一个数据的漏洞,从而杜绝了早期 X86 平台病毒泛滥(且危害程度大)的现象。

关于保护模式的内存分段和分页机制在之后继续专门展开讨论。

特权级

进入保护模式的时候,任何代码在运行时都带有一个运行级别,低级别代码不能访问高级别的数据、不能调用高级别的代码。但是也有例外的情况,当用户需要访问进行系统调用时,通过一个系统调用门而进入内核,该门可以让低运行级别的代码执行高运行级别的内核代码。

这些特权级别形成 0~3 共四种保护级别,其中环 0 级别最高处于最中心,对应内核代码和数据;环 3 级别最低位于最外层,通常对应于用户代码和数据。

XV6 需要区分用户态和内核态,因此需要控制在不同特权保护级别间切换。X86 处理器的 CS 寄存器中有 CPL 位(2 bits)用于记录当前代码的特权级。

4. AT&T 汇编

由于 XV6 使用的是 Linux 下通用的 AT&T 格式汇编,因此有必要对 AT&T 汇编有一些了解。