mirror of
https://github.com/lukaszraczylo/go-obfuscation.git
synced 2026-10-16 06:07:50 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Instruction struct {
|
||||
Op Opcode
|
||||
Args []int64
|
||||
}
|
||||
|
||||
type LabelledInstr struct {
|
||||
Label string
|
||||
Instr Instruction
|
||||
Target string
|
||||
}
|
||||
|
||||
func Compile(program []LabelledInstr) []byte {
|
||||
labelAddrs := make(map[string]int)
|
||||
|
||||
type fixup struct {
|
||||
offset int
|
||||
op Opcode
|
||||
target string
|
||||
}
|
||||
var fixups []fixup
|
||||
var bytecode []byte
|
||||
|
||||
for _, entry := range program {
|
||||
if entry.Label != "" {
|
||||
labelAddrs[entry.Label] = len(bytecode)
|
||||
}
|
||||
|
||||
instr := entry.Instr
|
||||
isJump := instr.Op == OpJmp || instr.Op == OpJz || instr.Op == OpJnz
|
||||
|
||||
if isJump && entry.Target != "" {
|
||||
fixups = append(fixups, fixup{
|
||||
offset: len(bytecode),
|
||||
op: instr.Op,
|
||||
target: entry.Target,
|
||||
})
|
||||
ph := make([]byte, InstrSize(instr.Op))
|
||||
ph[0] = encodeOp(instr.Op)
|
||||
bytecode = append(bytecode, ph...)
|
||||
continue
|
||||
}
|
||||
|
||||
arg := int64(0)
|
||||
if len(instr.Args) > 0 {
|
||||
arg = instr.Args[0]
|
||||
}
|
||||
bytecode = append(bytecode, EncodeInstr(instr.Op, arg)...)
|
||||
}
|
||||
|
||||
for _, f := range fixups {
|
||||
addr, ok := labelAddrs[f.target]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("vm: undefined label %q", f.target))
|
||||
}
|
||||
enc := EncodeInstr(f.op, int64(addr))
|
||||
copy(bytecode[f.offset:], enc)
|
||||
}
|
||||
|
||||
return bytecode
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Opcode byte
|
||||
|
||||
const (
|
||||
OpNop Opcode = 0x00
|
||||
OpPush Opcode = 0x01
|
||||
OpPop Opcode = 0x02
|
||||
OpDup Opcode = 0x03
|
||||
OpAdd Opcode = 0x04
|
||||
OpSub Opcode = 0x05
|
||||
OpMul Opcode = 0x06
|
||||
OpDiv Opcode = 0x07
|
||||
OpMod Opcode = 0x08
|
||||
OpXor Opcode = 0x09
|
||||
OpAnd Opcode = 0x0A
|
||||
OpOr Opcode = 0x0B
|
||||
OpNot Opcode = 0x0C
|
||||
OpShl Opcode = 0x0D
|
||||
OpShr Opcode = 0x0E
|
||||
OpCmpEq Opcode = 0x0F
|
||||
OpCmpLt Opcode = 0x10
|
||||
OpCmpGt Opcode = 0x11
|
||||
OpJmp Opcode = 0x12
|
||||
OpJz Opcode = 0x13
|
||||
OpJnz Opcode = 0x14
|
||||
OpLoad Opcode = 0x15
|
||||
OpStore Opcode = 0x16
|
||||
OpCall Opcode = 0x17
|
||||
OpRet Opcode = 0x18
|
||||
OpHalt Opcode = 0x19
|
||||
OpSwap Opcode = 0x1A
|
||||
OpRot Opcode = 0x1B
|
||||
opJunkA Opcode = 0x1C
|
||||
opJunkB Opcode = 0x1D
|
||||
opJunkC Opcode = 0x1E
|
||||
opJunkD Opcode = 0x1F
|
||||
)
|
||||
|
||||
const dispatchKey byte = 0xA5
|
||||
|
||||
var opcodeNames = map[Opcode]string{
|
||||
OpNop: "NOP", OpPush: "PUSH", OpPop: "POP", OpDup: "DUP",
|
||||
OpAdd: "ADD", OpSub: "SUB", OpMul: "MUL", OpDiv: "DIV",
|
||||
OpMod: "MOD", OpXor: "XOR", OpAnd: "AND", OpOr: "OR",
|
||||
OpNot: "NOT", OpShl: "SHL", OpShr: "SHR", OpCmpEq: "CMP_EQ",
|
||||
OpCmpLt: "CMP_LT", OpCmpGt: "CMP_GT", OpJmp: "JMP", OpJz: "JZ",
|
||||
OpJnz: "JNZ", OpLoad: "LOAD", OpStore: "STORE", OpCall: "CALL",
|
||||
OpRet: "RET", OpHalt: "HALT", OpSwap: "SWAP", OpRot: "ROT",
|
||||
}
|
||||
|
||||
func (o Opcode) String() string {
|
||||
if name, ok := opcodeNames[o]; ok {
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("UNK(0x%02X)", byte(o))
|
||||
}
|
||||
|
||||
func encodeOp(op Opcode) byte {
|
||||
return byte(op) ^ dispatchKey
|
||||
}
|
||||
|
||||
func decodeOp(b byte) Opcode {
|
||||
return Opcode(b ^ dispatchKey)
|
||||
}
|
||||
|
||||
type argEncoding int
|
||||
|
||||
const (
|
||||
argNone argEncoding = iota
|
||||
argI8
|
||||
argI16
|
||||
argI32
|
||||
argI64
|
||||
)
|
||||
|
||||
func opArgEnc(op Opcode) argEncoding {
|
||||
switch op {
|
||||
case OpPush:
|
||||
return argI64
|
||||
case OpShl, OpShr, OpSwap:
|
||||
return argI8
|
||||
case OpJmp, OpJz, OpJnz:
|
||||
return argI32
|
||||
case OpLoad, OpStore, OpCall:
|
||||
return argI16
|
||||
default:
|
||||
return argNone
|
||||
}
|
||||
}
|
||||
|
||||
func InstrSize(op Opcode) int {
|
||||
switch opArgEnc(op) {
|
||||
case argI64:
|
||||
return 9
|
||||
case argI32:
|
||||
return 5
|
||||
case argI16:
|
||||
return 3
|
||||
case argI8:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func EncodeInstr(op Opcode, arg int64) []byte {
|
||||
size := InstrSize(op)
|
||||
buf := make([]byte, size)
|
||||
buf[0] = encodeOp(op)
|
||||
|
||||
switch opArgEnc(op) {
|
||||
case argI64:
|
||||
binary.LittleEndian.PutUint64(buf[1:], uint64(arg))
|
||||
case argI32:
|
||||
binary.LittleEndian.PutUint32(buf[1:], uint32(arg))
|
||||
case argI16:
|
||||
binary.LittleEndian.PutUint16(buf[1:], uint16(arg))
|
||||
case argI8:
|
||||
buf[1] = byte(arg)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func DecodeInstr(code []byte, pc int) (Opcode, int64, int) {
|
||||
op := decodeOp(code[pc])
|
||||
size := InstrSize(op)
|
||||
|
||||
var arg int64
|
||||
switch opArgEnc(op) {
|
||||
case argI64:
|
||||
arg = int64(binary.LittleEndian.Uint64(code[pc+1 : pc+9]))
|
||||
case argI32:
|
||||
arg = int64(binary.LittleEndian.Uint32(code[pc+1 : pc+5]))
|
||||
case argI16:
|
||||
arg = int64(binary.LittleEndian.Uint16(code[pc+1 : pc+3]))
|
||||
case argI8:
|
||||
arg = int64(code[pc+1])
|
||||
}
|
||||
return op, arg, size
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
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 }
|
||||
@@ -0,0 +1,537 @@
|
||||
package vm
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestComputeExpr(t *testing.T) {
|
||||
// Compute (10 + 20) * 3 = 90
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{20}}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{3}}},
|
||||
{Instr: Instruction{Op: OpMul}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 90 {
|
||||
t.Errorf("expected 90, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubtraction(t *testing.T) {
|
||||
// 100 - 37 = 63
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{100}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{37}}},
|
||||
{Instr: Instruction{Op: OpSub}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 63 {
|
||||
t.Errorf("expected 63, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDivision(t *testing.T) {
|
||||
// 84 / 4 = 21
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{84}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{4}}},
|
||||
{Instr: Instruction{Op: OpDiv}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 21 {
|
||||
t.Errorf("expected 21, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestModulo(t *testing.T) {
|
||||
// 17 % 5 = 2
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{17}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{5}}},
|
||||
{Instr: Instruction{Op: OpMod}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 2 {
|
||||
t.Errorf("expected 2, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitwiseOps(t *testing.T) {
|
||||
// (0xFF XOR 0x0F) AND 0xF0 = 0xF0
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0xFF}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0x0F}}},
|
||||
{Instr: Instruction{Op: OpXor}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0xF0}}},
|
||||
{Instr: Instruction{Op: OpAnd}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 0xF0 {
|
||||
t.Errorf("expected 0xF0, got 0x%X", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftOps(t *testing.T) {
|
||||
// 1 SHL 8 = 256
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpShl, Args: []int64{8}}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 256 {
|
||||
t.Errorf("expected 256, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComparisons(t *testing.T) {
|
||||
// 5 < 10 → 1
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{5}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpCmpLt}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 1 {
|
||||
t.Errorf("expected 1, got %d", result)
|
||||
}
|
||||
|
||||
// 10 > 5 → 1
|
||||
program = []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{5}}},
|
||||
{Instr: Instruction{Op: OpCmpGt}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code = Compile(program)
|
||||
vm = NewVM(code, nil)
|
||||
result = vm.Run()
|
||||
|
||||
if result != 1 {
|
||||
t.Errorf("expected 1, got %d", result)
|
||||
}
|
||||
|
||||
// 7 == 7 → 1
|
||||
program = []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{7}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{7}}},
|
||||
{Instr: Instruction{Op: OpCmpEq}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code = Compile(program)
|
||||
vm = NewVM(code, nil)
|
||||
result = vm.Run()
|
||||
|
||||
if result != 1 {
|
||||
t.Errorf("expected 1, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJump(t *testing.T) {
|
||||
// JMP to "end", skipping the 999 push
|
||||
// PUSH 42
|
||||
// JMP end
|
||||
// PUSH 999 <- skipped
|
||||
// end: HALT
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{42}}},
|
||||
{Label: "", Instr: Instruction{Op: OpJmp}, Target: "end"},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{999}}},
|
||||
{Label: "end", Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 42 {
|
||||
t.Errorf("expected 42, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJzJnz(t *testing.T) {
|
||||
// JZ pops the test value; verify both branches work.
|
||||
//
|
||||
// Phase 1: JZ should take (value is 0)
|
||||
// PUSH 0, JZ zero, PUSH 999 (skipped), zero: PUSH 77
|
||||
// Phase 2: JNZ should take (value is 1)
|
||||
// DUP, JNZ nz, PUSH 888 (skipped), nz: HALT
|
||||
// Result: 77
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpJz}, Target: "zero"},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{999}}},
|
||||
{Label: "zero", Instr: Instruction{Op: OpPush, Args: []int64{77}}},
|
||||
{Instr: Instruction{Op: OpDup}},
|
||||
{Instr: Instruction{Op: OpJnz}, Target: "nz"},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{888}}},
|
||||
{Label: "nz", Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 77 {
|
||||
t.Errorf("expected 77, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStoreLoad(t *testing.T) {
|
||||
// PUSH 10
|
||||
// STORE 0 var[0] = 10
|
||||
// PUSH 20
|
||||
// STORE 1 var[1] = 20
|
||||
// LOAD 0
|
||||
// LOAD 1
|
||||
// ADD var[0] + var[1] = 30
|
||||
// HALT
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{20}}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpLoad, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpLoad, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 30 {
|
||||
t.Errorf("expected 30, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDup(t *testing.T) {
|
||||
// PUSH 5, DUP, MUL = 25
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{5}}},
|
||||
{Instr: Instruction{Op: OpDup}},
|
||||
{Instr: Instruction{Op: OpMul}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 25 {
|
||||
t.Errorf("expected 25, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSwap(t *testing.T) {
|
||||
// PUSH 1, PUSH 2, SWAP 1, SUB = 2 - 1 = 1
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{2}}},
|
||||
{Instr: Instruction{Op: OpSwap, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpSub}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 1 {
|
||||
t.Errorf("expected 1, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRot(t *testing.T) {
|
||||
// PUSH 1, PUSH 2, PUSH 3, ROT
|
||||
// Stack after ROT: [3, 1, 2] (top=2)
|
||||
// POP → 2, POP → 1, POP → 3
|
||||
// We verify by: PUSH 1, PUSH 2, PUSH 3, ROT, SUB, SUB = 3 - 1 - 2 ... wait
|
||||
// Actually ROT puts top to bottom: [1, 3, 2] with top = 2
|
||||
// Let me just check stack ordering
|
||||
// PUSH 10, PUSH 20, PUSH 30, ROT
|
||||
// Before: [10, 20, 30] top=30
|
||||
// After: top=20, then 10, then 30
|
||||
// So: POP=20, POP=10, POP=30 → 20 - 10 = 10, 10 - 30 = ... let me just add
|
||||
// PUSH 10, PUSH 20, PUSH 30, ROT, ADD, ADD = 20 + 10 + 30 = 60
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{20}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{30}}},
|
||||
{Instr: Instruction{Op: OpRot}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 60 {
|
||||
t.Errorf("expected 60, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNot(t *testing.T) {
|
||||
// NOT 0 = -1
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpNot}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != -1 {
|
||||
t.Errorf("expected -1, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCallout(t *testing.T) {
|
||||
// PUSH 7, PUSH 3, PUSH 2 (arity), CALL 42, HALT
|
||||
// callout(42) sums its args: 7 + 3 = 10
|
||||
callouts := map[int]func([]int64) int64{
|
||||
42: func(args []int64) int64 {
|
||||
return args[0] + args[1]
|
||||
},
|
||||
}
|
||||
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{7}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{3}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{2}}}, // arity
|
||||
{Instr: Instruction{Op: OpCall, Args: []int64{42}}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, callouts)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 10 {
|
||||
t.Errorf("expected 10, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoop(t *testing.T) {
|
||||
// Compute sum 1..5 = 15 using a loop
|
||||
// var[0] = accumulator = 0
|
||||
// var[1] = counter = 5
|
||||
// loop:
|
||||
// LOAD 1 push counter
|
||||
// JZ done
|
||||
// LOAD 0 push acc
|
||||
// LOAD 1 push counter
|
||||
// ADD acc + counter
|
||||
// STORE 0 acc = result
|
||||
// LOAD 1
|
||||
// PUSH 1
|
||||
// SUB counter - 1
|
||||
// STORE 1 counter = result
|
||||
// JMP loop
|
||||
// done:
|
||||
// LOAD 0
|
||||
// HALT
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{5}}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{1}}},
|
||||
{Label: "loop", Instr: Instruction{Op: OpLoad, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpJz}, Target: "done"},
|
||||
{Instr: Instruction{Op: OpLoad, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpLoad, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpLoad, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpSub}},
|
||||
{Instr: Instruction{Op: OpStore, Args: []int64{1}}},
|
||||
{Instr: Instruction{Op: OpJmp}, Target: "loop"},
|
||||
{Label: "done", Instr: Instruction{Op: OpLoad, Args: []int64{0}}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 15 {
|
||||
t.Errorf("expected 15, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNopPadding(t *testing.T) {
|
||||
// NOPs should be harmless padding
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpNop}},
|
||||
{Instr: Instruction{Op: OpNop}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{42}}},
|
||||
{Instr: Instruction{Op: OpNop}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 42 {
|
||||
t.Errorf("expected 42, got %d", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStepwiseExecution(t *testing.T) {
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{3}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{4}}},
|
||||
{Instr: Instruction{Op: OpMul}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
|
||||
steps := 0
|
||||
for vm.Step() {
|
||||
steps++
|
||||
}
|
||||
|
||||
if steps != 3 {
|
||||
t.Errorf("expected 3 steps, got %d", steps)
|
||||
}
|
||||
|
||||
if vm.Halted() != true {
|
||||
t.Error("expected VM to be halted")
|
||||
}
|
||||
|
||||
if vm.SP() != 1 {
|
||||
t.Errorf("expected SP=1, got %d", vm.SP())
|
||||
}
|
||||
}
|
||||
|
||||
func TestXorObfuscationDeterminism(t *testing.T) {
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{10}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{20}}},
|
||||
{Instr: Instruction{Op: OpAdd}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{3}}},
|
||||
{Instr: Instruction{Op: OpMul}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code1 := Compile(program)
|
||||
code2 := Compile(program)
|
||||
|
||||
for i := range code1 {
|
||||
if code1[i] != code2[i] {
|
||||
t.Fatalf("bytecode non-deterministic at offset %d", i)
|
||||
}
|
||||
}
|
||||
|
||||
vm1 := NewVM(code1, nil)
|
||||
vm2 := NewVM(code2, nil)
|
||||
|
||||
r1 := vm1.Run()
|
||||
r2 := vm2.Run()
|
||||
|
||||
if r1 != r2 {
|
||||
t.Errorf("results differ: %d vs %d", r1, r2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBytecodeIsXORed(t *testing.T) {
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{42}}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
|
||||
// First byte should be OpPush ^ dispatchKey, not OpPush itself
|
||||
rawPush := byte(OpPush) ^ dispatchKey
|
||||
if code[0] != rawPush {
|
||||
t.Errorf("expected bytecode[0]=0x%02X (XOR'd PUSH), got 0x%02X", rawPush, code[0])
|
||||
}
|
||||
|
||||
rawHalt := byte(OpHalt) ^ dispatchKey
|
||||
if code[9] != rawHalt {
|
||||
t.Errorf("expected bytecode[9]=0x%02X (XOR'd HALT), got 0x%02X", rawHalt, code[9])
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrOp(t *testing.T) {
|
||||
// 0x0F OR 0xF0 = 0xFF
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0x0F}}},
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{0xF0}}},
|
||||
{Instr: Instruction{Op: OpOr}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 0xFF {
|
||||
t.Errorf("expected 0xFF, got 0x%X", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShrOp(t *testing.T) {
|
||||
// 256 SHR 4 = 16
|
||||
program := []LabelledInstr{
|
||||
{Instr: Instruction{Op: OpPush, Args: []int64{256}}},
|
||||
{Instr: Instruction{Op: OpShr, Args: []int64{4}}},
|
||||
{Instr: Instruction{Op: OpHalt}},
|
||||
}
|
||||
|
||||
code := Compile(program)
|
||||
vm := NewVM(code, nil)
|
||||
result := vm.Run()
|
||||
|
||||
if result != 16 {
|
||||
t.Errorf("expected 16, got %d", result)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user