用栈实现一个简易的虚拟机

159 阅读9分钟

用栈实现一个简易的虚拟机

背景介绍

请你实现一个基础的、基于栈(先进后出) 的虚拟机。这个虚拟机不直接执行高级代码,而是执行一种预先定义好的字节流 (cmds) 。你的任务是模拟这个虚拟机的完整执行过程,并返回执行结束后栈中的最终数据。

核心组件

  1. 字节流 (cmds):

    • 这是一段由十六进制字符串表示的程序代码。

    • 每两个字符代表一个字节(例如,"C0" 代表一个字节)。

    • 字节流由一条或多条变长指令紧密排列而成。

    • 每条指令的格式为 [Op][Body]

      • Op (操作码): 固定为 2 个字节,用于决定执行什么操作。
      • Body (操作内容): 紧跟在 Op 后面,其长度和含义由 Op 决定。
  2. 操作数栈 (Operand Stack):

    • 这是一个后进先出 (LIFO) 的数据结构,用于存放计算过程中的数字。
    • PUSH 指令会向栈顶压入数据。
    • POP, ADD, SUB, JGT 等指令会从栈顶弹出数据。
  3. 指令指针 (Instruction Pointer):

    • 这是一个从 0 开始的偏移量(单位:字节),用于指向字节流中下一条将要执行的指令的起始位置。
    • JGT 指令可以修改这个指针,实现跳转。

字节序说明:大端序 (Big-Endian)

  • 字节流采用网络字节序,即大端序

  • 这意味着一个多字节的数字,其高位字节存储在低地址(前面),低位字节存储在高地址(后面)。

  • 示例:

    • 十进制数 65538 等于十六进制的 0x00010002
    • 在字节流中,它表示为 00 01 00 02。对应的字符串就是 "00010002"

功能要求:指令集

你需要实现以下指令:

指令操作码 (Op)Body 格式功能描述
PUSH0xC0014 字节,表示一个非负整数将 Body 中的 4 字节整数压入操作数栈的栈顶。
POP0xC002长度为 0从栈顶弹出一个数字并丢弃。 注意: 若栈为空,则忽略此指令,不做任何操作。
ADD0xC0114 字节,表示一个非负整数 B1. 从栈顶弹出一个数字 A(若栈为空,则视 A=0)。
2. 计算 A + B
3. 将计算结果压入栈顶。
SUB0xC0124 字节,表示一个非负整数 B1. 从栈顶弹出一个数字 A(若栈为空,则视 A=0)。
2. 计算 A - B
3. 将计算结果压入栈顶。
JGT0xC0212 字节,表示一个非负整数(跳转偏移量)1. 从栈顶弹出一个数字 A(若栈为空,则视 A=0)。
2. 再次从栈顶弹出一个数字 B(若栈为空,则视 B=0)。
3. 比较大小: 如果 A >= B,则将指令指针跳转到字节流中偏移量为 Body 值的位置。 4. 否则,顺序执行下一条指令。

输入格式

  • 一个字符串 cmds,表示十六进制字节流。

    • 1 <= cmds.length <= 50000,且长度为偶数。
  • 用例保证:

    • 输入合法,操作数和运算符符合规则。
    • 栈最大深度不超过 64。
    • 计算过程和结果的范围在 [-2^40, 2^40] 内(提示需要使用 long 类型)。
    • 跳转指令不会导致死循环,且跳转位置合法。

输出格式

  • 一个整数序列,表示最终从栈底到栈顶的数据。

样例

输入样例 1

"C002C01100000010C00100000020C0210024C00100000025C0110000001AC021002EC002C00100000002C0210012C00100000008"

输出样例 1

[8]

样例 1 执行流程详解

