CPU 中的微程序(以 x86 为例)

1,814 阅读9分钟

本文适合对逻辑电路和 x86 汇编有一定了解的读者阅读。本文会尽量通俗地解释所有概念,因此本文不需要您对计算机组成的知识有任何了解。

什么是微程序?

最简单的两种 CPU 设计方案分别是单周期(single cycle)和多周期(multicycle)设计。一个单周期或多周期的 CPU 核心通常由 数据通路(datapath)和 控制器(control)两部分组成。数据通路中定义了寄存器、内存输入输出接口,以及在不同元件间传输数据的通道。而控制器中含有 CPU 的控制逻辑,它通过向数据通路发出控制信号,控制数据的流动,以此来完成指令的执行。

微程序(microcode,又称 微代码)是一种代替单周期或多周期的 CPU 设计方案,通常用于解决 CISC 处理器的设计问题。不同于 RISC 指令,一条 CISC 指令通常需要被分解成很多个动作,比如 x86 中的 add [eax+4], ebx 指令的执行流程可以表示成下图:

这条指令总共需要两次 ALU 操作和两次内存操作。在实际的 CPU 中,这条指令可能需要分解成如下这些动作:

  • eax 寄存器的值,发送到 ALU 的 A 输入端口;
  • 取立即数 4,发送到 ALU 的 B 输入端口;
  • 控制 ALU 计算出 eax+4 的值,并发送到内存地址端口;
  • 控制内存进行读操作;
  • 等待内存返回 [eax+4] 的数据,并把数据发送到 ALU 的 A 输入端口;
  • ebx 寄存器的值,并发送到 ALU 的 B 输入端口;
  • 控制 ALU 计算出 [eax+4] + ebx 的值,并发送到内存数据端口;
  • 控制内存进行写操作,并等待写入完成。

如果只靠控制器中的逻辑门来表达所有这些动作,就需要消耗大量的逻辑门,并且电路的深度(逻辑门层数)会显著增加,这样会使得 CPU 设计变得过于复杂且低效。

由于以上这些动作具有程序化、串行化的特点,CPU 设计者们想到如下这个方案:不再直接用逻辑电路来实现控制器,而是在 CPU 中嵌入一个 ROM,用来存放一个特殊的程序,这个程序能够进行指令解码,并控制数据通路完成指令的执行。

这种程序与普通的程序不同,它的作用是 代替控制器 来向数据通路发出控制信号。这样的程序就被称为 微程序

微程序的设计思路

下面我们来考虑如何为 add [eax+4], ebx 这条指令设计微程序。

(注:x86 的实际执行机制比本文所描述的要复杂得多。本文所描述的思路仅作为教学,不代表商用 CPU 的执行方案,请注意。)

add [eax+4], ebx 的指令编码为 01 58 04,可以按如下方式理解:

  • 开头的 01 表示这是一条 add r32/m32, r32 格式的指令,其中 r32 表示 32 位寄存器,m32 表示 32 位内存值;也就是说,这是一条 32 位 add 指令,这条指令的左操作数可以是 32 位寄存器或内存,而右操作数是 32 位寄存器。
  • 第二个字节 58 定义了指令所用到的寄存器和内存的寻址方式(在 x86 中称为 ModR/M byte)。十六进制 58 可以拆解成二进制 01 011 000,其中:
    • 最左边的两位称为 Mod。01 表示使用 m32(内存)作为指令的操作目标,并且内存基址需要加一个 8 位的偏移量(disp8),这个偏移量用指令中的立即数表示;
    • 011 是源寄存器 ebx 的寄存器编号;
    • 最右边的三位称为 R/M。000 代表用 eax 作为内存寻址基寄存器(注意这里不再是寄存器编号,因为 x86 还支持其它内存寻址方式,如立即数、寄存器移位等)。
  • 第三个字节 04 就是指令中的立即数 4,代表内存偏移量。

我们的微程序大致需要进行如下这些操作:

  • eip 位置取指令,记为 IR[x]IR[x] 表示指令第 x 个字节,从 0 开始);
  • 判断 IR[0] 是不是 0x01,如果是,则跳转到 add r32/m32, r32 的处理程序;
  • 判断 IR[1] & 0x80 是否为 0,如果是,那么需要进行内存寻址,因此跳转到内存寻址程序;
  • eax 寄存器的值送入 ALU 的 A 端口 ALU_A
  • 判断 IR[1] & 0x40 是否不为 0,如果是,那么需要读取一个立即数,作为内存地址偏移量;
  • IR[2] 展开成 32 位立即数,送入 ALU 的 B 端口 ALU_B
  • 将 ALU 的输出 ALU_Out 送入内存地址寄存器 MAR,并等待内存读取完成;
  • 将内存数据寄存器 MDR 的值送入 ALU 的 A 端口 ALU_A
  • ebx 的值送入 ALU 的 B 端口 ALU_B
  • 将 ALU 的输出 ALU_Out 送入内存数据寄存器 MDR,并等待内存写入完成;
  • eip 的值加 3。(为了方便,这里把 eip 加 3 的操作放在最后。)

