Hacker News 中文摘要

RSS订阅

x86 未定义指令 ud2 为何带个 2 -- Why is the x86 undefined instruction called ud2? Why 2?

x86 指令集中有一条专门用来触发未定义指令异常的指令,记作 UD2。它的操作码是 0F 0B,执行后处理器会抛出无效操作码异常,常被编译器用来标记理论上不该到达的位置。这条指令的名字里带着一个数字 2,却看不到同名的 UD 或 UD1 与之并列。既然只有这一条,为什么还要叫 UD2,那个 2 究竟指什么。

正文

如果你查看 x86 编译器的输出,或者像我一样在分析某个软件试图钩取 API 导致的崩溃,可能会看到一条 ud2 指令。这是怎么回事?

Image 2: Microsoft

ud2 是一条架构上未定义的指令,保证会引发“无效操作码”异常。有些编译器会生成它来标记“不可达”代码,这样如果执行流莫名到了这里,程序会崩溃,而不是去执行随机指令。例如,如果一个标记为 [[noreturn]] 的函数居然返回了,编译器就会在调用后放一条 ud2,让程序崩溃,而不是落入下一个函数。

那么,为什么叫 ud2 而不是 ud?难道有过 ud1?ud1 出了什么问题非要搞个 ud2?

Image 6: Raymond Chen

我大概能还原当时的情况。

最初 x86 并没有架构上未定义的指令。所以想强制触发无效操作码异常的人,得去找执行时必定引发该异常的字节序列。

有人发现 0F FF 这个序列会引出无效操作码异常。不过不知为何,这条指令在内部被解码为带两个参数:一个寄存器目标和一个寄存器或内存源。这些参数其实没被用到,因为无效操作码异常在别的事发生前就抛出了。

与此同时,另一个人发现 0F B9 序列也有同样效果。于是出现了两派,信 0F FF 的和信 0F B9 的。两者之间其实没啥争斗,因为两种方法都管用,而且互不损害。

后来 Intel 研发下一代处理器,可能做了些改动,导致 0F FF 不再引发无效操作码异常。也许是想引入使用 0F FF 的新指令,或者它虽仍未定义却执行了某些随机操作。当他们在新处理器上跑软件时,发现有些程序不工作了,经过费力排查,才得知这些程序依赖 0F FF 作为无效操作码。

换句话说,他们撞上了 Hyrum 定律:只要有足够多的用户,所有可观察行为都会被某些人依赖。

0F B9 也被发现了类似问题。

既然意识到人们需要一种可靠触发无效操作码异常的方法,Intel 的人决定将其规范化,创建了真正受支持、永久无效的指令,命名为 ud2。

之所以叫 ud2,是因为 0F FF 被追认为 ud0,0F B9 被追认为 ud1,而 ud2 成了推荐的未定义操作码。

ud2 的一个好处是它是两字节、无参数的指令,所以你不必去处理那些被解码却未使用的源和目标操作数。

有人可能会问:为什么要在意 ud0 和 ud1 那些未用的参数?不能说它们也是两字节无效操作码吗?毕竟后面可能有第三字节甚至更多(如果内存操作数带偏移或比例变址),但处理器反正不用它。

这很关键,因为即使处理器不用那些字节,它仍然会解码。如果指令解码跨进了未映射的页面,你得到的根本不是无效操作码异常,而是访问违规。

还有些老处理器一解码 0F FF 就立刻抛无效操作码异常,不检查后续解码是否正确。所以如果 0F FF 在页尾且下一页不存在,有时是无效操作码异常,有时是访问违规。

最好还是用 ud2,它的行为一致且有架构保证。

Image 22

评论

讨论整体是干货加考据式的围观,分歧不大:少数人质疑文章里 ud2 编号由来的说法只是作者推测,多数人则顺着往下补 x86 里各种未定义指令的细节,另一半乐趣在于吐槽 Intel 追溯命名与从零计数造成的编号错位。

怀疑编号由来只是作者的推测

  • 有人直接追问这是否只是作者的猜测、有没有证据,反映出文章把考据与推断混写会让读者不放心。(少数人) > 这究竟只是他的猜测,还是有证据?(@dataflow)

认为 x86 从来不缺可主动触发的未定义指令

  • 补充指出 UD0、UD1、UD2 都已写进 SDM 和 APM,64 位模式还多了一条单字节变体 UDB(D6),所以「x86 没有软件异常指令」这个前提本身站不住。(较多人)
  • 与之呼应的疑问是:别的架构可以主动抛出软件中断或异常,x86 是没有这套设施,还是这套设施覆盖的用例太少?(少数人) > 一直还存在 UDW(FF FF),也就是第一字节 FF 的 group #5;它真正的意义在全 1 填充的内存、以及没有设备认领的全 1 总线上。(@349ru3h4f03)

对 Intel 从零计数感到意外

  • 有读者说读到那一段时很意外:本以为 0F FF 会顺理成章排 1 号、0F B9 排 2 号,结果 Intel 从零开始数,于是平白多出第三个 ud。(较多人)

用派系恩怨的口吻调侃追溯命名

  • 玩笑式说法是:0F FF 的信徒终于得到补偿,被封为 UD0;而 0F B9 的追随者则被扣上 UD1 的耻辱烙印,等于用命名给两派排了座次。(少数人) > 0F FF 派终于被追认为正统 UD0,0F B9 派则永久背负 UD1 的耻辱。(@hacker_homie)

ud2 在现实里是调试时的路标

  • 有人最近排查随机失败的构建,发现 V8 抛出的 ud2 就停在那里等着他,说明运行时确实把它当断言或陷阱在用,而不只是教科书里的未定义指令。(较多人)

有价值的补充

  • 一个解释力很强的类比:这事就像为什么第一块硬盘的盘符是 C——命名由历史占用决定,而不是由逻辑顺序决定。(@qbane)
  • UDW(FF FF)之所以值得一提,是因为它同时是「全 1 内存」和「无设备响应的总线」的自然编码,属于指令空间与硬件默认值撞车的边角案例。(@349ru3h4f03)