字节码虚拟机在意外场景中的应用 (2024)

Hacker News Top 工具

摘要

本文探讨了字节码虚拟机的出人意料的应用,特别是Linux内核中的eBPF以及编译后二进制文件中用于调试信息的DWARF表达式。

暂无内容
查看原文
查看缓存全文

缓存时间: 2026/05/25 09:45

# 意想不到的地方出现的字节码虚拟机 来源:https://dubroy.com/blog/bytecode-vms-in-surprising-places/ 2024年4月30日 作为对 Twitter¹(https://dubroy.com/blog/bytecode-vms-in-surprising-places/#fn:1)上一个问题的回应,Richard Hipp 写了一篇文章,解释了为什么 SQLite 使用字节码虚拟机(https://sqlite.org/draft/whybytecode.html)来执行 SQL 语句。 大多数人可能将字节码虚拟机与通用编程语言(如 JavaScript 或 Python)联系在一起。但有时它们会出现在令人意想不到的地方!以下是我知道的一些例子。 ## eBPF 你知道吗,Linux 内核内部有一种扩展机制,它包含一个字节码解释器和一个 JIT 编译器? 我以前完全不知道。它叫做 eBPF(https://en.wikipedia.org/wiki/EBPF),挺有意思的:一个基于寄存器的虚拟机,有十个通用寄存器和超过一百种不同的操作码。 eBPF 中的“BPF”代表 *Berkeley packet filter*(伯克利数据包过滤器),基本思想在1993年的 USENIX 论文(https://www.tcpdump.org/papers/bpf-usenix93.pdf)中有描述: > 许多 Unix 版本提供用户级数据包捕获功能,使得可以使用通用工作站进行网络监控。由于网络监控程序以用户级进程运行,数据包必须跨越内核/用户空间保护边界进行复制。通过部署一个称为数据包过滤器的内核代理,可以最小化这种复制,该代理尽早丢弃不需要的数据包。最初的 Unix 数据包过滤器基于栈式过滤器求值器设计,在当前的 RISC CPU 上性能欠佳。BSD 数据包过滤器(BPF)使用一种新的、基于寄存器的过滤器求值器,其速度比原始设计快 20 倍。 因此,它最初是为一个相当受限的用例设计的:一个有向、无环的控制流图,表示网络数据包的过滤函数。而且,在很长一段时间里,Linux 的实现也同样简单:两个通用寄存器、一个 switch 风格的解释器,没有向后分支。 2011 年,一个补丁为 x86-64(https://lwn.net/Articles/437981/)添加了 JIT 编译器。2012 年,第一个非网络用例出现了(https://lwn.net/Articles/475043/)。然后,在 2014 年,BPF 实现被大幅扩展,逐渐成为通用的内核内虚拟机(https://lwn.net/Articles/599755/): > 它将可用寄存器从两个扩展到十个,增加了一系列与真实硬件指令非常接近的指令,实现了 64 位寄存器,使 BPF 程序能够调用(严格控制的)一组内核函数,等等。内部 BPF 更容易编译成快速的机器码,并且更容易将 BPF 挂钩到其他子系统中。 ## DWARF 表达式 DWARF 是一种文件格式,被 GCC 和 LLVM 等编译器用来在编译后的二进制文件中包含调试信息。假设你要调试以下 C++ 代码: ```cpp void add_two(int x) { int ans = x + 2; return ans + 2; // 哎呀! } ``` 在调试器中,你可能想打印变量 `ans` 的值。但根据编译器和代码的不同,这可能相当困难!`ans` 可能在某寄存器中、在栈上,或者可能被优化掉了(仅举几种可能性)。 解决方案是让编译器指定一个*表达式*,该表达式将计算出局部变量的值。因此,DWARF 规范(https://dwarfstd.org/doc/DWARF5.pdf)包含了一种表达式语言: > **2.5 DWARF 表达式** DWARF 表达式描述了如何计算一个值或指定一个位置。它们以 DWARF 操作的形式表达,这些操作在值的栈上运行。DWARF 表达式被编码为操作的流,每个操作由一个操作码后跟零个或多个字面量操作数组成。操作数的数量由操作码隐含。 由调试器负责求值这些表达式。GDB 和 LLDB 都有一个基于 switch 的 DWARF 表达式解释器²(https://dubroy.com/blog/bytecode-vms-in-surprising-places/#fn:2)。 ## GDB 代理表达式 但事实证明,GDB 还有另一个字节码解释器! > 使用 GDB 的 `trace` 和 `collect` 命令,用户可以指定程序中的位置,以及到达这些位置时要求值的任意表达式。当 GDB 调试远程目标时,运行在目标上的 GDB 代理代码自行计算表达式的值。为了避免在代理上维护一个完整的符号表达式求值器,GDB 将源语言中的表达式翻译成更简单的字节码语言,然后将字节码发送给代理;代理随后执行字节码,并记录值供 GDB 稍后检索。字节码语言很简单;有大约四十个操作码,其中大部分是常见的 C 操作数词汇(加法、减法、移位等)以及各种大小的字面量和内存引用操作。字节码解释器严格操作在机器级值上——各种大小的整数和浮点数——并且不需要任何关于类型或符号的信息;因此,解释器的内部数据结构很简单,每个字节码只需要几条本地机器指令就能实现。解释器很小,并且很容易确定求值表达式所需的内存和时间严格限制,使其适合调试代理在实时应用中使用。 *(来自《使用 GDB 调试》,附录 F:GDB 代理表达式机制(https://sourceware.org/gdb/current/onlinedocs/gdb.html/Agent-Expressions.html))* ## WinRAR WinRAR³(https://dubroy.com/blog/bytecode-vms-in-surprising-places/#fn:3)是一个用于 Windows 的文件压缩工具,使用专有文件格式。谷歌漏洞研究员 Tavis Ormandy 发现 RAR 格式包含用于数据转换的字节码编码(https://blog.cmpxchg8b.com/2012/09/fun-with-constrained-programming.html): > 信不信由你,RAR 文件可以包含一个名为 RarVM 的、类似 x86 的简单虚拟机的字节码。它旨在提供过滤器(预处理器),对输入数据执行一些可逆变换以增加冗余度,从而改善压缩率。 他的 rarvmtools 仓库(https://github.com/taviso/rarvmtools)包含了一些架构细节: > 熟悉 x86(最好是 Intel 汇编语法)将是一个优势。RarVM 有 8 个命名寄存器,称为 r0 到 r7。r7 用作栈相关操作(如 push、call、pop 等)的栈指针。然而,与 x86 一样,没有限制将 r7 设置成任何你想要的值,尽管如果你做栈相关操作,它会在该操作期间被掩码以适配地址空间。 ## GPU 上的灵活着色器 《大规模并行渲染复杂封闭形式隐式曲面》(https://www.mattkeeter.com/research/mpr/keeter_mpr20.pdf)(2020): > 我们不是编译特定于模型的着色器程序/内核,而是设计一个通用解释器和一个编码格式,用于表示定义形状的算术表达式。这比执行特定模型程序效率低,但我们的核心优化是在每个递归级别和区域(完全在 GPU 上)减小表达式的大小;为每帧成千上万个专用程序重新编译着色器是不可行的。 《超级着色器:解决不可能问题的荒谬方案》(https://dolphin-emu.org/blog/2017/07/30/ubershaders/)(2017): > 有时,解决不可能问题的最佳方法之一是改变你的视角。无论我们如何尝试,都无法像游戏更改配置那样快地编译专用着色器。但如果我们不必依赖专用着色器呢?一个疯狂的想法诞生了:用一个直接在 GPU 上作为一组巨型灵活着色器运行的解释器来模拟渲染管线本身。如果我们在游戏启动时编译这些巨型着色器,那么每当游戏配置 Flipper/Hollywood 来渲染某些内容时,这些“超级着色器”就会自行配置并渲染它,而不需要任何新的着色器。理论上,这将通过完全避免编译来解决着色器编译卡顿。 另一些例子: - TrueType(https://developer.apple.com/fonts/TrueType-Reference-Manual/)字体规范包含一组超过 200 条指令,用于字形渲染和提示。 - PostScript(https://en.wikipedia.org/wiki/PostScript)不仅是一种页面描述语言,还是一种相对强大的基于栈的编程语言。PostScript 文件是纯文本,因此 PostScript 渲染器不一定使用字节码,但规范也包含二进制编码。 👉 *在 Hacker News 上讨论(https://news.ycombinator.com/item?id=40211205)。*

相似文章

SBCL: 终极汇编代码面包板 (2014)

Hacker News Top

一篇技术博客文章,探讨如何使用SBCL作为汇编代码的面包板,重点介绍基于堆栈的虚拟机技术,如旋转堆栈和高效的原语操作分发,并引用了F18处理器和x87堆栈。

Zig ELF 二进制文件代码高尔夫 (2025)

Lobsters Hottest

深入技术探讨如何缩小 Zig ELF 二进制文件的大小,从 2180K 缩减至 500 字节以下,通过去除调试信息、切换到 ReleaseSmall 以及使用 freestanding 目标。

QBE – 编译器后端

Hacker News Top

QBE 是一个紧凑的、爱好级别的编译器后端,仅用 10% 的代码即可实现工业级优化编译器 70% 的性能,支持 amd64、arm64 和 riscv64,并采用简单的基于 SSA 的中间语言。

Itanium C++ ABI中虚表的工作原理

Lobsters Hottest

一篇详细的博客文章,解释了Itanium C++ ABI中虚表(vtable)的实现方式,涵盖虚表结构、修饰名称和虚函数调度。