偏移量字节码指令Body执行后的栈 (右边为栈顶)说明
0C002POP-[]栈为空,POP 操作被忽略。指令指针前进 2 字节。
2C01100000010ADD16[16]栈为空,A=00 + 16 = 16。结果 16 压栈。指令指针前进 2+4=6 字节。
8C00100000020PUSH32[16, 32]32 压栈。指令指针前进 2+4=6 字节。
14C0210024JGT36[]弹出 A=32,再弹出 B=1632 >= 16 成立。指令指针跳转到偏移量 36。
36C00100000002PUSH2[2]2 压栈。指令指针前进 2+4=6 字节。
42C0210012JGT18[]弹出 A=2,栈为空,B=02 >= 0 成立。指令指针跳转到偏移量 18。
18C00100000025PUSH37[37]37 压栈。指令指针前进 2+4=6 字节。
24C0110000001AADD26[63]弹出 A=3737 + 26 = 63。结果 63 压栈。指令指针前进 6 字节。
30C021002EJGT46[]弹出 A=63,栈为空,B=063 >= 0 成立。指令指针跳转到偏移量 46。
46C00100000008PUSH8[8]8 压栈。指令指针前进 6 字节。
52(程序结束)--[8]没有更多指令。

最终,栈中只剩下一个数字 8。从栈底到栈顶输出为 [8]

import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.Scanner;

/**
 * 主类,用于解决“基于栈的虚拟机”问题。
 */
public class Main {

    /**
     * ACM 模式的主入口。
     * @param args 命令行参数(未使用)
     */
    public static void main(String[] args) {
        // 创建 Scanner 对象以从标准输入读取数据
        Scanner in = new Scanner(System.in);
        // 读取代表字节码的完整十六进制字符串
        String cmds = in.nextLine();
        // 关闭 scanner
        in.close();

        // 调用虚拟机执行引擎
        long[] result = executeVM(cmds);
        
        // 按题目要求的格式 "[a, b, c]" 输出最终栈的内容
        System.out.println(Arrays.toString(result));
    }

    /**
     * 模拟一个基于栈的虚拟机,执行给定的字节码。
     *
     * @param cmds 十六进制字符串表示的字节码指令流。
     * @return 一个 long 数组,表示执行完毕后,栈中从底到顶的数据。
     */
    public static long[] executeVM(String cmds) {
        // 将输入的十六进制字符串转换为字节数组
        byte[] bytecode = hexToBytes(cmds);
        // 使用 Deque (双端队列) 作为操作数栈,存储 long 类型以满足题目范围要求
        Deque<Long> stack = new ArrayDeque<>();
        // ip (Instruction Pointer) 指令指针,表示当前执行到的字节偏移量
        int ip = 0;

        // 主循环:只要指令指针没有超出字节码的范围,就继续执行
        while (ip < bytecode.length) {
            // 读取 2 字节作为操作码 (Opcode)
            int opcode = read2BytesAsInt(bytecode, ip);

            // 根据操作码执行相应指令
            switch (opcode) {
                case 0xC001: { // PUSH 指令
                    // 读取指令体中的 4 字节整数作为要压栈的值
                    long value = read4BytesAsLong(bytecode, ip + 2);
                    stack.push(value); // push 操作将元素压入栈顶
                    // 指令指针前进 6 字节 (2字节Opcode + 4字节Body)
                    ip += 6;
                    break;
                }
                case 0xC002: { // POP 指令
                    // 如果栈不为空,则从栈顶弹出一个数字并丢弃
                    if (!stack.isEmpty()) {
                        stack.pop();
                    }
                    // 指令指针前进 2 字节 (只有Opcode)
                    ip += 2;
                    break;
                }
                case 0xC011: { // ADD 指令
                    // 从栈顶弹出一个数字 A (若栈为空,则 A=0)
                    long a = stack.isEmpty() ? 0L : stack.pop();
                    // 读取指令体中的数字 B
                    long b = read4BytesAsLong(bytecode, ip + 2);
                    // 计算 A + B 并将结果压回栈顶
                    stack.push(a + b);
                    // 指令指针前进 6 字节
                    ip += 6;
                    break;
                }
                case 0xC012: { // SUB 指令
                    // 从栈顶弹出一个数字 A (若栈为空,则 A=0)
                    long a = stack.isEmpty() ? 0L : stack.pop();
                    // 读取指令体中的数字 B
                    long b = read4BytesAsLong(bytecode, ip + 2);
                    // 计算 A - B 并将结果压回栈顶
                    stack.push(a - b);
                    // 指令指针前进 6 字节
                    ip += 6;
                    break;
                }
                case 0xC021: { // JGT (Jump if Greater Than) 指令
                    // 从栈顶弹出第一个数字 A (若栈为空,则 A=0)
                    long a = stack.isEmpty() ? 0L : stack.pop();
                    // 再次从栈顶弹出第二个数字 B (若栈为空,则 B=0)
                    long b = stack.isEmpty() ? 0L : stack.pop();
                    // 读取指令体中的 2 字节跳转偏移量
                    int offset = read2BytesAsInt(bytecode, ip + 2);
                    
                    // 比较 A 和 B
                    if (a >= b) {
                        // 如果 A >= B,则跳转到指定的偏移量
                        ip = offset;
                    } else {
                        // 否则,顺序执行下一条指令,指令指针前进 4 字节 (2字节Opcode + 2字节Body)
                        ip += 4;
                    }
                    break;
                }
                default:
                    // 遇到未知指令,题目保证输入合法,此处可视为异常情况
                    System.err.println("错误:在偏移量 " + ip + " 遇到未知指令码: " + Integer.toHexString(opcode));
                    return new long[0]; // 返回空数组表示错误
            }
        }

        // --- 执行完毕,准备输出结果 ---
        // 将栈中数据按从底到顶的顺序转换为数组
        long[] result = new long[stack.size()];
        // Deque 的 descendingIterator() 方法提供了一个从队尾到队首的迭代器,
        // 对于用作栈的 Deque (push/pop 操作在头部),这正好是从栈底到栈顶的顺序。
        Iterator<Long> descendingIterator = stack.descendingIterator();
        int index = 0;
        while (descendingIterator.hasNext()) {
            result[index++] = descendingIterator.next();
        }
        return result;
    }

