找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2828

积分

0

好友

392

主题
发表于 9 小时前 | 查看: 3| 回复: 0

本文将带你亲手用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 的工作机制。

整体流程

  1. 定义指令集与虚拟机结构:定义了操作码枚举 TOpCode、虚拟机核心结构 TVM(包含代码区、栈、程序计数器PC、栈指针SP、停机标志)。
  2. 初始化 VM:调用 InitVM 过程,将 PC 置 0,SP 置 -1(表示空栈),并清空栈和代码区。
  3. 加载程序LoadProgram 过程将我们编写好的字节码数组复制到 VM.Code 存储区。
  4. 运行解释器:进入 RunVM 过程的取指-译码-执行主循环。
  5. 主程序:依次调用两个示例函数,并输出结果。

核心执行循环(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):

  1. PC=0: 执行 PUSH 5 -> 栈变为 [5], PC 跳到 2。
  2. PC=2: 执行 PUSH 3 -> 栈 [5, 3], PC=4
  3. PC=4: 执行 PUSH 2 -> 栈 [5, 3, 2], PC=6
  4. PC=6: 执行 MUL -> 弹出 23,计算 3*2=6 并压回,栈变为 [5, 6], PC=7
  5. PC=7: 执行 ADD -> 弹出 65,计算 5+6=11 并压回,栈 [11], PC=8
  6. PC=8: 执行 PRINT -> 输出 "Output: 11", PC=9
  7. PC=9: 执行 HALT -> 设置停机标志,循环结束。

示例2:条件跳转

这个例子演示了控制流是如何实现的:

  1. PC=0: PUSH 5 -> 栈 [5], PC=2
  2. PC=2: PUSH 5 -> 栈 [5,5], PC=4
  3. PC=4: SUB -> 弹出 55,计算5-5=0并压入,栈 [0], PC=5
  4. PC=5: JZ 11 -> 弹出栈顶值 0,判断为真(等于0),因此 PC 被直接设置为操作数 11,实现跳转。
  5. PC=11: (跳转到这里)PUSH 999 -> 栈 [999], PC=13
  6. PC=13: PRINT -> 输出 "Output: 999", PC=14
  7. PC=14: HALT -> 停机。

可以看到,地址7到10的指令(PUSH 888, PRINT, HALT)因为跳转而从未被执行。

三、核心总结与扩展思路

通过这个简单的实现,我们可以提炼出关于字节码虚拟机的几个核心知识点:

字节码结构

  • 指令编码:使用枚举定义操作码,运行时转换为整数存储。这是一种清晰且易于扩展的设计。
  • 指令格式:主要有单字指令(如ADDHALT)和双字指令(如PUSH immJMP target)。双字指令的第二“字”存放的是立即数或目标地址。
  • 存储载体:字节码被存放在一个整数数组 VM.Code 中,指令和操作数混合存放。

执行模型

  • 栈式架构:所有运算都通过一个后进先出(LIFO)的操作数栈完成。运算指令如ADD会消费栈顶的两个操作数,并将结果压回栈顶。
  • PC驱动:程序计数器PC指向下一条待执行指令的地址。执行完一条指令后,PC会根据指令长度自动递增,或被跳转指令直接修改。
  • 经典循环RunVM过程完美诠释了“取指-译码-执行”这个在计算机基础中至关重要的CPU工作模型。

控制流与异常处理

  • 跳转指令JMP实现无条件跳转,JZ实现条件跳转。注意JZ会消费(弹出)栈顶值用于判断。
  • 安全检查:实现中包含了PC越界、栈溢出/下溢、除零等常见运行时检查,确保了虚拟机的健壮性。

如何扩展这个虚拟机?

如果你想为这个VM增加新功能,比如支持按位与(AND)操作,遵循以下流程即可:

  1. 定义指令:在 TOpCode 枚举中添加 OP_AND
  2. 实现逻辑:在 RunVMcase 语句中增加 Ord(OP_AND) 的分支,实现弹出两个数、进行按位与运算、结果压栈的逻辑,并正确更新PC
  3. 使用指令:在编写字节码程序时,就可以使用新的 OP_AND 指令了。

一个重要的注意事项:引入新指令时,必须严格保持一致的长度概念。如果新指令是双字或三字指令,那么在case分支中更新PC的逻辑必须匹配,否则会导致后续指令解析错位。

通过这个从零搭建的简易虚拟机,我们不仅理解了字节码的执行原理,也掌握了扩展它的基本方法。希望这篇实践指南能为你后续探索更复杂的虚拟机或进行相关的逆向工程分析打下坚实的基础。欢迎在云栈社区分享你的实践心得或提出更深入的问题。




上一篇:Spring Boot实战:基于AOP与Guava注解限流,轻松应对高并发场景防系统过载
下一篇:嵌入式开发痛点:为何换芯片就要重写驱动?分层架构设计实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-3 18:23 , Processed in 0.355666 second(s), 38 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表