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()
|