文章总结: 文档通过Pascal代码实现简易栈式虚拟机,演示字节码定义、栈管理及取指译码执行循环。结合算术与跳转示例解析指令执行路径,总结了VM架构原理与指令扩展方法,旨在加深对逆向分析中虚拟机技术的理解。 综合评分: 90 文章分类: 逆向分析,二进制安全
“逆向VM字节码程序”的学习(三)
原创
MicroPest MicroPest
MicroPest
2026年1月31日 16:21 安徽
本篇文章,我们来编写一份VM字节码运用的代码,加深对字节码地理解与运用技巧。共分为三个部分,一是带2个例子的代码程序;二是代码的解读;三是总结。
一、用pascal写一个VM字节码的程序
先来看下运行后的结果图,
如下附上pascal代码,加深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 的过程,并附上对应代码位置,便于你定位细节。
整体流程
-
定义指令集与虚拟机结构:枚举操作码、栈与字节码存储、PC/SP/停机标志等在 SimpleVM.dpr:L8-L39。
-
初始化 VM:PC 置 0、SP 置 -1、清零栈与代码区在 InitVM。
-
加载程序:把字节码数组复制到 VM.Code 在 LoadProgram。
-
运行解释器:进入取指-译码-执行循环,在 RunVM。
-
主程序:依次运行两个示例,并输出结果在 main。
核心执行循环(RunVM)
-
取指:先检查 PC 越界,再从 Code[PC] 取出指令码 RunVM:L85-L95。
-
译码执行:用 case 根据操作码执行逻辑 RunVM:L96-L206。
-
典型指令行为:
– PUSH:读取操作数 Code[PC+1],压栈,PC += 2 RunVM:L103-L110
– ADD/SUB/MUL/DIV:弹出两个数,计算后压栈,PC += 1 RunVM:L118-L151
– JMP/JZ:读取目标地址,按条件修改 PC RunVM:L153-L172
– PRINT:输出栈顶但不出栈 RunVM:L191-L196
– HALT:设置停机标志,循环结束 RunVM:L198-L201
示例1:算术运算 5 + 3 * 2
-
字节码布局与意图在 Example_Arithmetic
-
执行路径(按 PC):
– 0: PUSH 5 → 栈 [5]
– 2: PUSH 3 → 栈 [5,3]
– 4: PUSH 2 → 栈 [5,3,2]
– 6: MUL → 弹出 2 和 3,压入 6 → 栈 [5,6]
– 7: ADD → 弹出 6 和 5,压入 11 → 栈 [11]
– 8: PRINT → 输出 11
– 9: HALT → 停机
示例2:条件跳转
-
目标逻辑:5-5=0 时跳转打印 999,否则打印 888,在 Example_Jump
-
执行路径:
– 0: PUSH 5 → 栈 [5]
– 2: PUSH 5 → 栈 [5,5]
– 4: SUB → 5-5=0 → 栈 [0]
– 5: JZ 11 → 弹出 0,条件成立,PC 跳到 11
– 10: HALT
– 11: PUSH 999
三、总结
下面是针对这个 SimpleVM 字节码实现的学习总结,聚焦“字节码如何表示、如何执行、如何扩展”,并附上关键代码定位,便于回看。
字节码结构
-
指令编码:用 TOpCode 枚举定义操作码,运行时用 Ord(OP_*)转为整数存入字节码数组 SimpleVM.dpr:L8-L25。
-
指令格式:多数是“单字指令”,但 PUSH/JMP/JZ` 是“双字指令(操作码+立即数/目标地址)RunVM。
-
字节码载体:VM.Code 是 Integer 数组,指令与操作数混合存放 TVM。
执行模型
-
栈式架构:所有运算通过操作数栈完成(LIFO),由 Push/Pop/Peek 操作 Push/Pop/Peek。
-
PC 驱动:PC 指向下一条指令,执行后按指令长度移动或跳转 RunVM。
-
取指-译码-执行循环:RunVM 中对 PC 取指、case 译码并执行,直到 HALT RunVM。
控制流与异常
-
无条件跳转:JMP 直接改 PC 到目标地址 RunVM:L153-L158。
-
条件跳转:JZ 会先弹栈判断为 0 再决定跳转 RunVM:L161-L171。
-
安全检查:包括 PC 越界、栈上溢/下溢、除零等 RunVM + Push/Pop。
示例字节码的学习点
-
算术示例:先把操作数压栈,再执行 MUL和ADD,体现栈式 VM “后入先算”特性 Example_Arithmetic。
-
条件跳转示例:JZ 会消费栈顶,说明条件判断通常会“用掉”值 Example_Jump。
扩展字节码的思路
-
新增指令的流程:在 TOpCode 添加枚举 → 在 RunVM case 中实现执行逻辑 → 在程序字节码里使用新指令 TOpCode + RunVM。
-
与格式相关的注意点:一旦引入新指令长度(比如三字指令),PC 移动逻辑必须一致,否则会“错位执行”。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:MicroPest MicroPest MicroPest《“逆向VM字节码程序”的学习(三)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论