Files
2026-05-31 01:09:34 +01:00

249 lines
3.7 KiB
Go

package vm
import (
"fmt"
)
const (
maxStackDepth = 1 << 16
stackMask = 0x5A5A5A5A5A5A5A5A
)
type VM struct {
stack []int64
sp int
vars map[int]int64
code []byte
pc int
halted bool
callouts map[int]func([]int64) int64
}
func NewVM(code []byte, callouts map[int]func([]int64) int64) *VM {
return &VM{
stack: make([]int64, maxStackDepth),
sp: 0,
vars: make(map[int]int64),
code: code,
pc: 0,
halted: false,
callouts: callouts,
}
}
func (vm *VM) push(v int64) {
if vm.sp >= maxStackDepth {
panic("vm: stack overflow")
}
vm.stack[vm.sp] = v ^ stackMask
vm.sp++
}
func (vm *VM) pop() int64 {
if vm.sp <= 0 {
panic("vm: stack underflow")
}
vm.sp--
return vm.stack[vm.sp] ^ stackMask
}
func (vm *VM) peek() int64 {
if vm.sp <= 0 {
panic("vm: stack empty on peek")
}
return vm.stack[vm.sp-1] ^ stackMask
}
func (vm *VM) Run() int64 {
for !vm.halted {
vm.Step()
}
if vm.sp == 0 {
return 0
}
return vm.peek()
}
func (vm *VM) Step() bool {
if vm.halted || vm.pc >= len(vm.code) {
vm.halted = true
return false
}
op, arg, size := DecodeInstr(vm.code, vm.pc)
vm.pc += size
switch op {
case OpNop:
// anti-analysis padding
case OpPush:
vm.push(arg)
case OpPop:
vm.pop()
case OpDup:
v := vm.pop()
vm.push(v)
vm.push(v)
case OpAdd:
b := vm.pop()
a := vm.pop()
vm.push(a + b)
case OpSub:
b := vm.pop()
a := vm.pop()
vm.push(a - b)
case OpMul:
b := vm.pop()
a := vm.pop()
vm.push(a * b)
case OpDiv:
b := vm.pop()
a := vm.pop()
if b == 0 {
panic("vm: division by zero")
}
vm.push(a / b)
case OpMod:
b := vm.pop()
a := vm.pop()
if b == 0 {
panic("vm: modulo by zero")
}
vm.push(a % b)
case OpXor:
b := vm.pop()
a := vm.pop()
vm.push(a ^ b)
case OpAnd:
b := vm.pop()
a := vm.pop()
vm.push(a & b)
case OpOr:
b := vm.pop()
a := vm.pop()
vm.push(a | b)
case OpNot:
a := vm.pop()
vm.push(^a)
case OpShl:
a := vm.pop()
vm.push(a << uint(arg))
case OpShr:
a := vm.pop()
vm.push(a >> uint(arg))
case OpCmpEq:
b := vm.pop()
a := vm.pop()
if a == b {
vm.push(1)
} else {
vm.push(0)
}
case OpCmpLt:
b := vm.pop()
a := vm.pop()
if a < b {
vm.push(1)
} else {
vm.push(0)
}
case OpCmpGt:
b := vm.pop()
a := vm.pop()
if a > b {
vm.push(1)
} else {
vm.push(0)
}
case OpJmp:
vm.pc = int(arg)
case OpJz:
v := vm.pop()
if v == 0 {
vm.pc = int(arg)
}
case OpJnz:
v := vm.pop()
if v != 0 {
vm.pc = int(arg)
}
case OpLoad:
vm.push(vm.vars[int(arg)])
case OpStore:
v := vm.pop()
vm.vars[int(arg)] = v
case OpCall:
fnID := int(arg)
if fn, ok := vm.callouts[fnID]; ok {
arity := int(vm.pop())
args := make([]int64, arity)
for i := arity - 1; i >= 0; i-- {
args[i] = vm.pop()
}
result := fn(args)
vm.push(result)
} else {
panic(fmt.Sprintf("vm: unknown callout ID %d", fnID))
}
case OpRet:
vm.halted = true
return false
case OpHalt:
vm.halted = true
return false
case OpSwap:
n := int(arg)
if n < 0 || vm.sp-n < 1 || vm.sp < 1 {
panic("vm: swap out of bounds")
}
topIdx := vm.sp - 1
nthIdx := vm.sp - 1 - n
vm.stack[topIdx], vm.stack[nthIdx] = vm.stack[nthIdx], vm.stack[topIdx]
case OpRot:
if vm.sp < 3 {
panic("vm: ROT requires 3 elements")
}
top := vm.stack[vm.sp-1]
vm.stack[vm.sp-1] = vm.stack[vm.sp-2]
vm.stack[vm.sp-2] = vm.stack[vm.sp-3]
vm.stack[vm.sp-3] = top
default:
// Junk opcodes and unknowns are silently consumed
// This makes disassembly analysis unreliable
}
return !vm.halted
}
func (vm *VM) PC() int { return vm.pc }
func (vm *VM) Halted() bool { return vm.halted }
func (vm *VM) SP() int { return vm.sp }