为什么 x86 的未定义指令被称为 ud2?为什么是 2?
Why is the x86 undefined instruction called ud2? Why 2?

原始链接: https://devblogs.microsoft.com/oldnewthing/20260910-00/?p=112689

x86 架构中的 `ud2` 指令是一种刻意设计、架构上保证的“无效操作码”,被编译器用于标记不可达代码。通过立即触发崩溃,它可以防止 CPU 在控制流进入不可达路径时,意外执行数据或非预期的指令。 在过去,开发人员会使用诸如字节序列 `0F FF`(后来被命名为 `ud0`)和 `0F B9` (`ud1`) 等不可靠的“黑客手段”来触发无效操作码。这些方法存在问题,因为它们的行为并不统一;一些处理器随着时间推移为它们分配了新功能,而另一些处理器如果其尾部的未使用参数跨越了未映射的内存页,则可能引发意外的访问违规。 为了解决这种不一致性,英特尔引入了 `ud2` 作为官方的双字节指令,专门用于可靠地触发无效操作码异常。与前代指令不同,`ud2` 不携带任何参数,从而确保了架构范围内的行为一致性,避免了旧版非官方方法所带来的不可预知的崩溃或安全风险。

这篇 Hacker News 帖子讨论了 x86 “未定义”指令 `ud2` 的起源与命名。 参与者解释说,`ud2`(操作码 `0F 0B`)是架构上保证触发“无效操作码”异常的标准方式。其编号系统(`ud0`、`ud1` 和 `ud2`)是英特尔事后进行的分类。由于现代架构采用从零开始的索引,早期的、无意中未定义的指令(如 `0F FF` 和 `0F B9`)被分别命名为 `ud0` 和 `ud1`,从而将 `ud2` 留作有意使程序崩溃或标记非法状态的标准推荐指令。 讨论还探讨了开发人员为何使用这些指令而非软件中断。使用无效操作码触发异常之所以更受青睐,是因为它能自动填充处理器所需的寄存器和栈帧,且指令更紧凑,无需复杂的设置代码。该帖还延伸到了早期的计算机历史,详细说明了为何 Windows 驱动器号从“C”开始——这一惯例源于早期的 MS-DOS 系统,当时“A”和“B”被保留给物理或虚拟软盘驱动器使用。
相关文章

原文

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. 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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

联系我们 contact @ memedata.com