x86 未定义指令为什么叫 ud2?
如果你查看过 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 是无效操作码这一点。
换句话说,他们撞上了 Hyrum 定律:只要用户足够多,所有可观察到的行为都会有人依赖。惯例附上 XKCD。
0F B9 也出了类似的问题。
既然意识到了大家需要一个可靠的触发无效操作码异常的方式,Intel 决定把它正式化,创造了一条真正受支持、永久无效的指令,命名为 ud2。
之所以叫 ud2,是因为 0F FF 那个变体被追认为 ud0,0F B9 的变体被追认为 ud1,于是推荐的未定义操作码就顺理成章地排到了 ud2。
ud2 的一个好处是,它是一条不带参数的两字节指令,不用去处理那些随机解码出来却毫无用处的源操作数和目标操作数。
额外闲聊:可是我们为什么要在乎 ud0 和 ud1 的无用参数呢?直接把 ud0 和 ud1 也当作两字节无效操作码不就行了?当然,它们后面还有第三个字节,如果内存操作数带偏移量或比例索引可能更多,但处理器并不使用它。
这很关键,因为虽然处理器不使用这些字节,但仍然要解码它们。如果指令的解码范围跨越到了一个不存在的内存页,你就不会得到无效操作码异常了——得到的是访问违例。
再补充一点:某些较老的处理器在解码到 0F FF 时,若未检查指令剩余部分是否正确解码,就会立即触发无效操作码异常。因此,如果 0F FF 位于页面末尾,而下一页不存在,有时你会遇到无效操作码异常,有时则是访问违例。
最好还是使用 ud2。它的行为一致,且在架构上有保证。
分类
主题
分享
作者
Raymond ChenRaymond 参与 Windows 的演进已超过 30 年。2003 年,他创立了名为《旧新事物》(The Old New Thing)的网站,其受欢迎程度远超他最狂野的想象,而这仍让他感到局促不安。该网站催生了一本书,巧合的是书名也叫《旧新事物》(Addison Wesley, 2007)。他偶尔会在 Windows Dev Docs 的 Twitter 账号上露面,讲一些没有实用信息的轶事。
7 条评论
加入讨论。
发表评论取消回复
行为准则 按以下排序: 最新 最新 热门 最旧-
John Musbach
2026 年 9 月 12 日
0
复制 John Musbach 的评论链接
折叠 John Musbach 的评论
我认为如今编译器的能力已经很强,我们真的不必再为汇编语言操心了。当然,如果你身处微软核心团队那是另一回事,但我不知道现在的 Z 世代有多少人真正了解汇编。
登录以投票或回复-
Csaba Varga
4 小时前
0
复制 Csaba Varga 的评论链接
折叠 Csaba Varga 的评论
不用操心?是的,你不需要这么做。但如果你需要编写对性能敏感的代码,就必须掌握基础知识。即便是如今强大的编译器,也未必能解决一个糟糕的算法——比如包含不可预测跳转或缓存利用率低的问题。即使使用向量会更高效且节省内存,编译器也不会自动把链表替换掉。只要你对源代码生成的机器码有一个大致的概念,就能帮助编译器充分发挥其潜力。
大多数人不再需要手写汇编代码,我会……
阅读更多不用操心?是的,你不需要这么做。但如果你需要编写对性能敏感的代码,就必须掌握基础知识。即便是如今强大的编译器,也未必能解决一个糟糕的算法——比如包含不可预测跳转或缓存利用率低的问题。即使使用向量会更高效且节省内存,编译器也不会自动把链表替换掉。只要你对源代码生成的机器码有一个大致的概念,就能帮助编译器充分发挥其潜力。
我承认,如今大多数人已经不需要手写汇编代码了。但能在没有源码的少数情况下读懂汇编,还是挺有用的。而且在 godbolt.org 上折腾一番,也能让你体会到编译器替你做了多少事 😀
收起 登录后投票或回复
-
Csaba Varga
4 小时前
0
复制 Csaba Varga 的评论链接
折叠 Csaba Varga 的评论
-
Shawn Van Ness
20 hours ago
0
Copy link to comment by Shawn Van Ness
Collapse comment by Shawn Van Ness
我从来没怎么听说过 UD2 和它的同类指令。我想让程序崩溃、并在有调试器时中断到调试器的时候,一直都是用 0xCC(INT 3)。
UD2 的处理方式和 0xCC 比起来有什么不同?
登录后投票或回复-
liamgraham
2026年9月12日
0
Copy link to comment by liamgraham
Collapse comment by liamgraham
CC(INT 3)是一个陷阱(trap),主要用于表明违反了前置条件或契约。
UD2 则更像是一种硬性终止,因为正如 Raymond 所说,“执行随机指令”会引发大量 bug。
登录后投票或回复
-
liamgraham
2026年9月12日
0
Copy link to comment by liamgraham
Collapse comment by liamgraham
-
Henry Skoglund
2 days ago
0
Copy link to comment by Henry Skoglund
Collapse comment by Henry Skoglund
这让我想起给 6502 编程的日子:如果程序出了 bug、处理器跳到了未知区域,而未使用的内存恰好被清零过,你可能更容易发现这些 bug,因为 0x00 这个操作码是 BRK(强制中断)。
至于 x86,我猜 UD2 指令出现得实在太晚,那些“好用的”操作码比如 0x00、0xFF 早就被占用了 🙁
在 Acorn 机器上,BRK 向量会捕获此类指令,并将后续字节读取为错误消息。(准确地说,先是一个错误代码,然后是 ASCII 消息。)这让语言和服务 ROM 能以非常简单的方式报告错误,同时也允许通过单一接口捕获来自任何 ROM 或应用程序的错误。因此,BASIC 的 ON ERROR 不仅能捕获 BASIC 本身的错误,还能捕获文件系统、网络、以及你插入的新硬件所对应的 ROM 等的错误。你的文字处理器、FORTH 程序、游戏、调试软件等也都能这样做。
当然,如果你的代码在内存中遇到一个随机的 0x00,屏幕会填满乱码,直到遇到下一个 0x00。由于字符输出例程管理所有图形和文本输出,它还可以清除屏幕、更改屏幕模式、重新定义字符、激活打印机,甚至在读取到正确字节时抑制所有输出。
因此可以说,虽然它让错误处理变得更容易,但也让它变得更加混乱。
-
Joshua Hudson
2 天前
0
复制 Joshua Hudson 的评论链接
折叠 Joshua Hudson 的评论
0x00 已经被占用了,它是 ADD 指令的操作码,从 8086 时代一直沿用至今
登录以投票或回复
0xFF 则是某个两字节指令的前缀字节。不过 0xFF 0xFF 组合即使在今天也不是有效指令(至少我的反汇编器是这么认为的)。
接下来阅读
2026 年 9 月 1 日Microspeak:Funded / unfunded
Raymond Chen
2026 年 7 月 14 日
Microspeak:Double-click and drill down
Raymond Chen
订阅更新
有新文章发布时通知我。 电子邮件 * 国家/地区 * 请选择...美国阿富汗奥兰群岛阿尔巴尼亚阿尔及利亚美属萨摩亚安道尔安哥拉安圭拉南极洲安提瓜和巴布达阿根廷亚美尼亚阿鲁巴澳大利亚奥地利阿塞拜疆巴哈马巴林孟加拉国巴巴多斯白俄罗斯比利时伯利兹贝宁百慕大不丹玻利维亚博内尔波斯尼亚和黑塞哥维那博茨瓦纳布韦岛巴西英属印度洋领地英属维尔京群岛文莱保加利亚布基纳法索布隆迪佛得角柬埔寨喀麦隆加拿大开曼群岛中非共和国乍得智利中国圣诞岛科科斯(基林)群岛哥伦比亚刚果刚果(民主共和国)库克群岛科特迪瓦克罗地亚库拉索塞浦路斯捷克丹麦吉布提多米尼克多米尼加共和国厄瓜多尔埃及萨尔瓦多厄立特里亚爱沙尼亚斯威士兰埃塞俄比亚福克兰群岛斐济芬兰法属圭亚那法属波利尼西亚法属南部领地加蓬冈比亚格鲁吉亚德国加纳直布罗陀希腊格陵兰格林纳达瓜德罗普关岛危地马拉根西岛几内亚几内亚比绍圭亚那海地赫德岛和麦克唐纳群岛洪都拉斯中国香港特别行政区匈牙利冰岛印度印度尼西亚伊拉克爱尔兰马恩岛以色列意大利牙买加扬马延日本泽西岛约旦哈萨克斯坦肯尼亚基里巴斯韩国科索沃科威特吉尔吉斯斯坦老挝拉脱维亚黎巴嫩莱索托利比里亚利比亚列支敦士登立陶宛卢森堡中国澳门特别行政区马达加斯加马拉维马来西亚马尔代夫马里马耳他马绍尔群岛马提尼克毛里塔尼亚毛里求斯马约特墨西哥密克罗尼西亚摩尔多瓦摩纳哥蒙古黑山蒙特塞拉特摩洛哥莫桑比克缅甸瑙鲁尼泊尔荷兰新喀里多尼亚新西兰尼加拉瓜尼日尔尼日利亚纽埃诺福克岛北马其顿北马里亚纳群岛挪威阿曼巴基斯坦帕劳巴勒斯坦当局巴拿马巴布亚新几内亚巴拉圭秘鲁皮特凯恩群岛波兰葡萄牙波多黎各卡塔尔留尼汪罗马尼亚卢旺达萨巴圣巴泰勒米圣基茨和尼维斯圣卢西亚圣马丁圣皮埃尔和密克隆圣文森特和格林纳丁斯萨摩亚圣马力诺圣多美和普林西比沙特阿拉伯塞内加尔塞尔维亚塞舌尔塞拉利昂新加坡圣尤斯特歇斯圣马丁(荷属)斯洛伐克斯洛文尼亚所罗门群岛索马里南非南乔治亚和南桑威奇群岛南苏丹西班牙斯里兰卡圣赫勒拿、阿森松和特里斯坦-达库尼亚苏里南斯瓦尔巴瑞典瑞士中国台湾塔吉克斯坦坦桑尼亚泰国东帝汶多哥托克劳汤加特立尼达和多巴哥突尼斯土耳其土库曼斯坦特克斯和凯科斯群岛图瓦卢美国边远岛屿美属维尔京群岛乌干达乌克兰阿拉伯联合酋长国英国乌拉圭乌兹别克斯坦瓦努阿图梵蒂冈委内瑞拉越南瓦利斯和富图纳也门赞比亚津巴布韦 我希望订阅 The Old New Thing 新闻通讯。隐私声明。 订阅 关注此博客 确定要删除这条评论吗? 确定 取消 登录 主题插入/编辑链接
关闭输入目标网址
网址 链接文本 在新选项卡中打开链接或链接到已有内容
搜索 未指定搜索词,显示最近项目。 输入搜索词或使用上下方向键选择项目。 取消