Why is the x86 undefined instruction called ud2? Why 2?

Why is the x86 undefined instruction called ud2? Why 2?

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

If you look at x86 compiler output (or if, like me, you’re looking at a crash caused by some software that tried to detour an API), you may see an instruction ud2. What’s up with that? The ud2 instruction is an architecturally undefined instruction, guaranteed to raise an “invalid opcode” exception. 如果你查看 x86 编译器的输出(或者像我一样,在排查因软件尝试拦截 API 而导致的崩溃时),你可能会看到一条 ud2 指令。这是什么东西?ud2 指令是一条架构上未定义的指令,它保证会触发“无效操作码”(invalid opcode)异常。

Some compilers generate it to mark “unreachable” code, so that if execution somehow manages to reach it, you get a crash rather than executing random instructions. For example, if a function marked [[noreturn]] somehow returns, the compiler will put a ud2 after the call so that the program crashes instead of falling through to the next function. 一些编译器会生成它来标记“不可达”代码,这样如果程序执行流意外到达此处,程序会直接崩溃,而不是去执行随机的指令。例如,如果一个标记为 [[noreturn]] 的函数意外返回了,编译器会在调用后放置一个 ud2,这样程序就会崩溃,而不是继续执行到下一个函数。

Anyway, why is this instruction called ud2 instead of just ud? Was there a ud1? What was so wrong about ud1 that we had to make a ud2? I think I can reconstruct what happened. 总之,为什么这条指令叫 ud2 而不是直接叫 ud?曾经有过 ud1 吗?ud1 有什么问题导致我们必须制造一个 ud2?我想我可以还原当时发生的事情。

Originally, there was no architecturally undefined instruction on x86. So people who wanted to force an invalid opcode exception went looking for some byte sequence that reliably raised the invalid opcode exception when executed. Somebody found that the 0F FF sequence led to an invalid opcode exception. Though, for whatever reason, the instruction internally decoded as if it took two parameters, a register destination and a register-or-memory source. The parameters aren’t actually used because the invalid opcode exception gets raised before anything else can happen. 最初,x86 架构上并没有官方定义的“未定义指令”。因此,那些想要强制触发无效操作码异常的人,开始寻找在执行时能可靠触发该异常的字节序列。有人发现 0F FF 序列会导致无效操作码异常。不过,出于某种原因,该指令在内部解码时,会被识别为带有两个参数:一个寄存器目标和一个寄存器或内存源。这些参数实际上并不会被使用,因为在任何其他操作发生之前,无效操作码异常就已经被触发了。

Meanwhile, somebody else found that the 0F B9 sequence also had the same properties. So you now had two factions, the 0F FF believers and the 0F B9 adherents. There really wasn’t much of a battle between them, because both techniques seemed to work, and it’s not like one was coming at the detriment of the other. 与此同时,另一些人发现 0F B9 序列也具有相同的特性。于是出现了两个阵营:0F FF 的信徒和 0F B9 的拥护者。他们之间并没有发生什么激烈的争斗,因为这两种技术似乎都有效,而且它们之间并没有什么冲突。

Intel then worked on their next processor, and maybe they made some changes that resulted in 0F FF no longer raising an invalid opcode exception. Maybe they tried introducing a new instruction that uses 0F FF. Or maybe it was still undefined but just performed some random operation instead of raising the invalid opcode instruction. And when they started running software on their new processor, they found that some programs stopped working, and after laborious investigation, they discovered that the programs were relying on 0F FF being an invalid opcode. 后来,英特尔在研发下一代处理器时,可能做了一些改动,导致 0F FF 不再触发无效操作码异常。也许他们尝试引入了一条使用 0F FF 的新指令,或者它虽然仍未定义,但执行了一些随机操作而不是触发异常。当他们在新的处理器上运行软件时,发现一些程序无法正常工作了。经过艰苦的调查,他们发现这些程序依赖于 0F FF 必须是无效操作码这一特性。

In other words, they ran into Hyrum’s Law: With a sufficient number of users, all observable behaviors will be depended upon by somebody. Obligatory XKCD. A similar discovery was made with 0F B9. 换句话说,他们遇到了“海勒姆定律”(Hyrum’s Law):当用户数量足够多时,所有可观察到的行为都会被某些人所依赖。(此处应有 XKCD 漫画)。对于 0F B9 也发现了类似的情况。

