为什么x86的未定义指令叫做ud2?为什么是2?

The Old New Thing (Raymond Chen) 新闻

摘要

本文解释了x86未定义指令'ud2'命名背后的历史和原因,追溯了其从非官方操作码到Intel官方实现的演变,这得益于Hyrum定律。

<p>如果你查看x86编译器输出(或者像我一样,正在查看由某些试图劫持API的软件引起的崩溃),你可能会看到指令<code>ud2</code>。这是怎么回事?</p> <p><code>ud2</code>指令是一个架构上未定义的指令,保证会引发“无效操作码”异常。一些编译器生成它来标记“不可达”代码,这样如果执行不知何故到达那里,你会得到崩溃而不是执行随机指令。例如,如果一个标记为<code>[[noreturn]]</code>的函数不知何故返回了,编译器会在调用后放置一个<code>ud2</code>,这样程序会崩溃而不是继续执行下一个函数。</p> <p>总之,为什么这个指令叫<code>ud2</code>而不是直接叫<code>ud</code>?有<code>ud1</code>吗?<code>ud1</code>有什么问题以至于我们需要制作<code>ud2</code>?</p> <p>我想我可以重建发生的事情。</p> <p>最初,x86上没有架构上未定义的指令。所以,想要强制引发无效操作码异常的人们开始寻找一些字节序列,这些序列在执行时能可靠地引发无效操作码异常。</p> <p>有人发现<code>0F FF</code>序列会导致无效操作码异常。尽管由于某种原因,指令内部解码时似乎接受两个参数,一个寄存器目标和一个寄存器或内存源。这些参数实际上没有被使用,因为在任何其他事情发生之前,无效操作码异常就被引发了。</p> <p>与此同时,有人发现<code>0F B9</code>序列也具有相同的特性。所以现在有两个派系,<code>0F FF</code>信徒和<code>0F B9</code>支持者。他们之间并没有太多的斗争,因为两种技术似乎都有效,而且一种并没有损害另一种。</p> <p>然后Intel开始研发他们的下一个处理器,也许他们做了一些更改,导致<code>0F FF</code>不再引发无效操作码异常。也许他们尝试引入一个使用<code>0F FF</code>的新指令。或者它仍然是未定义的,但执行一些随机操作而不是引发无效操作码指令。当他们开始在新处理器上运行软件时,他们发现一些程序停止工作,经过艰苦调查,他们发现这些程序依赖于<code>0F FF</code>是无效操作码。</p> <p>换句话说,他们遇到了<a href="https://www.hyrumslaw.com/">Hyrum定律</a>:只要有足够多的用户,所有可观察的行为都会被某些人依赖。<a href="https://xkcd.com/1172/">必看XKCD</a>。</p> <p>对于<code>0F B9</code>也有类似的发现。</p> <p>既然他们意识到人们想要一种可靠的方法来触发无效操作码异常,Intel的人们决定将其官方化,他们创建了一个实际支持的永久无效指令,并将其命名为<code>ud2</code>。</p> <p>它被叫做<code>ud2</code>是因为<code>0F FF</code>变体被追溯命名为<code>ud0</code>,而<code>0F B9</code>变体被追溯命名为<code>ud1</code>,使得<code>ud2</code>成为推荐的未定义操作码。</p> <p><code>ud2</code>的一个优势是它是一个两字节指令,没有参数,所以你不必处理随机解码但未使用的源和目标。</p> <p><b>额外讨论</b>:但我们为什么要关心<code>ud0</code>和<code>ud1</code>的未使用参数?我们不能说<code>ud0</code>和<code>ud1</code>也是两字节无效操作码吗?我的意思是,当然,有第三个字节,或者如果内存操作数有偏移或缩放索引,可能更多,但处理器并不使用它。</p> <p>这很重要,因为即使处理器不使用它,它仍然<i>解码</i>它。如果指令的解码跨越到一个不存在的页面,你根本不会得到无效操作码异常。你会得到一个访问违规。</p> <p><b>额外额外讨论</b>:除了一些较旧的处理器在解码<code>0F FF</code>后立即引发无效操作码指令,而不检查指令的其余部分是否正确解码。所以如果你的<code>0F FF</code>在页面的末尾,而下一个页面不存在,你有时会得到无效操作码异常,有时会得到访问违规。</p> <p>最好坚持使用<code>ud2</code>。其行为一致且在架构上得到保证。</p> <p>文章<a href="https://devblogs.microsoft.com/oldnewthing/20260910-00/?p=112689">为什么x86未定义指令叫<CODE>ud2</CODE>?为什么是2?</a>首先出现在<a href="https://devblogs.microsoft.com/oldnewthing">The Old New Thing</a>。</p>
查看原文
查看缓存全文

