本文将带你亲手用Delphi (Pascal) 语言编写一个简单的虚拟机(VM),并通过两个具体的示例程序来深入理解字节码是如何被表示、加载和执行的。这不仅是学习编译原理和逆向工程中虚拟机技术的绝佳实践,也能帮你理解程序如何在底层被解释运行。
整个实现分为三部分:一个包含两个示例的完整VM程序、详细的代码解读,以及核心知识点的总结。
一、用Pascal实现一个简易虚拟机
我们先通过代码来构建这个名为SimpleVM的虚拟机。下面是完整的Pascal源代码,其中包含了VM的核心结构、指令解释循环以及两个演示程序。
为了让你对运行结果有个直观印象,我们先描述一下程序执行后的输出。控制台会先打印“算术运算 (5 + 3 * 2)”的结果11,接着打印“条件跳转测试 (5-5=0 跳转到打印999)”的结果999。整个程序清晰地展示了VM如何按序执行字节码指令。
0001 program SimpleVM;
0002
0003 {$APPTYPE CONSOLE}
0004
0005 uses
0006 SysUtils;
0007
0008 type
0009 // 定义字节码操作码(枚举类型)
0010 // 每个枚举值对应一个整数:OP_NOP=0, OP_PUSH=1, 以此类推
0011 TOpCode = (
0012 OP_NOP, // 0: 无操作,仅跳过
0013 OP_PUSH, // 1: 将立即数压入栈(占2字节:指令+操作数)
0014 OP_POP, // 2: 弹出栈顶并丢弃
0015 OP_ADD, // 3: 弹出两数,相加后压栈(先弹出的是右操作数)
0016 OP_SUB, // 4: 减法(栈顶为减数,次栈顶为被减数)
0017 OP_MUL, // 5: 乘法
0018 OP_DIV, // 6: 除法(整数除法)
0019 OP_JMP, // 7: 无条件跳转(占2字节:指令+目标地址)
0020 OP_JZ, // 8: 条件跳转:如果栈顶为0则跳转(占2字节+弹出栈顶)
0021 OP_DUP, // 9: 复制栈顶元素(Dupicate)
0022 OP_SWAP, // 10: 交换栈顶两个元素
0023 OP_PRINT, // 11: 打印栈顶值(不出栈,仅查看)
0024 OP_HALT // 12: 停止虚拟机执行
0025 );
0026
0027 const
0028 STACK_SIZE = 256; // 操作数栈最大容量
0029 CODE_SIZE = 1024; // 字节码存储区大小(可存放1024个Integer)
0030
0031 type
0032 // 虚拟机核心结构
0033 TVM = record
0034 Code: array[0..CODE_SIZE - 1] of Integer; // 字节码存储区(指令+数据混合)
0035 Stack: array[0..STACK_SIZE - 1] of Integer; // 操作数栈(LIFO结构)
0036 PC: Integer; // 程序计数器(Program Counter),指向下一条要执行的指令地址
0037 SP: Integer; // 栈指针(Stack Pointer),指向栈顶元素索引;-1表示空栈
0038 Halted: Boolean;// 停机标志,设为True时主循环结束
0039 end;
0040
0041 // 初始化虚拟机状态
0042 procedure InitVM(var VM: TVM);
0043 begin
0044 VM.PC := 0; // 从字节码第0条指令开始执行
0045 VM.SP := -1; // -1表示栈为空(因为栈索引从0开始,-1是无效索引)
0046 VM.Halted := False;
0047 // 清空内存(Delphi中FillChar将内存区域置0)
0048 FillChar(VM.Stack, SizeOf(VM.Stack), 0);
0049 FillChar(VM.Code, SizeOf(VM.Code), 0);
0050 end;
0051
0052 // 压栈操作:将Value放入栈顶,SP自增
0053 procedure Push(var VM: TVM; Value: Integer);
0054 begin
0055 if VM.SP >= STACK_SIZE - 1 then // 检查栈溢出(最大索引255)
0056 raise Exception.Create('Stack overflow');
0057 Inc(VM.SP); // 栈指针上移(-1 -> 0 -> 1...)
0058 VM.Stack[VM.SP] := Value; // 在SP位置存入数据
0059 end;
0060
0061 // 出栈操作:取出栈顶值,SP自减
0062 function Pop(var VM: TVM): Integer;
0063 begin
0064 if VM.SP < 0 then // 检查栈下溢(空栈时不能再弹出)
0065 raise Exception.Create('Stack underflow');
0066 Result := VM.Stack[VM.SP]; // 获取栈顶值作为结果
0067 Dec(VM.SP); // 栈指针下移(逻辑上删除该元素)
0068 end;
0069
0070 // 查看栈顶(不出栈):用于DUP、PRINT等操作
0071 function Peek(var VM: TVM): Integer;
0072 begin
0073 if VM.SP < 0 then
0074 raise Exception.Create('Stack empty');
0075 Result := VM.Stack[VM.SP]; // 仅读取,不修改SP
0076 end;
0077
0078 // 核心解释器:Fetch(取指)- Decode(译码)- Execute(执行)循环
0079 procedure RunVM(var VM: TVM);
0080 var
0081 Opcode: Integer; // 当前指令的操作码
0082 Operand: Integer; // 指令的操作数(如果有)
0083 A, B: Integer; // 临时变量,用于二元运算
0084 begin
0085 // 主循环:直到执行HALT指令或发生异常
0086 while not VM.Halted do
0087 begin
0088 // ===== 阶段1:Fetch(取指)=====
0089 // 检查PC合法性(防止越界访问字节码数组)
0090 if (VM.PC < 0) or (VM.PC >= CODE_SIZE) then
0091 raise Exception.Create('PC out of bounds');
0092
0093 // 从Code数组中读取当前指令
0094 Opcode := VM.Code[VM.PC];
0095
0096 // ===== 阶段2&3:Decode & Execute(译码与执行)====
0097 // 注意:Ord()函数将枚举转换为整数,因为case语句在Delphi中需要整数
0098 case Opcode of
0099 Ord(OP_NOP):
0100 // 空操作,仅将PC+1,移动到下一指令
0101 Inc(VM.PC);
0102
0103 Ord(OP_PUSH):
0104 begin
0105 // PUSH指令格式:[OP_PUSH][立即数]
0106 // 立即数存储在下一个字(PC+1的位置)
0107 Operand := VM.Code[VM.PC + 1];
0108 Push(VM, Operand); // 将立即数压入栈
0109 Inc(VM.PC, 2); // PC跳过2个字(指令+操作数)
0110 end;
0111
0112 Ord(OP_POP):
0113 begin
0114 Pop(VM); // 丢弃栈顶值
0115 Inc(VM.PC); // PC加1
0116 end;
0117
0118 Ord(OP_ADD):
0119 begin
0120 // 注意POP顺序:栈是LIFO,先弹出的是后压入的(右操作数)
0121 B := Pop(VM); // B = 右操作数(栈顶)
0122 A := Pop(VM); // A = 左操作数(次栈顶)
0123 Push(VM, A + B); // 计算A+B,结果压栈
0124 Inc(VM.PC);
0125 end;
0126
0127 Ord(OP_SUB):
0128 begin
0129 B := Pop(VM); // B = 减数(右操作数)
0130 A := Pop(VM); // A = 被减数(左操作数)
0131 Push(VM, A - B); // 计算A-B(注意顺序:次栈顶- 栈顶)
0132 Inc(VM.PC);
0133 end;
0134
0135 Ord(OP_MUL):
0136 begin
0137 B := Pop(VM);
0138 A := Pop(VM);
0139 Push(VM, A * B);
0140 Inc(VM.PC);
0141 end;
0142
0143 Ord(OP_DIV):
0144 begin
0145 B := Pop(VM); // 除数
0146 A := Pop(VM); // 被除数
0147 if B = 0 then // 除零检查
0148 raise Exception.Create('Division by zero');
0149 Push(VM, A div B); // 整数除法(Delphi的div关键字)
0150 Inc(VM.PC);
0151 end;
0152
0153 Ord(OP_JMP):
0154 begin
0155 // 无条件跳转格式:[OP_JMP][目标地址]
0156 Operand := VM.Code[VM.PC + 1]; // 读取目标地址
0157 VM.PC := Operand; // 直接修改PC,实现跳转
0158 // 注意:不需要Inc,因为已经直接赋值
0159 end;
0160
0161 Ord(OP_JZ):
0162 begin
0163 // 条件跳转(Jump if Zero):如果栈顶为0则跳转
0164 // 指令格式:[OP_JZ][目标地址]
0165 // 执行逻辑:先弹出栈顶值判断,再决定是否跳转
0166 Operand := VM.Code[VM.PC + 1]; // 读取目标地址(紧接在指令后)
0167
0168 if Pop(VM) = 0 then // 弹出栈顶并判断是否为0
0169 VM.PC := Operand // 为零:跳转到目标地址
0170 else
0171 Inc(VM.PC, 2); // 不为零:顺序执行,跳过指令和操作数
0172 end;
0173
0174 Ord(OP_DUP):
0175 begin
0176 // 复制栈顶:例如栈为[5, 3](3是栈顶),执行后变为[5, 3, 3]
0177 Push(VM, Peek(VM)); // 读取栈顶并再次压入
0178 Inc(VM.PC);
0179 end;
0180
0181 Ord(OP_SWAP):
0182 begin
0183 // 交换栈顶两元素:例如栈为[5, 3](3是栈顶),执行后变为[3, 5]
0184 A := Pop(VM); // A = 原栈顶
0185 B := Pop(VM); // B = 原次栈顶
0186 Push(VM, A); // 新栈顶= 原栈顶
0187 Push(VM, B); // 新次栈顶 = 原次栈顶
0188 Inc(VM.PC);
0189 end;
0190
0191 Ord(OP_PRINT):
0192 begin
0193 // 打印当前栈顶值(常用于调试或查看计算结果)
0194 WriteLn('Output: ', Peek(VM));
0195 Inc(VM.PC);
0196 end;
0197
0198 Ord(OP_HALT):
0199 begin
0200 VM.Halted := True; // 设置停机标志,主循环将在下次判断时退出
0201 end;
0202
0203 else
0204 // 异常处理:遇到未定义的操作码
0205 raise Exception.CreateFmt('Unknown opcode: %d at PC=%d', [Opcode, VM.PC]);
0206 end;
0207 end;
0208 end;
0209
0210 // 加载程序:将字节码数组复制到VM的Code存储区
0211 procedure LoadProgram(var VM: TVM; const ProgramCode: array of Integer);
0212 var
0213 I: Integer;
0214 begin
0215 for I := Low(ProgramCode) to High(ProgramCode) do
0216 if I < CODE_SIZE then // 安全检查,防止超出VM容量
0217 VM.Code[I] := ProgramCode[I];
0218 end;
0219
0220 // 示例1:算术运算5 + 3 * 2 = 11
0221 // 字节码逻辑:先压入5,再压入3和2,执行MUL(3*2=6),再执行ADD(5+6=11)
0222 procedure Example_Arithmetic;
0223 var
0224 VM: TVM;
0225 const
0226 // 字节码内存布局(地址: 内容):
0227 // 00: OP_PUSH
0228 // 01: 5
0229 // 02: OP_PUSH
0230 // 03: 3
0231 // 04: OP_PUSH
0232 // 05: 2
0233 // 06: OP_MUL (弹出2和3,压入6)
0234 // 07: OP_ADD (弹出6和5,压入11)
0235 // 08: OP_PRINT (打印栈顶11)
0236 // 09: OP_HALT (停止)
0237 // 10: 0 (填充)
0238 // 11: 0 (填充)
0239 Prog: array[0..11] of Integer = (
0240 Ord(OP_PUSH), 5,
0241 Ord(OP_PUSH), 3,
0242 Ord(OP_PUSH), 2,
0243 Ord(OP_MUL),
0244 Ord(OP_ADD),
0245 Ord(OP_PRINT),
0246 Ord(OP_HALT),
0247 0, 0); // 填充对齐,无实际作用
0248
0249 begin
0250 InitVM(VM);
0251 LoadProgram(VM, Prog); // 将字节码加载到VM
0252 RunVM(VM); // 启动虚拟机执行
0253 end;
0254
0255 // 示例2:条件跳转演示
0256 // 逻辑:计算5-5=0,如果结果为0则跳转到打印999,否则打印888
0257 procedure Example_Jump;
0258 var
0259 VM: TVM;
0260 const
0261 // 字节码内存布局与执行流程:
0262 // 地址00: OP_PUSH 5 -> 栈:[5]
0263 // 地址02: OP_PUSH 5 -> 栈:[5,5]
0264 // 地址04: OP_SUB -> 弹出5,5,计算5-5=0,压入0 -> 栈:[0]
0265 // 地址05: OP_JZ 11 -> 弹出0(为真),跳转到地址11
0266 // 地址07: OP_PUSH 888 -> (被跳过,不执行)
0267 // 地址09: OP_PRINT -> (被跳过,不执行)
0268 // 地址10:Ord(OP_HALT),-> (被跳过,不执行)
0269 // 地址11: OP_PUSH 999 -> (跳转到这里)压入999 -> 栈:[999]
0270 // 地址13: OP_PRINT -> 打印999
0271 // 地址14: OP_HALT -> 停止
0272 Prog: array[0..14] of Integer = (
0273 Ord(OP_PUSH), 5, // 地址0-1
0274 Ord(OP_PUSH), 5, // 地址2-3
0275 Ord(OP_SUB), // 地址4:栈顶变为0
0276 Ord(OP_JZ), 11, // 地址5-6:JZ操作数11是跳转目标地址
0277 Ord(OP_PUSH), 888, // 地址7-8(未执行)
0278 Ord(OP_PRINT), // 地址9(未执行)
0279 Ord(OP_HALT), // 地址10(未执行)
0280 // 跳转目标地址11从这里开始:
0281 Ord(OP_PUSH), 999, // 地址11-12
0282 Ord(OP_PRINT), // 地址13
0283 Ord(OP_HALT) // 地址14
0284 );
0285 begin
0286 InitVM(VM);
0287 LoadProgram(VM, Prog);
0288 RunVM(VM);
0289 end;
0290
0291 begin
0292 try
0293 WriteLn('=== Delphi VM字节码演示===');
0294 WriteLn;
0295
0296 WriteLn('--- 算术运算 (5 + 3 * 2) ---');
0297 Example_Arithmetic;
0298
0299 WriteLn;
0300 WriteLn('--- 条件跳转测试 (5-5=0 跳转到打印999) ---');
0301 Example_Jump;
0302
0303 WriteLn;
0304 WriteLn('执行完成');
0305 ReadLn; // 防止控制台窗口关闭
0306
0307 except
0308 on E: Exception do
0309 begin
0310 WriteLn('错误: ', E.Message);
0311 ReadLn;
0312 end;
0313 end;
0314 end.
二、代码解读与执行流程分析
下面我们按照“整体流程 → 核心执行循环 → 示例执行路径”的顺序来梳理这个 SimpleVM 的工作机制。
整体流程
- 定义指令集与虚拟机结构:定义了操作码枚举
TOpCode、虚拟机核心结构 TVM(包含代码区、栈、程序计数器PC、栈指针SP、停机标志)。
- 初始化 VM:调用
InitVM 过程,将 PC 置 0,SP 置 -1(表示空栈),并清空栈和代码区。
- 加载程序:
LoadProgram 过程将我们编写好的字节码数组复制到 VM.Code 存储区。
- 运行解释器:进入
RunVM 过程的取指-译码-执行主循环。
- 主程序:依次调用两个示例函数,并输出结果。
核心执行循环(RunVM)
这是虚拟机的“心脏”,它不断循环直到遇到 HALT 指令:
- 取指 (Fetch):先检查
PC 是否越界,然后从 Code[PC] 读取当前指令的操作码。
- 译码与执行 (Decode & Execute):通过一个大的
case 语句,根据操作码跳转到对应的执行逻辑。这是理解计算机基础中指令执行模型的关键部分。
- 典型指令行为:
- PUSH:读取紧随其后的操作数
Code[PC+1],将其压栈,然后 PC 增加 2(跳过指令和操作数)。
- ADD/SUB/MUL/DIV:连续弹出栈顶两个数,进行计算,将结果压栈,
PC 增加 1。
- JMP/JZ:读取目标地址,
JMP直接修改PC实现跳转;JZ则先弹出栈顶判断是否为0,再决定是跳转还是顺序执行。
- PRINT:查看并打印栈顶值,但不改变栈。
- HALT:设置停机标志,循环将在下次判断时退出。
示例1:算术运算 5 + 3 * 2
我们来一步步跟踪虚拟机的执行状态(假设初始PC=0, SP=-1):
PC=0: 执行 PUSH 5 -> 栈变为 [5], PC 跳到 2。
PC=2: 执行 PUSH 3 -> 栈 [5, 3], PC=4。
PC=4: 执行 PUSH 2 -> 栈 [5, 3, 2], PC=6。
PC=6: 执行 MUL -> 弹出 2 和 3,计算 3*2=6 并压回,栈变为 [5, 6], PC=7。
PC=7: 执行 ADD -> 弹出 6 和 5,计算 5+6=11 并压回,栈 [11], PC=8。
PC=8: 执行 PRINT -> 输出 "Output: 11", PC=9。
PC=9: 执行 HALT -> 设置停机标志,循环结束。
示例2:条件跳转
这个例子演示了控制流是如何实现的:
PC=0: PUSH 5 -> 栈 [5], PC=2。
PC=2: PUSH 5 -> 栈 [5,5], PC=4。
PC=4: SUB -> 弹出 5和5,计算5-5=0并压入,栈 [0], PC=5。
PC=5: JZ 11 -> 弹出栈顶值 0,判断为真(等于0),因此 PC 被直接设置为操作数 11,实现跳转。
PC=11: (跳转到这里)PUSH 999 -> 栈 [999], PC=13。
PC=13: PRINT -> 输出 "Output: 999", PC=14。
PC=14: HALT -> 停机。
可以看到,地址7到10的指令(PUSH 888, PRINT, HALT)因为跳转而从未被执行。
三、核心总结与扩展思路
通过这个简单的实现,我们可以提炼出关于字节码虚拟机的几个核心知识点:
字节码结构
- 指令编码:使用枚举定义操作码,运行时转换为整数存储。这是一种清晰且易于扩展的设计。
- 指令格式:主要有单字指令(如
ADD、HALT)和双字指令(如PUSH imm、JMP target)。双字指令的第二“字”存放的是立即数或目标地址。
- 存储载体:字节码被存放在一个整数数组
VM.Code 中,指令和操作数混合存放。
执行模型
- 栈式架构:所有运算都通过一个后进先出(LIFO)的操作数栈完成。运算指令如
ADD会消费栈顶的两个操作数,并将结果压回栈顶。
- PC驱动:程序计数器
PC指向下一条待执行指令的地址。执行完一条指令后,PC会根据指令长度自动递增,或被跳转指令直接修改。
- 经典循环:
RunVM过程完美诠释了“取指-译码-执行”这个在计算机基础中至关重要的CPU工作模型。
控制流与异常处理
- 跳转指令:
JMP实现无条件跳转,JZ实现条件跳转。注意JZ会消费(弹出)栈顶值用于判断。
- 安全检查:实现中包含了
PC越界、栈溢出/下溢、除零等常见运行时检查,确保了虚拟机的健壮性。
如何扩展这个虚拟机?
如果你想为这个VM增加新功能,比如支持按位与(AND)操作,遵循以下流程即可:
- 定义指令:在
TOpCode 枚举中添加 OP_AND。
- 实现逻辑:在
RunVM 的 case 语句中增加 Ord(OP_AND) 的分支,实现弹出两个数、进行按位与运算、结果压栈的逻辑,并正确更新PC。
- 使用指令:在编写字节码程序时,就可以使用新的
OP_AND 指令了。
一个重要的注意事项:引入新指令时,必须严格保持一致的长度概念。如果新指令是双字或三字指令,那么在case分支中更新PC的逻辑必须匹配,否则会导致后续指令解析错位。
通过这个从零搭建的简易虚拟机,我们不仅理解了字节码的执行原理,也掌握了扩展它的基本方法。希望这篇实践指南能为你后续探索更复杂的虚拟机或进行相关的逆向工程分析打下坚实的基础。欢迎在云栈社区分享你的实践心得或提出更深入的问题。