Migration to New Versions of Styx¶
Multi-Processor Changes¶
styx-processor supports multiple vCPUs per processor. The change is broad and
mostly mechanical. It groups into three concerns:
Multi-vCPU: the processor core split, the
ProcessorBundlebuilder, and the per-vCPU access pattern (proc.vcpus[..]).Event distributor: the split of the event controller trait and the reshaped
Peripheral::tick.Timing: the distinction between
DeltaandGlobalDelta.
The core split¶
The old single ProcessorCore, which bundled cpu, mmu, and event controller, is
split in two:
Type |
Scope |
Holds |
|---|---|---|
|
processor-wide, shared between vCPUs |
|
|
per-vCPU |
|
A Processor exposes pub vcpus: Vec<VcpuCore> and pub core: ProcessorCore.
Single-vCPU processors have one entry in vcpus.
1. Multi-vCPU¶
Building a processor: ProcessorBundle::builder()¶
ProcessorBundleBuilder and the nested vCPU builders supply defaults for most
processor and vCPU components, and provide methods to add vCPUs.
// OLD
Ok(ProcessorBundle {
cpu: Box::new(MyCpu),
tlb: Box::new(MyTlb),
event_controller: Box::new(MyEc),
memory,
peripherals,
loader_hints,
})
// NEW: one vCPU, configured through the nested VcpuBundleBuilder
Ok(ProcessorBundle::builder()
.with_memory(memory)
.with_vcpu(|v| v.with_cpu(MyCpu).with_tlb(MyTlb).with_event_controller(MyEc))
.add_peripheral(MyUart::new())
.with_arch_hint(Arch::Arm)
.build()?)
// NEW: 16 vCPUs configured quickly via `with_vcpus`
Ok(ProcessorBundle::builder()
.with_memory(memory)
.with_vcpus(16, |_idx, v| v.with_cpu(DummyBackend).with_event_controller(MyEc))
.add_peripheral(MyUart::new())
.with_arch_hint(Arch::Arm)
.build()?)
See the ProcessorBundleBuilder docs for the full list of methods.
modify_memory() gives mutable access to the MemoryBackend, so memory can be
mapped from within the builder.
.with_memory(MemoryBackend::new_region_store())
.modify_memory(|mem| { mem.add_memory_region(/* .. */)?; Ok(()) })?
Per-vCPU access in init() and elsewhere¶
proc.core.cpu and proc.core.mmu no longer exist. In ProcessorImpl::init and
other inits, BuildingProcessor exposes them through vcpus:
pub struct BuildingProcessor<'a> {
pub vcpus: &'a mut [VcpuCore], // NEW
pub core: &'a mut ProcessorCore,
pub runtime: &'a mut ProcessorRuntime,
pub routes: RoutesBuilder,
// ..
}
For processors that were previously single-vCPU, replace proc.core.cpu with
proc.vcpus[0].cpu and proc.core.mmu with proc.vcpus[0].mmu. Multi-vCPU
systems can add hooks to one or many vCPUs.
If your memory operations use physical addresses, proc.memory() gives a
reference to the MemoryBackend.
Running processors: Processor::run() vs Processor::run_multi()¶
Method |
Returns |
Notes |
|---|---|---|
|
|
errors if there is more than one vCPU |
|
|
one report per vCPU |
Single-vCPU callers are unaffected. Multi-vCPU callers must use run_multi.
Other surface changes¶
CoreHandle::vcpu_id() -> VcpuIdidentifies the current vCPU.Processor::memory() -> &Arc<MemoryBackend>returns shared physical memory.Processor::for_vcpu(|v| ..)andProcessor::add_hooks(|vcpu_id| StyxHook)apply an operation or hook across every vCPU.
Executor changes¶
The ExecutorImpl trait is gone, replaced by two traits behind an
ExecutorKind:
StrideExecutor: the common case. The Styx core drives the multi-vCPU loop, so you supply onlyget_stride_length(),halt_emulation() -> Option<HaltFn>, and theinit,emulation_setup,emulation_teardown, andticklifecycle hooks.CustomExecutor: full control for debuggers and fuzzers. It has one method,execute(&mut [VcpuCore], &mut ProcessorCore, &mut Plugins, &ExecutionConstraintConcrete) -> Result<Vec<EmulationReport>>. A custom executor ticks all components itself.
Because the executor types are separate, ProcessorBuilder has different methods
depending on the executor type passed.
// when creating a processor
let builder = ProcessorBuilder::default();
// Stride executors keep `with_executor`.
// NOTE: the builder comes initialized with the default executor
// so you don't need to add the default executor, this is just
// for example purposes.
let builder = builder.with_executor(DefaultExecutor::default())
// GdbExecutor is a "custom executor"
let builder = builder.with_custom_executor(GdbExecutor::<Ppc4xxTargetDescription>::new(gdb_params)?)
// ExecutorKind can hold either custom or stride executors.
let builder = builder.with_executor_kind(ExecutorKind::custom(GdbExecutor::<Ppc4xxTargetDescription>::new(gdb_params)?));
2. Event distributor¶
The single EventControllerImpl trait is split into two.
Trait |
Scope |
Role |
|---|---|---|
|
per-vCPU |
latch, next, and execute interrupts on its own CPU; also routes from the vCPU to the event distributor |
|
processor-wide |
owns peripherals and routes IRQs raised by peripheral ticks to the correct vCPU |
For single-vCPU processors the split requires little work: existing controller
logic stays as the secondary event controller, and the SingleVcpuEventDistributor
in the Styx core acts as the event distributor.
Per-vCPU EventControllerImpl¶
Keep all latch, priority, and ISR logic. Only two signatures move:
next()no longer takes&mut Peripherals, because peripherals moved to the event distributor.tick()gains a&Deltaparameter.
latch(), execute(), finish_interrupt(), reset(), and init() are
unchanged. The dummy is still DummyEventController. EventController::new()
now takes (Box<dyn EventControllerImpl>, vcpu_index: VcpuId), and
finish_interrupt() now returns Option<ExceptionNumber>.
EventDistributorImpl¶
pub trait EventDistributorImpl {
fn init(&mut self, vcpus: &mut [VcpuCore], memory: &Arc<MemoryBackend>) -> Result<(), UnknownError> { Ok(()) }
fn on_processor_start(&mut self, vcpus: &mut [VcpuCore]) -> Result<(), UnknownError> { Ok(()) }
fn on_processor_stop(&mut self, vcpus: &mut [VcpuCore]) -> Result<(), UnknownError> { Ok(()) }
fn tick(&mut self, delta: &GlobalDelta, pending_irqs: &[ExceptionNumber], vcpus: &mut [VcpuCore]) -> Result<(), UnknownError> { Ok(()) }
fn reset(&mut self, cpu: &mut dyn CpuBackend, mmu: &mut Mmu) -> Result<(), UnknownError> { Ok(()) }
// latch(..), etc.
}
SingleVcpuEventDistributor, the default, routes every IRQ raised by a peripheral
tick to the secondary controller of vcpus[0]. Use it for single-vCPU processors
with interrupt-driven peripherals.
Write a custom EventDistributorImpl when you have processor-wide lifecycle or
routing logic, such as routing IRQs to one specific core.
Peripheral::tick has a new signature and returns IRQs¶
The event distributor owns peripherals. It ticks each one per round, collects the
returned IRQs, and hands them to EventDistributorImpl::tick for routing.
// OLD: latched directly onto the event controller
fn tick(&mut self, cpu: &mut dyn CpuBackend, mmu: &mut Mmu,
ec: &mut dyn EventControllerImpl, delta: &Delta) -> Result<(), UnknownError> {
if self.has_data() { ec.latch(self.irqn)?; }
Ok(())
}
// NEW: returns the IRQs it wants raised
fn tick(&mut self, ctx: &PeripheralTickCtx<'_>) -> Result<RaisedIrqs, UnknownError> {
let mut raised = RaisedIrqs::none();
if self.has_data() { raised.push(self.irqn); }
Ok(raised)
}
What tick can and cannot do:
Can: read
ctx.delta, and read and write physical memory throughctx.memory(&MemoryBackend). DMA-style peripherals can write guest memory directly instead of staging through an MMIO hook.Cannot: touch CPU registers, touch a per-vCPU MMU or virtual address, or call
latchdirectly. Register a hook ininit()if you need register or virtual memory access.
Peripheral hooks¶
Peripherals no longer live near vCPUs, so hooks no longer have access to the
peripheral that owns them (previously via
proc.event_controller.peripherals.get_expect::<MyPeripheral>()?;).
Share peripheral data with hooks through an ``Arc``ed data store. The stm32f107 processor’s i2c implementation is an example.
// in Peripheral::init
cpu.add_hook(StyxHook::memory_write(
base_addr + I2C_CR1_OFFSET,
hooks::I2cCr1WHook { inner: i2c.clone() },
))?;
pub(crate) struct I2cCr1WHook {
pub(crate) inner: Arc<Mutex<I2CPortInner>>,
}
impl MemoryWriteHook for I2cCr1WHook {
fn call(
&mut self,
proc: CoreHandle,
address: u64,
_size: u32,
data: &[u8],
) -> Result<(), UnknownError> {
let port = self.inner.lock().unwrap();
// do some stuff with `port`
Ok(())
}
}
Migrate away from post_event_hook¶
post_event_hook, and any related “peripheral event done” callbacks, is removed
from the Peripheral trait and from its UART and SPI implementations. The
callback fired on the peripheral that raised an interrupt once the firmware
returned from the handler, letting the peripheral do post-event cleanup such as
re-latching an IRQ that still had data pending. Detecting return from interrupt
is architecture specific and fragile, and some firmware never returns from the
handler at all (FreeRTOS on PPC, for example), so the mechanism was dropped. The
Remove Post Event Hook ADR has
the full rationale.
If your peripheral relied on post_event_hook to re-check state, move that logic
into Peripheral::tick. It runs every round and returns the IRQs to raise, so
re-latching happens on the next tick without a dedicated callback.
3. Timing: Delta vs GlobalDelta¶
The split introduces two delta types, one for processor time and one for vCPU time.
Type |
Fields |
Used by |
|---|---|---|
|
|
secondary |
|
|
|
ProcessorCore::time (ProcessorTime) tracks the processor-wide simulated
clock and advances once per round. VcpuCore::time (VcpuTime) tracks per-vCPU
time. See the styx_core::executor::time module docs for the full model.
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(())
}