缓存时间: 2026/09/12 02:11

# 为什么x86的未定义指令叫ud2?为什么是2? - 新事物旧缘 来源:https://devblogs.microsoft.com/oldnewthing/20260910-00?p=112689 如果你查看x86编译器输出(或者像我一样,遇到某个试图钩挂API的软件导致的崩溃),可能会看到`ud2`指令。这是怎么回事? `ud2`指令是架构层面未定义的指令,必定会触发“非法操作码”异常。某些编译器会生成该指令来标记“不可达代码”,这样即使执行流意外到达此处,程序也会崩溃而非执行随机指令。例如,当标注为`[[noreturn]]`的函数意外返回时,编译器会在调用后放置`ud2`,使程序崩溃而非跳转执行后续函数。 那么,为什么这条指令叫`ud2`而非简单的`ud`?曾经存在过`ud1`吗?`ud1`究竟有什么缺陷,必须设计`ud2`? 我认为可以还原当时的情况: 最初的x86架构并未定义未定义指令。因此,希望触发非法操作码异常的人们开始寻找能可靠引发该异常的字节序列。 有人发现`0F FF`字节序列会导致非法操作码异常。不过,该指令在内部解码时似乎需要两个参数(寄存器目标和寄存器/内存源操作数),但由于异常在其他操作前就会触发,这些参数实际上不会被使用。 与此同时,另有人发现`0F B9`序列也具有相同特性。于是形成了两个阵营:`0F FF`支持者与`0F B9`拥护者。双方并无激烈争执,因为两种方法都有效,且互不影响。 当Intel开发下一代处理器时,某些改动可能导致`0F FF`不再触发非法操作码异常。可能是他们尝试引入使用`0F FF`的新指令,也可能是该指令仍处于未定义状态但会执行某些随机操作而非引发异常。当他们在新处理器上运行软件时,发现部分程序失效,经艰难排查后确认这些程序依赖于`0F FF`作为非法操作码的特性。 换句话说,他们遇到了海勒姆定律(https://www.hyrumslaw.com/):当用户基数足够大时,所有可观测行为都会被某些人所依赖。相关经典漫画:https://xkcd.com/1172/ 针对`0F B9`也出现了类似发现。 意识到用户需要可靠的非法操作码触发方式后,Intel决定将其正式化,于是创建了官方支持的永久无效指令,命名为`ud2`。 之所以称为`ud2`,是因为`0F FF`变体被追溯命名为`ud0`,`0F B9`变体被追溯命名为`ud1`,因此`ud2`成为推荐的未定义操作码。 `ud2`的优势在于它是两字节指令且无参数,无需处理随机解码但未使用源/目标操作数。 **额外说明**:但为什么要在意`ud0`和`ud1`的未使用参数?不能直接说它们也是两字节非法操作码吗?即使存在第三字节(若内存操作数含偏移量或比例变址则可能更多),但处理器确实不使用它。 这很重要,因为即使处理器不使用这些参数,它仍然会解码指令。若指令解码跨越未映射页面,你不会得到非法操作码异常,而是访问违例。 **更深入的说明**:不过某些旧款处理器在解码`0F FF`时会立即触发非法操作码异常,不会检查指令其余部分是否正确解码。因此,若`0F FF`位于页面末尾且下一页不存在,有时会触发非法操作码异常,有时则触发访问违例。 最好还是使用`ud2`,其行为一致且受架构规范保证。 ### 分类 ### 主题 ## 作者 雷蒙德·陈 雷蒙德参与Windows演进已超三十年。2003年,他创建了名为《新事物旧缘》的网站,其受欢迎程度远超他最疯狂的想象,这种状况至今仍令他毛骨悚然。该网站后来出版了一本同名书籍(Addison Wesley 2007)。他偶尔会出现在Windows开发文档的推特账号上,讲述些并无实用价值的故事。

相似文章

Intel 8087浮点芯片的指令解码

Ken Shirriff

对Intel 8087浮点协处理器指令解码的详细逆向工程分析,解释主CPU与协处理器之间的交互、微码ROM的使用以及总线接口单元。

80386 微码反汇编

Hacker News Top

一篇博客文章,详细介绍了成功反汇编和分析 Intel 80386 微码的过程,揭示了215条指令入口点以及其复杂的内部架构。