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

Run this example with cargo run -p unicorn-replacement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
//! Use `Styx` purely as a CPU emulator.
use std::borrow::Cow;

use styx_emulator::core::cpu::arch::arm::{ArmRegister, ArmVariants};
use styx_emulator::core::executor::DefaultExecutor;
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(mut proc: CoreHandle) -> Result<(), UnknownError> {
    println!(">>> Tracing instruction at 0x{:x}", proc.pc()?);
    Ok(())
}

/// Callback for tracing interrupts
fn hook_interrupts(mut proc: CoreHandle, intno: i32) -> Result<(), UnknownError> {
    println!(
        ">>> Tracing interrupt at 0x{:x}, interrupt number = {intno}",
        proc.pc()?
    );
    // quit emulation
    proc.stop();
    Ok(())
}

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 mut proc = ProcessorBuilder::default()
        .with_backend(Backend::Pcode)
        .with_builder(RawProcessor::new(
            Arch::Arm,
            ArmVariants::ArmCortexM4,
            ArchEndian::LittleEndian,
        ))
        .with_loader(RawLoader)
        .with_executor(DefaultExecutor::default())
        .with_input_bytes(Cow::Owned(assemble_code()))
        // add hooks for instructions and interrupts
        .add_hook(StyxHook::code(u64::MIN..=u64::MAX, hook_code))
        .add_hook(StyxHook::interrupt(hook_interrupts))
        .build()?;

    // start emulation
    proc.run(Forever)?;

    // check that R2 holds the value 15 to see if emulation was successful
    assert_eq!(
        proc.vcpus[0].cpu.read_register::<u32>(ArmRegister::R2)?,
        15_u32
    );

    Ok(())
}

Python Example

Run this example with python3 main.py from styx/bindings/styx-py-api/examples/unicorn-replacement.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# Uses `Styx` purely as a CPU emulator, like `Unicorn`.
from styx_emulator.cpu import ArchEndian, Backend, ProcessorCore
from styx_emulator.cpu.hooks import CodeHook, 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: ProcessorCore):
    '''
    Callback for tracing instructions
    '''
    print(f">>> Tracing instruction at 0x{cpu.pc:x}")

def hook_interrupts(cpu: ProcessorCore, 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 and interrupts
    proc.add_hook(CodeHook(0, 0xFFFFFFFFFFFFFFFF, hook_code))
    proc.add_hook(InterruptHook(hook_interrupts))

    # start emulation and wait for the interrupt hook to stop it
    proc.start()
    proc.wait_for_stop()

    # check that R2 holds the value 15 to see if emulation was successful
    assert(proc.read_register(ArmRegister.R2) == 15)

if __name__ == '__main__':
    main()