Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 26 additions & 20 deletions tests/test_assembler.nim
Original file line number Diff line number Diff line change
Expand Up @@ -19,46 +19,52 @@ suite "Assembler Module Tests":
test "assembleInstruction encodes NOP correctly":
if not isKeystoneAvailable():
skip()
let bytes = assembleInstruction("nop", archX64)
check bytes.len == 1
check bytes[0] == 0x90'u8
else:
let bytes = assembleInstruction("nop", archX64)
check bytes.len == 1
check bytes[0] == 0x90'u8

test "assembleInstruction encodes RET correctly":
if not isKeystoneAvailable():
skip()
let bytes = assembleInstruction("ret", archX64)
check bytes.len == 1
check bytes[0] == 0xC3'u8
else:
let bytes = assembleInstruction("ret", archX64)
check bytes.len == 1
check bytes[0] == 0xC3'u8

test "assembleInstruction returns empty seq for invalid instruction":
if not isKeystoneAvailable():
skip()
let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64)
check bytes.len == 0
else:
let bytes = assembleInstruction("notavalidinstruction xyz_!!!", archX64)
check bytes.len == 0

test "assembleBlock encodes multiple semicolon-separated instructions":
if not isKeystoneAvailable():
skip()
let output = assembleBlock("nop; nop; ret", archX64)
check output.bytes.len == 3
check output.statCount == 3
check output.bytes[0] == 0x90'u8
check output.bytes[1] == 0x90'u8
check output.bytes[2] == 0xC3'u8
else:
let output = assembleBlock("nop; nop; ret", archX64)
check output.bytes.len == 3
check output.statCount == 3
check output.bytes[0] == 0x90'u8
check output.bytes[1] == 0x90'u8
check output.bytes[2] == 0xC3'u8

test "assembleBlock returns empty output for invalid instructions":
if not isKeystoneAvailable():
skip()
let output = assembleBlock("notvalid !!!", archX64)
check output.bytes.len == 0
check output.statCount == 0
else:
let output = assembleBlock("notvalid !!!", archX64)
check output.bytes.len == 0
check output.statCount == 0

test "assembleInstruction handles x86 32-bit mode":
if not isKeystoneAvailable():
skip()
let bytes = assembleInstruction("nop", archX86)
check bytes.len == 1
check bytes[0] == 0x90'u8
else:
let bytes = assembleInstruction("nop", archX86)
check bytes.len == 1
check bytes[0] == 0x90'u8

test "makeNops generates the correct number of NOP bytes":
let nops = makeNops(4)
Expand Down
105 changes: 55 additions & 50 deletions tests/test_disassembler.nim
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,6 @@
# unconditionally.
import unittest, disassembler, binary

# ---------------------------------------------------------------------------
# Helper: skip a test when Capstone is not installed.
# ---------------------------------------------------------------------------
template requireCapstone() =
if not isCapstoneAvailable():
skip()

# ---------------------------------------------------------------------------
# Suite: Capstone availability probe
# ---------------------------------------------------------------------------
Expand All @@ -34,59 +27,71 @@ suite "disassembleBytes":
check result.len == 0

