两个浮点寄存器互换,听起来像一根线就能解决。
Ken Shirriff 最近拆的 Intel 8087 FXCH 指令,反而给了一个很好的反例:交换 ST(0) 与 ST(i),在芯片内部要走 14 条微指令。
反常点就在这里。FXCH 不是把两个裸数对调。8087 要先把两个操作数读进 tmpA、tmpB,再检查 tag bits,还要处理空寄存器、invalid exception,以及异常被屏蔽后的 NaN 替换。
这篇逆向好看的地方,不是老芯片怀旧,而是把一件小事讲清了:早期浮点硬件的复杂性,很多时候不在算术本身,而在状态和错误处理。
14 条微指令到底在忙什么
8087 于 1980 年推出,是 x87 浮点体系的起点之一,也影响了后来的浮点标准设计。这里要留个边界:不能把 8087 直接说成现代 IEEE 754 的完整标准,它更像是后来标准形成前的重要工程样本。
8087 内部使用 80 位浮点格式:64 位 significand、15 位 exponent、1 位 sign。数据通路也顺着这个格式拆开,尾数走 64 位路径,指数和符号走另一条路径。
Shirriff 和 Opcode Collective 还原出的微码 ROM 里,共有 1648 条微指令。FXCH 只占其中 14 条,但这 14 条足够说明问题。
FXCH 的执行路径大致是这样:先读 ST(0) 到 tmpA,等待一个周期;再读 ST(i) 到 tmpB;随后检查两个临时寄存器的 tag bits;如果没有踩到空寄存器路径,再把 tmpB 写回 ST(0),把 tmpA 写回 ST(i)。
| 表面动作 | 8087 内部动作 | 代价在哪里 |
|---|---|---|
| 交换 ST(0) 与 ST(i) | 先读入 tmpA/tmpB,再反向写回 | ST(i) 是栈相对位置,不是简单固定编号 |
| 判断值能不能用 | 检查寄存器和临时寄存器的 tag bits | tag bits 记录 valid、special、zero、empty |
| 遇到空寄存器 | 设置 invalid exception | 不能把已弹出的栈位置当正常数值用 |
| 异常被屏蔽 | 用 NaN 替代空值后继续 | 程序不一定立刻停,要保留恢复执行路径 |
所以,14 条微指令不是为了把交换写得复杂。它们是在维护 x87 这套抽象:栈顶、相对寄存器、标签状态、异常语义,都要同时成立。
麻烦来自栈,也来自异常
x87 的寄存器模型和后来常见的 SSE、AVX 不一样。程序员看到的是 ST(0)、ST(1) 这样的栈相对位置,而不是一排稳定编号的通用向量寄存器。
push 和 pop 会改变栈顶。ST(i) 对应到哪个物理位置,要由栈控制逻辑解释。FXCH 的作用,也正是把栈中间某个值换到栈顶,方便后续运算。
这个设计有它的历史合理性。表达式求值天然像栈,早期硬件资源也紧。问题是,状态被藏起来以后,微码就要替你兜底。
更麻烦的是 tag bits。每个栈寄存器有 tag bits,tmpA、tmpB 这样的临时寄存器也有。一个槽位可能是 valid,也可能是 special、zero 或 empty。
FXCH 如果发现任一操作数为空,会设置 invalid exception。但这里不能简单理解成程序必然中断。
如果 invalid exception 没有被屏蔽,微码会在相应位置结束,并交给 8086 侧处理中断。如果异常被屏蔽,8087 会把空值替换成 NaN,然后继续完成交换。
这就是 FXCH 最容易被低估的地方。它交换的不是两个数,而是两个带状态的浮点对象。数值、标签、栈位置、异常策略,缺一项都会把语义讲错。
谁该在意,以及接下来该看什么
这件事最直接影响两类读者。
一类是处理器架构和微码爱好者。读 FXCH 这种小指令,比读一条复杂算术指令更能校准直觉:ISA 手册里的一行说明,落到芯片上可能是一段带条件跳转、状态检测和异常出口的控制流程。
另一类是做 x87 模拟器、反汇编器、验证工具的人。动作上很具体:不能只给 FXCH 写一个交换数值的用例,还要补空寄存器、masked invalid、unmasked invalid,以及 NaN 替换后继续写回的路径。
如果工具只实现正常路径,普通样例可能跑得过去。但遇到依赖异常行为的旧程序、测试集或兼容性验证,就会露出差异。
也要克制一点。Shirriff 这次拆出的是 FXCH 的微码路径,不等于我们已经拿到了 8087 全部控制逻辑。要判断 8087 对后续浮点标准的影响到底更多来自数值格式,还是来自异常处理习惯,还得把算术、栈控制和异常路径放在一起看。
接下来该看的变量很明确:更多 8087 微码路径能否被还原,尤其是普通算术指令遇到 special、zero、empty 时怎样分流。只有这些路径拼起来,才能看清这颗老协处理器真正的工程性格。
回到开头那个问题:为什么交换两个寄存器要 14 条微指令?答案不在交换,在边界。硬件真正难的地方,常常是正常情况之外的那几步。