我们把微程序中的每一条指令称为 微指令(microinstruction)。为了方便实现跳转,每条微指令应该有一个对应的 地址,并且微指令应该是 定长的

在我们的设计中,每一条微指令含有以下这些信息:

  • 一条数据转移命令,格式为 源寄存器 → 目标寄存器,如 eax → ALU_A,可以为空;
  • 一个 ALU 操作(这里仅考虑加法),可以为空;
  • 一个跳转命令,可以为空(表示顺序执行);
  • 控制 eip 自增的数值,可以为 0。

注意,与一般的计算机程序不同,一条微指令能够同时进行多个操作,只要这些操作所用到的元件之间没有冲突。这是由电路的并行性导致的自然结果。

为了简化微指令的设计,我们假设数据通路中有如下这些附加器件(除寄存器、ALU 和内存输入输出端口外)和执行机制:

  • 指令寄存器 IR 的长度为 16 个字节,并跟随 eip 自动更新(也就是说,数据通路能够自动根据 eip 取指令);
  • 微程序支持条件跳转,能够通过判断 IR[x] 的某一位是否为 0 或 1 来决定是否跳转;
  • 任何一条使用到 MDR 的微指令都会自动等待内存读取 / 写入完成。

那么,我们的微程序如下(左侧序号代表微指令地址,【】中是代码注释):

  1. 根据 IR[0] 表示的指令类型,跳转到对应指令的执行逻辑(假设 0x01 会跳转到地址 [1] 处)。
  2. 如果 (IR[1] & 0x80) != 0,则跳转到 add r32, r32 的执行逻辑,否则顺序执行;
  3. eax → ALU_A【取寻址基寄存器】,然后如果 (IR[1] & 0x40) != 0,则跳转到 [4]
  4. 0 → ALU_B【不加偏移量】,控制 ALU 执行加法,并跳转到 [5]
  5. IR[2] → ALU_B【加偏移量】,并控制 ALU 执行加法;
  6. ALU_Out → MAR【计算内存地址】;
  7. MDR → ALU_A【读内存值】;
  8. ebx → ALU_B,并控制 ALU 执行加法;
  9. ALU_Out → MDR【将结果写回内存,并自动等待写入完成】,然后如果 (IR[1] & 0x40) != 0,则跳转到 [10]
  10. 控制 eip += 2【代表无偏移量的情况】,并跳转到 [0]
  11. 控制 eip += 3【代表有偏移量的情况】,并跳转到 [0]

注:为了简化微程序的表示,以上的程序中忽略了 eaxebx 两个寄存器编号的解码过程。这在电路中不难实现,仍然只需要一条微指令就能够完成。

上述程序实际上仍有优化的空间,比如可以让一条微指令能够同时把数据发送到 ALU_AALU_B。这里不再深入讨论。

如果希望对微程序的理论有更多的深入了解,可以参考 Wikipedia 上的 Microcode 条目,并深入阅读与计算机组成有关的书籍。

现代 x86 CPU 中的微程序

现代 x86 CPU 中的微程序又被称为 微代码微码。Intel 和 AMD 生产的大多数商用 CPU 中都含有微代码。虽然为了提高执行效率,许多常用指令的执行逻辑都已经以电路的形式固化到了 CPU 中,但仍有一部分复杂指令(如部分浮点指令)的执行逻辑仍然是通过微代码描述的。这样还有一个额外的好处,就是能通过微码的更新来动态修改指令的执行机制,以修复 CPU 中的 bug。

现代的 Intel CPU 在底层采用的实际上是类似 RISC 的执行机制,即 CPU 会自动把 CISC 的指令分解成许多个 微操作(micro-op),然后送入一个类 RISC 的流水线中执行。比如,上文中的 add [eax+4], ebx 可能会被分解成以下这些微操作:

lea     tmp1, [eax+4]   ; 寻址
load    tmp2, [tmp1]    ; 读内存
add     tmp2, ebx       ; 做加法
store   tmp2, [tmp1]    ; 写回内存

将指令进行分解的好处是:分解后的指令粒度更小,更适合于硬件执行,并且更容易进行流水化。

为了进一步提高执行效率,Intel CPU 中还可能针对某些指令序列进行优化,比如进行指令合并(macrofusion)等,以减少完成指令序列所需的时钟周期数。

对于 CISC 指令的分解和合并操作可能由 CPU 中的硬件电路完成,也可能由微代码完成。

Intel 会定期发布微代码的更新,这些更新会以文件的形式,随着操作系统升级包一起被下载到用户的计算机中,并在系统启动时自动安装到 CPU 内。微代码可以用于修补 CPU 的一部分 bug 或漏洞。比如著名的 Spectre 漏洞就是通过微码更新的方式修复的,但由于这个补丁会强制 CPU 在特定情况下关闭部分优化功能,这个补丁也导致了 CPU 性能的降低。

在 Windows 下,微码更新会随着 Windows Update 自动推送。但在 Linux 下,由于版权限制,用户可能需要手动安装对应的软件包,如 intel-microcode

参考文献