test "NOP byte (0x90) decodes as 'nop' on x64":
requireCapstone()
let data: seq[byte] = @[0x90'u8]
let instrs = disassembleBytes(data, 0x1000'u64, archX64)
check instrs.len == 1
check instrs[0].mnemonic == "nop"
check instrs[0].address == 0x1000'u64
check instrs[0].rawBytes == @[0x90'u8]
if not isCapstoneAvailable():
skip()
else:
let data: seq[byte] = @[0x90'u8]
let instrs = disassembleBytes(data, 0x1000'u64, archX64)
check instrs.len == 1
check instrs[0].mnemonic == "nop"
check instrs[0].address == 0x1000'u64
check instrs[0].rawBytes == @[0x90'u8]

test "RET byte (0xC3) decodes as 'ret' on x64":
requireCapstone()
let data: seq[byte] = @[0xC3'u8]
let instrs = disassembleBytes(data, 0x2000'u64, archX64)
check instrs.len == 1
check instrs[0].mnemonic == "ret"
check instrs[0].address == 0x2000'u64
if not isCapstoneAvailable():
skip()
else:
let data: seq[byte] = @[0xC3'u8]
let instrs = disassembleBytes(data, 0x2000'u64, archX64)
check instrs.len == 1
check instrs[0].mnemonic == "ret"
check instrs[0].address == 0x2000'u64

test "Function prologue bytes decode to push/mov on x64":
requireCapstone()
# 55 push rbp
# 48 89 E5 mov rbp, rsp
let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8]
let instrs = disassembleBytes(data, 0x4000'u64, archX64)
check instrs.len == 2
check instrs[0].mnemonic == "push"
check instrs[0].address == 0x4000'u64
check instrs[1].mnemonic == "mov"
check instrs[1].address == 0x4001'u64
if not isCapstoneAvailable():
skip()
else:
# 55 push rbp
# 48 89 E5 mov rbp, rsp
let data: seq[byte] = @[0x55'u8, 0x48'u8, 0x89'u8, 0xE5'u8]
let instrs = disassembleBytes(data, 0x4000'u64, archX64)
check instrs.len == 2
check instrs[0].mnemonic == "push"
check instrs[0].address == 0x4000'u64
check instrs[1].mnemonic == "mov"
check instrs[1].address == 0x4001'u64

test "Multiple instructions have sequential addresses":
requireCapstone()
# 90 = nop (1 byte), C3 = ret (1 byte)
let data: seq[byte] = @[0x90'u8, 0xC3'u8]
let instrs = disassembleBytes(data, 0x5000'u64, archX64)
check instrs.len == 2
check instrs[0].address == 0x5000'u64
check instrs[1].address == 0x5001'u64
if not isCapstoneAvailable():
skip()
else:
# 90 = nop (1 byte), C3 = ret (1 byte)
let data: seq[byte] = @[0x90'u8, 0xC3'u8]
let instrs = disassembleBytes(data, 0x5000'u64, archX64)
check instrs.len == 2
check instrs[0].address == 0x5000'u64
check instrs[1].address == 0x5001'u64

test "Invalid/garbage bytes return empty or partial result without crashing":
requireCapstone()
# 0xFF 0xFF is not a valid x64 instruction at this position; Capstone
# may decode it partially or return empty, but must not raise.
let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8]
let instrs = disassembleBytes(data, 0'u64, archX64)
# We only check it did not crash; result may be empty or partial.
check instrs.len >= 0
if not isCapstoneAvailable():
skip()
else:
# 0xFF 0xFF is not a valid x64 instruction at this position; Capstone
# may decode it partially or return empty, but must not raise.
let data: seq[byte] = @[0xFF'u8, 0xFF'u8, 0xFF'u8, 0xFF'u8]
let instrs = disassembleBytes(data, 0'u64, archX64)
# We only check it did not crash; result may be empty or partial.
check instrs.len >= 0

test "Base address is reflected in instruction addresses":
requireCapstone()
let data: seq[byte] = @[0x90'u8]
let base = 0xDEAD0000'u64
let instrs = disassembleBytes(data, base, archX64)
check instrs.len == 1
check instrs[0].address == base
if not isCapstoneAvailable():
skip()
else:
let data: seq[byte] = @[0x90'u8]
let base = 0xDEAD0000'u64
let instrs = disassembleBytes(data, base, archX64)
check instrs.len == 1
check instrs[0].address == base

# ---------------------------------------------------------------------------
# Suite: disassembleSection (pure-Nim paths that do not need Capstone)
Expand Down
131 changes: 70 additions & 61 deletions tests/test_emulator.nim
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ suite "Emulator Module Tests":
test "closeEmulator is safe to call twice on a real context":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
closeEmulator(ctx)
closeEmulator(ctx)
else:
var ctx = createEmulator(archX64)
closeEmulator(ctx)
closeEmulator(ctx)

test "loadMemory returns false when engine is nil":
var ctx = EmulatorContext()
Expand All @@ -51,10 +52,11 @@ suite "Emulator Module Tests":
test "loadMemory succeeds with valid data when Unicorn available":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
let data: seq[byte] = @[0x90'u8, 0x90'u8]
check ctx.loadMemory(0x00401000'u64, data) == true
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
let data: seq[byte] = @[0x90'u8, 0x90'u8]
check ctx.loadMemory(0x00401000'u64, data) == true

test "readRegister returns 0 for nil engine":
let ctx = EmulatorContext()
Expand Down Expand Up @@ -83,86 +85,92 @@ suite "Emulator Module Tests":
test "register write and read round-trip":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true
check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
check ctx.writeRegister(UC_X86_REG_RAX, 0xDEADBEEF'u64) == true
check ctx.readRegister(UC_X86_REG_RAX) == 0xDEADBEEF'u64

test "register write and read round-trip for RBX":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true
check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
check ctx.writeRegister(UC_X86_REG_RBX, 0x1234'u64) == true
check ctx.readRegister(UC_X86_REG_RBX) == 0x1234'u64

test "emulate a NOP sled executes without error":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)

# Four NOP bytes. The emulator stops after maxInstructions = 4.
let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true
# Four NOP bytes. The emulator stops after maxInstructions = 4.
let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true

let res = ctx.emulateRange(base, base + uint64(code.len), 4)
check res.success == true
let res = ctx.emulateRange(base, base + uint64(code.len), 4)
check res.success == true

test "emulate mov eax, 42 and read register result":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)

# Encoding: b8 2a 00 00 00 (mov eax, 42)
let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true
# Encoding: b8 2a 00 00 00 (mov eax, 42)
let code: seq[byte] = @[0xb8'u8, 0x2a'u8, 0x00'u8, 0x00'u8, 0x00'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true

# Execute exactly 1 instruction and verify EAX.
let res = ctx.emulateRange(base, base + uint64(code.len), 1)
check res.success == true
check ctx.readRegister(UC_X86_REG_EAX) == 42'u64
# Execute exactly 1 instruction and verify EAX.
let res = ctx.emulateRange(base, base + uint64(code.len), 1)
check res.success == true
check ctx.readRegister(UC_X86_REG_EAX) == 42'u64

test "emulate xor rax, rax zeroes the register":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)

check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true
check ctx.writeRegister(UC_X86_REG_RAX, 0xFFFF'u64) == true

# Encoding: 48 31 c0 (xor rax, rax)
let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true
# Encoding: 48 31 c0 (xor rax, rax)
let code: seq[byte] = @[0x48'u8, 0x31'u8, 0xc0'u8]
let base = 0x00401000'u64
check ctx.loadMemory(base, code) == true

let res = ctx.emulateRange(base, base + uint64(code.len), 1)
check res.success == true
check ctx.readRegister(UC_X86_REG_RAX) == 0'u64
let res = ctx.emulateRange(base, base + uint64(code.len), 1)
check res.success == true
check ctx.readRegister(UC_X86_REG_RAX) == 0'u64

test "code hook fires for each instruction in a NOP sled":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)

var counter: int = 0
var counter: int = 0

proc countHook(uc: ptr UcEngine, address: uint64, size: uint32,
userData: pointer) {.cdecl.} =
var p = cast[ptr int](userData)
p[] += 1
proc countHook(uc: ptr UcEngine, address: uint64, size: uint32,
userData: pointer) {.cdecl.} =
var p = cast[ptr int](userData)
p[] += 1

let base = 0x00401000'u64
let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8]
check ctx.loadMemory(base, code) == true
check ctx.addCodeHook(countHook, addr counter) == true
let base = 0x00401000'u64
let code: seq[byte] = @[0x90'u8, 0x90'u8, 0x90'u8, 0x90'u8]
check ctx.loadMemory(base, code) == true
check ctx.addCodeHook(countHook, addr counter) == true

discard ctx.emulateRange(base, base + uint64(code.len), 4)
check counter == 4
discard ctx.emulateRange(base, base + uint64(code.len), 4)
check counter == 4

test "loadBinary returns false when engine is nil":
var ctx = EmulatorContext()
Expand All @@ -172,11 +180,12 @@ suite "Emulator Module Tests":
test "loadBinary returns false for unknown binary format":
if not isUnicornAvailable():
skip()
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
# parseBinary on a nonexistent file returns bfUnknown with no sections.
let info = parseBinary("nonexistent_binary_xyz.elf")
check ctx.loadBinary(info, ".text") == false
else:
var ctx = createEmulator(archX64)
defer: closeEmulator(ctx)
# parseBinary on a nonexistent file returns bfUnknown with no sections.
let info = parseBinary("nonexistent_binary_xyz.elf")
check ctx.loadBinary(info, ".text") == false

test "isUnicornAvailable does not raise when library is missing":
# Regardless of whether Unicorn is installed, the call must not raise.
Expand Down
Loading
Loading