<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>虚拟机  Leo的技术日志</title>
    <link>https://jksoftcn.com/tags/%E8%99%9A%E6%8B%9F%E6%9C%BA/</link>
    <description>  虚拟机  Leo的技术日志</description>
    <generator>Hugo</generator>
    <language>zh</language>
    <lastBuildDate>Sun, 01 Feb 2026 00:00:00 +0000</lastBuildDate>
      <atom:link href="https://jksoftcn.com/tags/%E8%99%9A%E6%8B%9F%E6%9C%BA/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>漫谈Stack-Based VM的设计(1)</title>
      <link>https://jksoftcn.com/blog/00-a-discussion-on-stack-based-vm-design-1/</link>
      <pubDate>Sun, 01 Feb 2026 00:00:00 +0000</pubDate>
      <guid>https://jksoftcn.com/blog/00-a-discussion-on-stack-based-vm-design-1/</guid>
      <description>&lt;p&gt;Stack-based VM（栈式虚拟机）是一种用栈结构来管理指令执行和内存操作的虚拟机模型。可以把它想象成一个&amp;quot;叠盘子&amp;quot;的机器——每次操作都从栈顶取数据，结果也放回栈顶，使用者不需要关心数据存储在哪里。&lt;/p&gt;&#xA;&lt;p&gt;它的核心特点是：所有算术运算、逻辑运算、函数调用等操作，都通过压栈（push）和弹栈（pop）来完成。比如计算&amp;quot;1+2&amp;quot;，会先把1和2压入栈，然后执行加法指令，从栈顶弹出两个数相加，再把结果3压回栈顶。这种设计让指令集变得非常简洁（每条指令通常只有操作码，不需要指定操作数地址），但相比寄存器式VM，因为频繁的栈操作会产生额外开销，执行效率相对较低。&lt;/p&gt;&#xA;&lt;p&gt;很多编程语言的运行时环境都采用这种模型，因为它实现简单、跨平台性好，适合作为中间表示层。&lt;/p&gt;&#xA;&lt;p&gt;这里说的&amp;quot;栈&amp;quot;指的是操作数栈（operand stack），用于临时存放计算过程中的数据，和程序调用栈（call stack）是分开的。&lt;/p&gt;&#xA;&lt;h2 id=&#34;怎么设计一个最小的vm&#34;&gt;怎么设计一个最小的VM&lt;/h2&gt;&#xA;&lt;h3 id=&#34;vm-的本质&#34;&gt;VM 的本质&lt;/h3&gt;&#xA;&lt;p&gt;一个 VM 至少需要三样东西：指令序列（Bytecode）、执行状态（State）、解释循环（Fetch → Decode → Execute）。&lt;/p&gt;&#xA;&lt;p&gt;所以最小 VM 可以被描述为：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;while (true):&#xA;    instr = code[ip]&#xA;    ip++&#xA;    execute(instr)&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;区别只在于：状态如何组织。&lt;/p&gt;&#xA;&lt;h3 id=&#34;为什么选择-stack-based&#34;&gt;为什么选择 Stack-Based？&lt;/h3&gt;&#xA;&lt;p&gt;在 Stack-Based VM 中，核心状态是一个操作数栈（Operand Stack）和一个指令指针（IP）。指令隐式地从栈中取操作数、把结果放回栈中。&lt;/p&gt;&#xA;&lt;p&gt;例如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;PUSH 1&#xA;PUSH 2&#xA;ADD&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;执行过程：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;stack: []&#xA;PUSH 1  -&amp;gt; [1]&#xA;PUSH 2  -&amp;gt; [1, 2]&#xA;ADD     -&amp;gt; pop 2, pop 1, push 3 -&amp;gt; [3]&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Stack-Based VM 最重要的特点：&lt;strong&gt;指令不需要显式指定操作数位置。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;关于-stack-based-vm-的-pushpop-和-dup-指令&#34;&gt;关于 Stack-Based VM 的 PUSH、POP 和 DUP 指令&lt;/h2&gt;&#xA;&lt;h3 id=&#34;为什么在很多-stack-based-vm-的指令中看不到-push-指令&#34;&gt;为什么在很多 Stack-Based VM 的指令中看不到 PUSH 指令？&lt;/h3&gt;&#xA;&lt;p&gt;Stack-Based VM 一定存在 push 行为，只是：push 往往是&amp;quot;指令的副作用&amp;quot;，而不是一个独立、频繁出现的显式操作。所以不会单独设计一个 PUSH 指令。&lt;/p&gt;&#xA;&lt;p&gt;例如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;LOAD_CONST 42 → push 常量&#xA;ADD           → push 运算结果&#xA;CALL          → push 返回值&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此说：&lt;strong&gt;&lt;code&gt;push&lt;/code&gt; 指令隐藏在几乎其他所有指令中。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;POP 指令用于控制副作用，必不可少。如：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;CALL f&#xA;POP&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果不关心 f 的返回值，就必须显式丢弃它。&lt;/p&gt;&#xA;&lt;p&gt;DUP 指令是用于值共享，如计算 &lt;code&gt;x + x&lt;/code&gt;：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;LOAD x&#xA;DUP&#xA;ADD&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;没有 DUP 指令，就必须重新计算或重新加载，或者设计一个 ADD 的变体。&lt;/p&gt;&#xA;&lt;p&gt;总结一下：值的产生 = 自动 push，值的消费 = pop，值的复用 = dup。这使得 Stack-Based VM 的指令集可以极小化。&lt;/p&gt;&#xA;&lt;h3 id=&#34;常见指令汇总&#34;&gt;常见指令汇总&lt;/h3&gt;&#xA;&lt;p&gt;为了方便参考，这里列出 Stack-Based VM 中出现频率较高的指令及其栈效果：&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;指令&lt;/th&gt;&#xA;          &lt;th&gt;描述&lt;/th&gt;&#xA;          &lt;th&gt;栈效果&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;LOAD_CONST x&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;将常量 x 压入栈&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[] → [x]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;LOAD var&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;将变量 var 的值压入栈&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[] → [val]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;POP&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;弹出栈顶元素并丢弃&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[a] → []&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;DUP&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;复制栈顶元素&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[a] → [a, a]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;ADD&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;弹出两个数，将和压入栈&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[a, b] → [a+b]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;SUB&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;弹出两个数，将差压入栈&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[a, b] → [a-b]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;MUL&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;弹出两个数，将积压入栈&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[a, b] → [a*b]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;CALL f, n&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;调用函数 f，消费 n 个参数，压入返回值&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[arg1..n] → [ret]&lt;/code&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;RET&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;从当前函数返回&lt;/td&gt;&#xA;          &lt;td&gt;&lt;code&gt;[ret] → []&lt;/code&gt;（回到调用者栈帧）&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;h2 id=&#34;从-lispscheme的视角看-stack-based-vm&#34;&gt;从 Lisp（Scheme）的视角看 Stack-Based VM&lt;/h2&gt;&#xA;&lt;p&gt;Lisp / Scheme 是理解栈式 VM 的理想语言，原因在于它的语义天然匹配栈模型：表达式为中心、一切都有返回值、嵌套结构直接对应栈的入栈出栈顺序。&lt;/p&gt;&#xA;&lt;p&gt;一个简单的例子：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lisp&#34; data-lang=&#34;lisp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;+&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;) &lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;)&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;编译为 Stack-Based Bytecode：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;LOAD_CONST 2&#xA;LOAD_CONST 3&#xA;MUL&#xA;LOAD_CONST 4&#xA;ADD&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Stack 的变化如下：&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[]&#xA;[2]&#xA;[2, 3]&#xA;[6]        ← MUL 弹出 2 和 3，压入 6&#xA;[6, 4]&#xA;[10]       ← ADD 弹出 6 和 4，压入 10&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以看出，Scheme 表达式的嵌套层次结构，恰好对应了栈的时序。内层表达式 &lt;code&gt;(* 2 3)&lt;/code&gt; 先执行并将结果留在栈上，外层表达式 &lt;code&gt;(+ ... 4)&lt;/code&gt; 再消费它。&lt;/p&gt;&#xA;&lt;p&gt;再看一个函数调用的例子：&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-lisp&#34; data-lang=&#34;lisp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;(f (&lt;span style=&#34;color:#a6e22e&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;))&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;LOAD_CONST 1&#xA;LOAD_CONST 2&#xA;ADD&#xA;CALL f, 1&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里参数 &lt;code&gt;(+ 1 2)&lt;/code&gt; 先被计算并留在栈顶，然后 &lt;code&gt;CALL&lt;/code&gt; 指令直接从栈上取走它作为参数。如果是多参数调用，比如 &lt;code&gt;(f (+ 1 2) (* 3 4))&lt;/code&gt;，参数会按顺序依次计算并压入栈，最后一个 &lt;code&gt;CALL f, 2&lt;/code&gt; 一次性取走两个参数。这种机制让函数调用的编译变得非常自然。&lt;/p&gt;&#xA;&lt;h2 id=&#34;stack-based-vm-的优缺点&#34;&gt;Stack-Based VM 的优缺点&lt;/h2&gt;&#xA;&lt;p&gt;Stack-Based VM 的优点在于：指令集简单；Bytecode 体积小（很多场景看中这个优点）；编译器实现容易；非常适合解释执行；天然匹配表达式语言。&lt;/p&gt;&#xA;&lt;p&gt;Stack-Based VM 的缺点在于：指令条数多；频繁 push / pop；对 JIT 和 CPU pipeline 不友好（你主动想要优化难，现代 CPU 也难高效执行）；隐式数据依赖，优化困难（在编译器层面）。&lt;/p&gt;&#xA;&lt;h2 id=&#34;stack-based-vm-以外的-vm&#34;&gt;Stack-Based VM 以外的 VM&lt;/h2&gt;&#xA;&lt;p&gt;除了栈式 VM，另外两种常见的架构值得了解。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Register-Based VM（寄存器式虚拟机）&lt;/strong&gt;：它的核心区别在于，指令会显式指定操作数所在的寄存器编号，而不是隐式地从栈顶取数据。比如同样的 &lt;code&gt;1+2&lt;/code&gt;，栈式 VM 需要 &lt;code&gt;PUSH 1; PUSH 2; ADD&lt;/code&gt;，而寄存器式 VM 只需要一条指令：&lt;code&gt;ADD R0, R1, R2&lt;/code&gt;（将 R0 和 R1 的值相加，结果放到 R2）。这样指令条数少、数据依赖显式，对优化和调度友好，但每条指令的体积会更大（需要编码寄存器编号）。Dalvik VM（Android）和 Lua VM 都是典型的寄存器式 VM。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;混合设计&lt;/strong&gt;：有些 VM 会在不同层级混合两种方式。比如底层用栈式的字节码表示（利用其体积小的优势），但在 JIT 编译或运行热路径时，将栈式代码转换为寄存器式的中间表示来做优化和代码生成。这种方式试图兼顾两者的优势。&lt;/p&gt;&#xA;&lt;h2 id=&#34;现实世界中的-stack-based-vm&#34;&gt;现实世界中的 Stack-Based VM&lt;/h2&gt;&#xA;&lt;p&gt;很多 Scheme VM 都是经典的 Stack-based VM。&lt;/p&gt;&#xA;&lt;p&gt;传统的 JVM 是标准的栈式虚拟机，应该也是最具影响力的。（Android 上的 Dalvik VM 则是 Register-based VM。）&lt;/p&gt;&#xA;&lt;p&gt;在文曲星上有个 LavaX，可以编译运行 C 代码，它的运行时也是 Stack-Based VM，现在在 GitHub 上可以找到相关资料。&lt;/p&gt;&#xA;&lt;h2 id=&#34;历史上-stack-based-cpu&#34;&gt;历史上 Stack-Based CPU&lt;/h2&gt;&#xA;&lt;p&gt;除了软件实现的 Stack-Based VM，还有硬件级的实现。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Burroughs B5000 系列&lt;/strong&gt;：人类历史上最成功、最纯粹的栈式 CPU 架构之一。它的指令集和内存管理都围绕栈来设计，对后续栈式体系结构的研究有深远影响。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Forth 处理器&lt;/strong&gt;：为 Forth 语言量身定制的硬件实现，栈操作是第一公民。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;早期 HP 计算器 CPU&lt;/strong&gt;：其后续型号迁移至 ARM 架构时，据报道通过软件层模拟原有的栈式执行环境，保持了用户体验的连续性。&lt;/p&gt;&#xA;&lt;h2 id=&#34;stack-based-vm-对编译器的挑战&#34;&gt;Stack-Based VM 对编译器的挑战&lt;/h2&gt;&#xA;&lt;p&gt;Stack-Based VM 看起来是对编译器友好的，但一旦考虑到优化、JIT、跨平台性能，它一点都不简单。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;隐式操作数依赖：&lt;/strong&gt; 栈式虚拟机的指令不显式指定操作数位置，都隐式地从栈顶获取。这使得数据流分析变得困难，编译器很难追踪值的来源和去向，进而影响依赖分析、死代码消除等优化。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;局部性差：&lt;/strong&gt; 频繁的栈操作（push/pop）导致内存访问模式不规则。即使是简单的表达式计算也需要多次内存读写，cache 命中率通常低于寄存器式 VM。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;冗余的栈操作：&lt;/strong&gt; 典型的栈式字节码包含大量冗余操作。例如计算 &lt;code&gt;(a+b)*(c+d)&lt;/code&gt; 需要：&lt;code&gt;load a, load b, add, load c, load d, add, multiply&lt;/code&gt;，中间结果反复入栈出栈。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;难以进行寄存器分配：&lt;/strong&gt; 传统的寄存器分配算法（如图着色）依赖于明确的变量生命周期分析。栈式架构中变量通过栈槽间接访问，难以应用这些成熟技术。&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;指令调度受限：&lt;/strong&gt; 由于严格的栈顺序依赖，指令重排序空间很小。即使两个计算逻辑上独立，也可能因为栈状态而无法并行或乱序执行。&lt;/p&gt;&#xA;&lt;p&gt;尽管如此，业界也发展出了一些有效的优化手段：&lt;strong&gt;栈到寄存器映射&lt;/strong&gt;，将栈顶几个元素映射到物理寄存器；&lt;strong&gt;窥孔优化&lt;/strong&gt;，识别常见的指令模式并替换为更高效的序列；&lt;strong&gt;JIT 编译&lt;/strong&gt;，运行时将热点代码翻译成寄存器式的机器码；&lt;strong&gt;中间表示转换&lt;/strong&gt;，先转换为 SSA 形式再优化，最后再生成栈式代码。&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>
