Styx as a Replacement for Unicorn¶
Styx can be used as a replacement for Unicorn i.e. purely as a CPU emulator. The following examples show how to emulate 32 bit Arm code using either Rust or Python. Both examples produce identical results.
Rust Example¶
use std::borrow::Cow;
use styx_emulator::core::cpu::arch::arm::{ArmRegister, ArmVariants};
use styx_emulator::core::processor::executor::Executor;
use styx_emulator::core::cpu::hooks::StyxHook;
use styx_emulator::prelude::*;
use styx_emulator::processors::RawProcessor;
use keystone_engine::Keystone;
/*
MOV R0, #5 ; Load 5 into register R0
MOV R1, #3 ; Load 3 into register R1
MUL R2, R0, R1 ; Multiply R0 by R1, store result in R2
SVC #0 ; Trigger a software interrupt
*/
const THUMB_CODE: &str = "MOV R0, #5; MOV R1, #3; MUL R2, R0, R1; SVC #0";
/// Uses Keystone to assemble some Arm instructions and return the resulting bytes
fn assemble_code() -> Vec<u8> {
let ks = Keystone::new(keystone_engine::Arch::ARM, keystone_engine::Mode::THUMB)
.expect("Could not initialize Keystone engine");
let asm = ks
.asm(THUMB_CODE.to_string(), 0x4000)
.expect("Could not assemble");
println!("Assembled {} instructions", asm.stat_count);
asm.bytes
}
/// Callback for tracing instructions
fn hook_code(cpu: CpuBackend) {
println!(">>> Tracing instruction at 0x{:x}", cpu.pc().unwrap());
}
/// Callback for tracing basic blocks
fn hook_block(_cpu: CpuBackend, address: u64, size: u32) {
println!(">>> Tracing basic block at 0x{:x}, block size = {}", address, size);
}
/// Callback for tracing interrupts
fn hook_interrupts(cpu: CpuBackend, intno: i32) {
println!(">>> Tracing interrupt at 0x{:x}, interrupt number = {}", cpu.pc().unwrap(), intno);
// quit emulation
cpu.stop().unwrap();
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
// create a RawProcessor (i.e. minimal processor) for 32 bit Arm LE, using the PCode backend
let proc = ProcessorBuilder::default()
.with_backend(Backend::Pcode)
.with_endian(ArchEndian::LittleEndian)
.with_variant(ArmVariants::ArmCortexM4)
.with_loader(RawLoader)
.with_executor(Executor::default())
.with_input_bytes(Cow::Owned(assemble_code()))
.build::<RawProcessor>()?;
// add hooks for instructions, basic blocks, and interrupts
proc.add_hook(StyxHook::Code { start: u64::MIN, end: u64::MAX, callback: Box::new(hook_code) })?;
proc.add_hook(StyxHook::Block { callback: Box::new(hook_block) })?;
proc.add_hook(StyxHook::Interrupt { callback: Box::new(hook_interrupts) })?;
// start emulation
proc.start()?;
// check that R2 holds the value 15 to see if emulation was successful
assert_eq!(proc.read_register::<u32>(ArmRegister::R2).unwrap(), 15_u32);
Ok(())
}
Python Example¶
from styx_emulator.cpu import ArchEndian, Backend, CpuBackend
from styx_emulator.cpu.hooks import CodeHook, BlockHook, InterruptHook
from styx_emulator.processor import ProcessorBuilder, Target
from styx_emulator.loader import RawLoader
from styx_emulator.executor import DefaultExecutor
from styx_emulator.arch.arm import ArmVariant, ArmRegister
from keystone import Ks, KS_ARCH_ARM, KS_MODE_THUMB
'''
MOV R0, #5 ; Load 5 into register R0
MOV R1, #3 ; Load 3 into register R1
MUL R2, R0, R1 ; Multiply R0 by R1, store result in R2
SVC #0 ; Trigger a software interrupt
'''
THUMB_CODE = "MOV R0, #5; MOV R1, #3; MUL R2, R0, R1; SVC #0"
def assemble_code() -> bytes:
'''
Uses Keystone to assemble some Arm instructions and return the resulting bytes
'''
ks = Ks(KS_ARCH_ARM, KS_MODE_THUMB)
asm_bytes, asm_stat_count = ks.asm(THUMB_CODE)
print(f"Assembled {asm_stat_count} instructions")
return asm_bytes
def hook_code(cpu: CpuBackend):
'''
Callback for tracing instructions
'''
print(f">>> Tracing instruction at 0x{cpu.pc:x}")
def hook_block(_cpu: CpuBackend, address: int, size: int):
'''
Callback for tracing basic blocks
'''
print(f">>> Tracing basic block at 0x{address:x}, block size = {size}")
def hook_interrupts(cpu: CpuBackend, intno: int):
'''
Callback for tracing interrupts
'''
print(f">>> Tracing interrupt at 0x{cpu.pc:x}, interrupt number = {intno}")
# quit emulation
cpu.stop()
def main():
# create a RawProcessor (i.e. minimal processor) for 32 bit Arm LE, using the PCode backend
builder = ProcessorBuilder()
builder.backend = Backend.Pcode
builder.endian = ArchEndian.LittleEndian
builder.variant = ArmVariant.ArmCortexM4
builder.loader = RawLoader()
builder.executor = DefaultExecutor()
builder.input_bytes = bytes(assemble_code())
proc = builder.build(Target.Raw)
# add hooks for instructions, basic blocks, and interrupts
proc.add_hook(CodeHook(0, 0xFFFFFFFFFFFFFFFF, hook_code))
proc.add_hook(BlockHook(hook_block))
proc.add_hook(InterruptHook(hook_interrupts))
# start emulation
proc.start()
# check that R2 holds the value 15 to see if emulation was successful
assert(proc.read_register(ArmRegister.R2) == 15)
if __name__ == '__main__':
main()