    // --- 辅助函数 ---

    /**
     * 将十六进制字符串转换为字节数组。
     * @param s 十六进制字符串,例如 "C001"
     * @return 对应的字节数组,例如 [0xC0, 0x01]
     */
    private static byte[] hexToBytes(String s) {
        int len = s.length();
        byte[] data = new byte[len / 2];
        // 每次处理两个十六进制字符,组成一个字节
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
                                 + Character.digit(s.charAt(i + 1), 16));
        }
        return data;
    }
    
    /**
     * 从字节数组的指定位置读取 4 个字节,并按大端序 (Big-Endian) 组合成一个 long。
     * @param arr 字节数组
     * @param pos 起始位置(偏移量)
     * @return 组合后的 long 值
     */
    private static long read4BytesAsLong(byte[] arr, int pos) {
        // Java 的 byte 是有符号的 (-128 到 127)。直接进行位运算时,如果字节的最高位是1,
        // 它会被当作负数处理,在类型提升到 int 或 long 时会进行“符号扩展”,用1填充高位,导致结果错误。
        // 使用 & 0xFF (对于 int) 或 & 0xFFL (对于 long) 可以屏蔽掉符号扩展,
        // 将 byte 安全地转换为 0-255 范围的无符号整数值,然后再进行位移。
        // 注意要转成long,因为要左移24位
        return ((long)(arr[pos] & 0xFF) << 24) |
               ((long)(arr[pos + 1] & 0xFF) << 16) |
               ((long)(arr[pos + 2] & 0xFF) << 8) |
               ((long)(arr[pos + 3] & 0xFF));
    }

    /**
     * 从字节数组的指定位置读取 2 个字节,并按大端序 (Big-Endian) 组合成一个 int。
     * @param arr 字节数组
     * @param pos 起始位置(偏移量)
     * @return 组合后的 int 值
     */
    private static int read2BytesAsInt(byte[] arr, int pos) {
        // 同样的,使用 & 0xFF 确保将 byte 作为无符号数处理
        return ((arr[pos] & 0xFF) << 8) | (arr[pos + 1] & 0xFF);
    }
}