Skip to content

reflexive.space

Decoding ARMv5 Instructions in Rust

tl;dr I wrote a somewhat-simple crate for decoding 32-bit ARMv5 and Thumb instructions called armbf.

Most of the implementation is straightforward enough that it's probably just easier for you to read through it yourself, although I'd like to talk just a little bit about how to build lookup tables for ARM.

In general, you can bin ARM instructions into groups using bits 27-20 and bits 7-4. Thumb instructions are slightly simpler and are mostly identifiable by looking at bits 15-11.

The first thing that comes to mind when writing some kind of parser, is that you should use your language's switch/case statements to discriminate between things, i.e. in armbf:

pub enum ArmInst {
  ...
}

impl ArmInst {
  pub fn decode(x: u32) -> Self {
    match get_group!(x) {
      0b000 => { ... },
      0b001 => { ... },
      0b010 => { ... },
      ...
    }
  }
}

This is expensive during runtime -- if you were implementing all of the instructions in an interpreting-style emulator, you'd be condemned to doing this lookup for every instruction. Instead, in the context of an emulator, it's advantageous to pre-compute a table which is a one-to-one mapping from "some instruction" to "function pointer to implementation."

First, we have a generic array of lookup table entries -- it's easier for users to just implement ArmLutEntry and ThumbLutEntry for whatever needs to be retrieved (i.e. function pointers, strings for a disassembler, etc).

/// An ARMv5 lookup table.
#[repr(C, align(64))]
pub struct ArmLut<T: ArmLutEntry> { pub data: [T; 0x1000] }

/// A Thumb lookup table.
#[repr(C, align(64))]
pub struct ThumbLut<T: ThumbLutEntry> { pub data: [T; 0x0800] }

/// Implemented on all types store-able by some ArmLut.
pub trait ArmLutEntry { fn from_inst(inst: ArmInst) -> Self; }

/// Implemented on all types store-able by some ThumbLut.
pub trait ThumbLutEntry { fn from_inst(inst: ThumbInst) -> Self; }

Then, we have the two functions that populate the lookup tables:

/// Creates a new ArmLut for some T.
///
/// The details of how to obtain an entry T are left to the user.
pub fn make_arm_lut<T: ArmLutEntry + Copy>(default_entry: T) -> ArmLut<T> {
    let mut lut = ArmLut { data: [default_entry; 0x1000] };
    for i in 0..0x1000 {
        let inst: u32 = ((i & 0x0ff0) << 16) | ((i & 0x000f) << 4);
        lut.data[i as usize] = T::from_inst(ArmInst::decode(inst));
    }
    lut
}

/// Create a new ThumbLut for some T.
pub fn make_thumb_lut<T: ThumbLutEntry + Copy>(default_entry: T) -> ThumbLut<T> {
    let mut lut = ThumbLut { data: [default_entry; 0x0800] };
    for i in 0..0x800 {
        let inst: u16 = i << 5;
        lut.data[i as usize] = T::from_inst(ThumbInst::decode(inst));
    }
    lut
}

This iterates over the space of unique opcodes, decodes them into the corresponding ArmInst (or ThumbInst), and then calls the user-defined trait function from_inst(), which maps an instruction to some lookup table entry.

For example, if we were going to fill the lookup table with function pointers to implementations of instructions, you could do something like:

use armbf::lut::*;
use armbf::inst::*;

use crate::cpu::interp::InstRes;

/// Inner type for ARM LUT entries.
pub type ArmFn = fn(&mut Cpu, &u32) -> InstRes;

/// Newtype representing a function pointer in the ARM LUT.
#[derive(Copy, Clone)]
pub struct LutFn(pub ArmFn);

/// An unimplemented instruction handler.
pub fn unimpl_handler(cpu: &mut Cpu, x: &u32) -> InstRes { InstRes::Unimpl }

/// Implement the mapping from ARM instruction to ARM LUT entry.
impl ArmLutEntry for LutFn {
  fn from_inst(inst: ArmInst) -> Self {

    // Coerce plain function pointers into an `ArmFn`
    macro_rules! cfn { ($func:expr) => { unsafe {
      std::mem::transmute::<*const fn(), ArmFn>($func as *const fn())
    }}}

    match inst {
      // Mappings from instructions to functions go here
      // ...
      _ => LutFn(cfn!(unimpl_handler)),
    }
  }
}

Then, during runtime, all we need to do is mask off the necessary bits from some instruction -- at this point, all you need to do is index into a table!

pub struct Cpu {
  armlut: ArmLut,
}

...

impl Cpu {
  fn decode_arm(&mut self, opcd: u32) -> LutFn {
    let idx = (((opcd >> 16) & 0x0ff0) | ((opcd >> 4) & 0x000f)) as usize;
    self.armlut.data[idx]
  }
}