“逆向VM字节码程序”的学习(三)

admin 2026-01-31 23:37:34 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文档通过Pascal代码实现简易栈式虚拟机,演示字节码定义、栈管理及取指译码执行循环。结合算术与跳转示例解析指令执行路径,总结了VM架构原理与指令扩展方法,旨在加深对逆向分析中虚拟机技术的理解。 综合评分: 90 文章分类: 逆向分析,二进制安全


cover_image

“逆向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字节码程序”的学习(三)》

评论:0   参与:  0