Migration to New Versions of Styx¶
1.0.0 to 1.2.0¶
Emulation API Changes¶
The API for Executor
, CpuBackend
and Processor
emulation has been updated to provide more detailed execution information. The return type for emulation methods has changed from Result<TargetExitReason, ...>
to Result<EmulationReport, ...>
.
Before¶
let exit_reason: TargetExitReason = processor.run(constraint)?;
// exit_reason is a TargetExitReason enum
After¶
let report: EmulationReport = processor.run(constraint)?;
// report is an EmulationReport containing:
// - exit_reason: TargetExitReason
// - instruction_count: u64
// - etc.
This change allows users to access additional execution information like instruction counts and other metrics without requiring separate API calls.
NOTE: this also propagated to the python and C bindings accordingly
0.53.0 to 1.0.0¶
At a very high level, this refactor reorganized the major processor components and improved interactions between them. See (TODO: add link to diagram) to get an idea of how components now fit together.
A major paradigm shift was moving from Arc<Mutex<>>
based components to mut
components. We realized that we were both spending a lot of time in locks and that most of these locks were not really necessary, so we changed it. For users, this mostly affects Styx API calls in minor ways
Most of the changes that average users will encounter have to do with defining and building a processor. This document gives examples of how code was structured before and after to help users migrate to the new release.
Other notable changes that users might encounter includes changes to import paths.
By and large it is encouraged to use prelude imports from styx_core::prelude::or styx_emulator::prelude:; when possible. Additionally many of the common modules that were used were elevated to more ergonomic positions in the import (notably styx_core::sync::sync is now just styx_core::sync).
Inside of styx_core
, a lot has changed as we transition to a slightly different internal crate structure. This is leading to a partial sunsetting of styx-cpu retaining the majority of backend functionality and a reduced need for a large number of crates, consolidating a lot of the processor-level logic into styx-processor
.
Processor Definition¶
The previous way of defining a new processor involved lots of duplicated, boiler-plate code that could just be copy-pasted from an existing definition. We realized that the ProcessorImpl
was entirely stateless and pretty much only performed initialization duties. We combined the previous ProcessorImpl
and BuildableProcessor
traits into a single, simplified trait and moved other behavior to different parts of the codebase.
Before¶
pub struct ExampleCpu {
cpu: CpuBackend,
#[derivative(Debug = "ignore")]
event_controller: Arc<EvtController>,
weak_ref: Weak<Self>,
}
impl BuildableProcessor for ExampleCpu {
fn from_builder(
variant: impl Into<styx_core::cpu::arch::backends::ArchVariant>,
endian: styx_core::cpu::ArchEndian,
exception_behavior: ExceptionBehavior,
loader: Arc<dyn Loader>,
target_program: Cow<[u8]>,
runtime: Handle,
backend: Option<Backend>,
) -> Result<Arc<Self>, ProcessorBuilderImplError> {
...
}
}
impl ProcessorImpl for ExampleCpu {
fn cpu(&self) -> CpuBackend {
self.cpu.clone()
}
fn cpu_stop(&self) -> Result<(), StyxMachineError> {
...
}
fn event_controller(&self) -> Arc<dyn EventController> {
self.event_controller.clone()
}
fn cpu_start(
&self,
timeout: Option<Duration>,
insns: Option<u64>,
) -> Result<TargetExitReason, StyxMachineError> {
...
}
fn initialize(&self) -> Result<(), StyxMachineError> {
...
}
fn populate_default_registers(
&self,
desc: &mut MemoryLoaderDesc,
) -> Result<(), StyxMachineError> {
...
}
fn setup_address_space(&self) -> Result<(), StyxMachineError> {
...
}
}
After¶
pub struct ExampleCpuBuilder {}
impl ProcessorImpl for ExampleCpuBuilder {
fn build(
&self,
_runtime: &ProcessorRuntime,
cpu_backend: Backend,
) -> Result<ProcessorBundle, UnknownError> {
...
}
fn init(&self, proc: &mut BuildingProcessor) -> Result<(), UnknownError> {
...
}
}
See styx/processors/arm/styx-kinetis21-processor/src/lib.rs
for an example of what implementing this trait looks like in practice.
Instantiating a Processor¶
The previous ProcessorBuilder
had options for things like endianness, architecture, architecture variants, and the build method was generic with the processor being built. In the new architecture, most of these options are intrinsic to the processor being built and as such they are handled by the ProcessorImpl
passed to ProcessorBuilder::with_builder()
.
Before¶
let proc = ProcessorBuilder::default()
.with_endian(ArchEndian::LittleEndian)
.with_executor(Executor::default())
.with_loader(RawLoader)
.with_target_program(get_firmware_path())
.with_variant(ArmVariants::ArmCortexM3)
.build::<ExampleCpu>()?;
After¶
let mut proc = ProcessorBuilder::default()
.with_builder(ExampleCpuBuilder {})
.with_target_program(get_firmware_path())
.build()?;
To run the processor use Processor::run()
with an ExecutionConstraint
. The simplest one is Forever
.
let mut proc = ProcessorBuilder::default()
.with_builder(ExampleCpuBuilder {})
.with_target_program(get_firmware_path())
.build()?;
proc.run(Forever);
Hooks¶
The ways of adding and removing hooks haven’t really changed but the hook callback function prototypes have changed. Instead of a CpuBackend
as the first argument to hook callbacks, you now get a CoreHandle
which bundles together the cpu, mmu, and event controller components as mutable references.
Before¶
fn code_hook_callback(cpu: CpuBackend) {
// do something
}
After¶
fn code_hook_callback(proc: CoreHandle) -> Result<(), UnknownError> {
// do something
Ok(())
}
Memory Access¶
The 1.0 introduces the Mmu to the processor. This defined an api for device specific address translation. There is also support for separate code/data memory as is needed by some architectures. For the Styx user this means there is no longer read_memory()
/write_memory()
and instead read_code()
/write_code()
and read_data()
/write_data()
for code and data memory regions respectively. On architectures with no distinction between code/data memory then they will operate the same.
Data can be read without checking mmu permissions with the sudo_
variants: e.g. sudo_read_code()
.
There is also an experimental, alternative memory api accessed by the Mmu::code()
and Mmu::data()
methods. An example is shown below.
Before¶
fn code_hook_callback(cpu: CpuBackend) {
let mut buf = [0u8; 4];
cpu.read_memory(0x1000, &mut buf).unwrap();
let my_u32 = u32::from_le_bytes(&buf);
}
After¶
fn code_hook_callback(proc: CoreHandle) -> Result<(), UnknownError> {
let mut buf = vec![0u8; 8];
cpu.read_data(0x1000, &mut buf)?; // read from data region
let my_u32 = u32::from_le_bytes(&buf);
// or with experiment memory api
// note we can also use ? operator to propagate errors
let my_u32 = cpu.data().read(0x1000).le().u32()?;
Ok(())
}