数学很难(VAX 上的 OpenBSD)

Lobsters Hottest 新闻

摘要

OpenBSD 开发者细数 VAX 浮点异常的恼人怪癖,以及它们如何给内核移植添堵。

<p><a href="https://lobste.rs/s/nvwn1i/math_is_hard_openbsd_on_vax">评论</a></p>
查看原文 导出为 Word 导出为 PDF
查看缓存全文

缓存时间: 2026/04/22 05:38

# 数学很难 来源:http://miod.online.fr/software/openbsd/stories/vaxfp.html 在 Unix 环境下开发软件时,你通常可以复用同一套系统调用,并享受完善的开发工具,而无需关心具体平台——大多数处理器都提供丰富的指令集和虚拟内存等特性。 可一旦“翻围墙”进入内核世界,所有平台差异的丑陋细节就再也躲不掉了;某些处理器架构的短板,会让你真切体会到什么叫“屁股疼”。 如果你读过 m88k 的辛酸史(http://miod.online.fr/software/openbsd/stories/m88kall.html),大概还记得:操作系统异常处理程序必须在返回前完成所有悬而未决的 load/store,这一需求曾让那帮黑客头疼多年。 88100 并不是唯一让内核开发者怀疑人生的 CPU。今天给你讲一段“处理器设计一时爽,内核填坑火葬场”的往事。 --- ## VAX 浮点异常:教科书式的设计,地狱式的实现 VAX(https://en.wikipedia.org/wiki/VAX)架构诞生于 1977 年末,是最早的 32 位架构之一。 指令集庞大、寻址方式多到眼花,但没什么“花活”:没有乱序执行、没有分支延迟槽、没有寄存器重命名、没有超线程,最早连缓存都没有——因为根本跑不过内存刷新周期(当年 CPU 速度用周期时间描述,5 MHz 就是 200 ns;内存刷新约 120 ns,80 ns、70 ns、60 ns 是 90 年代以后的事了)。 VAX 的异常模型同样朴素,《VAX 体系结构参考手册》第一版“异常与中断”一章只有 36 页(第二版 43 页,主要是字体大了)。 > 陷阱(trap)是在**导致异常的指令执行完毕后**才发生的异常,因此压栈的 PC 指向**下一条**指令。 > 故障(fault)发生在**指令执行过程中**,此时寄存器与内存状态仍保持一致,消除故障条件后**重新执行该指令**可得到正确结果;压栈的 PC 指向**故障指令本身**。 很标准的定义: - 不可恢复 → 陷阱,进程直接被杀。 - 有机会补救 → 故障,内核先抢救,不行再杀。 举例: - 访问未映射页面 → 故障,合法地址就换入,非法就发 SIGSEGV。 - 整数除零 → 陷阱,直接发 SIGFPE(整数除零也会报“浮点异常”,`siginfo_t` 会告诉你到底是 `FPE_INTDIV` 还是 `FPE_FLTDIV`)。 于是 VAX 的异常处理函数 `trap()`(`sys/arch/vax/vax/trap.c`)自 3BSD 以来几乎没变:缺页就交给 VM,算术陷阱就递 SIGFPE。 --- ### 冷知识:1980 年的信号名 当年可没有 SIGILL、SIGSEGV 这些“洋气”名字: ```c #define SIGINS 4 /* illegal instruction */ #define SEGSEG 11 /* segmentation violation */ #define SIGFPT 8 /* floating exception */ ``` --- ## 2002 年 4 月:Perl 5.8 快照在 i386 / VAX 上“转圈自杀” **Todd Miller** 负责维护 OpenBSD base 里的 Perl。他测试即将发布的 Perl 5.8 快照时发现: miniperl(构建阶段用的小号 Perl)会在 i386 和 VAX 上**死循环**——CPU 占满但啥也不干。 Todd 很快给出了最小复现: ```c signal(SIGFPE, SIG_IGN); int i = 1 / 0; ``` i386 的问题迅速解决,VAX 却卡住了。 5 月 7 日,OpenBSD 开发者聊天室: > “Todd,那堆 SIGFPE 破事咋样了?” > “还能咋样,依旧完蛋。Perl 升级后 VAX 又得跪。” 一周后: > “所以正确行为应该是?” > “忽略 SIGFPE 后就不该再递信号,而是跳过那条指令。” 第二天我插话: > “我刚在 Linux-vax 的待办清单里看到: > 算术故障类型 8/9/10 会把 PC 回卷到故障指令;若信号被忽略又没人处理,就会无限循环。” --- ## 内核自救:自己把指令“跳”过去 VAX 的异常模型**不允许**“返回并跳过”这条指令。 内核只能自己算长度,把 PC 掰到下一指令。 VAX 指令变长,极端情况下一条指令能超 16 字节;想算长度就得**反汇编**。 高层逻辑很清晰(`trap.c` 片段): ```c if (code == (T_ARITHFLT | T_USER) && frame->code >= 8) { extern void *skip_opcode(void *); frame->pc = skip_opcode(frame->pc); } ``` 我把内核调试器的反汇编代码“借”过来,6 小时后凑出第一版 diff。 结果被大家喷成狗:调试器代码不能进内核常态路径。 于是重写 `skip_opcode()`,独立实现,仅百来行,DDB 与否都能编译。 新 diff 顺利通过,commit 4833dad9b0 立即进树。 --- ## 余波 7 年后 NetBSD 合并了同一修复,两天后被 **Michael Hitch** 发现顺序 bug: `trapsignal()` 会改 `frame->sp` 以调用用户信号处理函数,因此**必须先** `skip_opcode()`。 我 3 年后才把修正 cherry-pick 回 OpenBSD——因为 base 里新引进的 SQLite 在 VAX 上编译又触发了同一坑。 --- ## 悬案:1979 年就跑 BSD 的 VAX,为何拖到 2002 年才修? 早期几乎**没有程序**会忽略 SIGFPE;信号一来进程就死,根本没机会死循环。 但我觉得真正原因是—— > 早年的 VAX,这些算术故障根本**不存在**。

相似文章

浮点数不与自己一致

Hacker News Top

开发者发布了 `exact-poly`,这是一个使用精确整数算术而非浮点数的二维几何库,旨在消除因 IEEE 754 实现差异导致的跨平台重现性问题。

@no_stp_on_snek: https://x.com/no_stp_on_snek/status/2052833502475833384

X AI KOLs Following

使用 Qwen2.5-32B-Instruct 搭配 longctx 和 vllm-turboquant 的单个 AMD MI300X 开源技术栈,在 MRCR v2 百万级上下文基准测试中取得了与 SubQ 闭源模型(0.659)相竞争的结果(0.601-0.688),表明开源权重方法已接近达到同等水平。

当编译器让你惊喜

Lobsters Hottest

Matt Godbolt 探讨了编译器优化如何将 O(n) 求和循环转换为 O(1) 的闭式解,突出了 Clang 和 GCC 如何采用循环展开和数学简化等复杂技术来大幅提升代码性能。