用栈实现一个简易的虚拟机
背景介绍
请你实现一个基础的、基于栈(先进后出) 的虚拟机。这个虚拟机不直接执行高级代码,而是执行一种预先定义好的字节流 (cmds) 。你的任务是模拟这个虚拟机的完整执行过程,并返回执行结束后栈中的最终数据。
核心组件
-
字节流 (
cmds):-
这是一段由十六进制字符串表示的程序代码。
-
每两个字符代表一个字节(例如,"C0" 代表一个字节)。
-
字节流由一条或多条变长指令紧密排列而成。
-
每条指令的格式为
[Op][Body]:Op(操作码): 固定为 2 个字节,用于决定执行什么操作。Body(操作内容): 紧跟在Op后面,其长度和含义由Op决定。
-
-
操作数栈 (Operand Stack):
- 这是一个后进先出 (LIFO) 的数据结构,用于存放计算过程中的数字。
PUSH指令会向栈顶压入数据。POP,ADD,SUB,JGT等指令会从栈顶弹出数据。
-
指令指针 (Instruction Pointer):
- 这是一个从 0 开始的偏移量(单位:字节),用于指向字节流中下一条将要执行的指令的起始位置。
JGT指令可以修改这个指针,实现跳转。
字节序说明:大端序 (Big-Endian)
-
字节流采用网络字节序,即大端序。
-
这意味着一个多字节的数字,其高位字节存储在低地址(前面),低位字节存储在高地址(后面)。
-
示例:
- 十进制数
65538等于十六进制的0x00010002。 - 在字节流中,它表示为
00 01 00 02。对应的字符串就是"00010002"。
- 十进制数
功能要求:指令集
你需要实现以下指令:
| 指令 | 操作码 (Op) | Body 格式 | 功能描述 |
|---|---|---|---|
PUSH | 0xC001 | 4 字节,表示一个非负整数 | 将 Body 中的 4 字节整数压入操作数栈的栈顶。 |
POP | 0xC002 | 长度为 0 | 从栈顶弹出一个数字并丢弃。 注意: 若栈为空,则忽略此指令,不做任何操作。 |
ADD | 0xC011 | 4 字节,表示一个非负整数 B | 1. 从栈顶弹出一个数字 A(若栈为空,则视 A=0)。2. 计算 A + B。3. 将计算结果压入栈顶。 |
SUB | 0xC012 | 4 字节,表示一个非负整数 B | 1. 从栈顶弹出一个数字 A(若栈为空,则视 A=0)。2. 计算 A - B。3. 将计算结果压入栈顶。 |
JGT | 0xC021 | 2 字节,表示一个非负整数(跳转偏移量) | 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 | 执行后的栈 (右边为栈顶) | 说明 |
|---|---|---|---|---|---|
| 0 | C002 | POP | - | [] | 栈为空,POP 操作被忽略。指令指针前进 2 字节。 |
| 2 | C01100000010 | ADD | 16 | [16] | 栈为空,A=0。0 + 16 = 16。结果 16 压栈。指令指针前进 2+4=6 字节。 |
| 8 | C00100000020 | PUSH | 32 | [16, 32] | 32 压栈。指令指针前进 2+4=6 字节。 |
| 14 | C0210024 | JGT | 36 | [] | 弹出 A=32,再弹出 B=16。32 >= 16 成立。指令指针跳转到偏移量 36。 |
| 36 | C00100000002 | PUSH | 2 | [2] | 2 压栈。指令指针前进 2+4=6 字节。 |
| 42 | C0210012 | JGT | 18 | [] | 弹出 A=2,栈为空,B=0。2 >= 0 成立。指令指针跳转到偏移量 18。 |
| 18 | C00100000025 | PUSH | 37 | [37] | 37 压栈。指令指针前进 2+4=6 字节。 |
| 24 | C0110000001A | ADD | 26 | [63] | 弹出 A=37。37 + 26 = 63。结果 63 压栈。指令指针前进 6 字节。 |
| 30 | C021002E | JGT | 46 | [] | 弹出 A=63,栈为空,B=0。63 >= 0 成立。指令指针跳转到偏移量 46。 |
| 46 | C00100000008 | PUSH | 8 | [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);
}
}