Now that they realized that people wanted a reliable way to trigger an invalid opcode exception, the folks at Intel decided to make it official, and they created an actual supported permanently-invalid instruction and called it ud2. It’s called ud2 because the 0F FF variant was retroactively named ud0, and the 0F B9 variant was retroactively named ud1, leaving ud2 as the recommended undefined opcode. 既然意识到人们需要一种可靠的方法来触发无效操作码异常,英特尔决定将其官方化。他们创建了一条受支持的、永久无效的指令,并将其命名为 ud2。之所以叫 ud2,是因为 0F FF 变体被追溯命名为 ud00F B9 变体被追溯命名为 ud1,从而将 ud2 作为推荐的未定义操作码。

One advantage of ud2 is that it is a two-byte instruction with no parameters, so you don’t have to deal with the random decoded-but-unused source and destinations. ud2 的一个优点是它是一条没有参数的双字节指令,因此你不需要处理那些被解码但未使用的随机源和目标参数。

Bonus chatter: But why do we care about the unused parameters to ud0 and ud1? Can’t we just say that ud0 and ud1 are also two-byte invalid opcodes? I mean, sure, there’s a third byte, or possibly more if the memory operand has an offset or a scaled index, but the processor doesn’t use it. It matters, because even though the processor doesn’t use it, it still decodes it. And if the decoding of the instruction crosses into a not-present page, you don’t get an invalid opcode exception at all. You get an access violation. 额外讨论:为什么我们要关心 ud0ud1 未使用的参数?我们不能直接说 ud0ud1 也是双字节无效操作码吗?我的意思是,确实,后面可能还有第三个字节,如果内存操作数有偏移量或缩放索引,甚至可能更多,但处理器并不使用它们。这很重要,因为即使处理器不使用它们,它仍然会进行解码。如果指令的解码跨越到了一个不存在的页面,你就不会得到无效操作码异常,而是会得到访问违规(access violation)。

Bonus bonus chatter: Except that some older processors raised the invalid opcode instruction as soon as they decoded the 0F FF without checking whether the rest of the instruction decoded properly. So if your 0F FF is at the end of a page, and the next page is not present, you sometimes got an invalid opcode exception and you sometimes got an access violation. Better to stick with ud2. Its behavior is consistent and architecturally guaranteed. 额外补充:不过,一些较旧的处理器在解码出 0F FF 后会立即触发无效操作码异常,而不检查指令的其余部分是否解码正确。因此,如果你的 0F FF 位于页面末尾,而下一页不存在,你有时会得到无效操作码异常,有时会得到访问违规。最好还是坚持使用 ud2。它的行为是一致的,并且在架构上是有保证的。

Worry about it? No, you don’t need to do that. But you need to know the basics if you ever need to write performance-critical code. Even today’s amazing compilers won’t necessarily fix a badly chosen algorithm that contains unpredictable jumps or has poor cache utilization. They won’t replace a linked list with a vector even if the latter would be faster and more memory-efficient. If you have at least a general idea about what your source code turns into, you can help the compiler make the best out of it. Most of us won’t need to hand-write assembly code anymore, I’ll grant you that. Being able to read it could be helpful in rare cases where you don’t have the source code available. It can also help you appreciate what your compiler does for you, when you play around on godbolt.org 😀 需要担心这些吗?不,你不需要。但如果你需要编写对性能要求极高的代码,你必须了解基础知识。即使是当今强大的编译器,也不一定能修复算法选择不当的问题(例如包含不可预测的跳转或缓存利用率低)。即使 vector 比链表更快、内存效率更高,编译器也不会自动帮你替换。如果你对源代码转换后的结果有一个大致的了解,你就能帮助编译器发挥出最佳性能。我承认,我们大多数人不再需要手写汇编代码了。但在极少数没有源代码的情况下,能够阅读汇编代码会很有帮助。当你使用 godbolt.org 时,它也能帮你更好地理解编译器为你做了什么 😀

I’ve never really heard of UD2 and friends.. I’ve always seen and used 0xCC (INT 3) when I want my program to crash.. and break in the debugger, if present. How does the handling of UD2 compare to 0xCC? 我以前从未听说过 UD2 之类的指令……当我想让程序崩溃时,我总是使用 0xCC (INT 3)……如果调试器存在,它会触发断点。UD2 的处理方式与 0xCC 相比如何?

CC (INT 3) is a trap. Mostly used to signal precondition/contract breach. UD2 is more a hard stop because “executing random instructions” is dangerous. CC (INT 3) 是一个陷阱(trap)。主要用于标记前提条件或契约违规。UD2 更像是一个硬停止,因为“执行随机指令”是非常危险的。