本文适合对逻辑电路和 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 还支持其它内存寻址方式,如立即数、寄存器移位等)。
- 最左边的两位称为 Mod。
- 第三个字节
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的微指令都会自动等待内存读取 / 写入完成。
那么,我们的微程序如下(左侧序号代表微指令地址,【】中是代码注释):
- 根据
IR[0]表示的指令类型,跳转到对应指令的执行逻辑(假设0x01会跳转到地址[1]处)。 - 如果
(IR[1] & 0x80) != 0,则跳转到add r32, r32的执行逻辑,否则顺序执行; eax → ALU_A【取寻址基寄存器】,然后如果(IR[1] & 0x40) != 0,则跳转到[4];0 → ALU_B【不加偏移量】,控制 ALU 执行加法,并跳转到[5];IR[2] → ALU_B【加偏移量】,并控制 ALU 执行加法;ALU_Out → MAR【计算内存地址】;MDR → ALU_A【读内存值】;ebx → ALU_B,并控制 ALU 执行加法;ALU_Out → MDR【将结果写回内存,并自动等待写入完成】,然后如果(IR[1] & 0x40) != 0,则跳转到[10];- 控制
eip += 2【代表无偏移量的情况】,并跳转到[0]; - 控制
eip += 3【代表有偏移量的情况】,并跳转到[0]。
注:为了简化微程序的表示,以上的程序中忽略了 eax 和 ebx 两个寄存器编号的解码过程。这在电路中不难实现,仍然只需要一条微指令就能够完成。
上述程序实际上仍有优化的空间,比如可以让一条微指令能够同时把数据发送到 ALU_A 和 ALU_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。
参考文献
- 关于 x86 中的 ModR/M byte:Intel硬编码(二):不定长指令、ModR/M与SIB详解(基于P6微架构) - Apollon_krj 的博客