cranelift: Add big and little endian memory accesses to interpreter (#5893)

* Added `mem_flags` parameter to `State::checked_{load,store}` as the means
for determining the endianness, typically derived from an instruction.

* Added `native_endianness` property to `InterpreterState` as fallback when
determining endianness, such as in cases where there are no memory flags
avaiable or set.

* Added `to_be` and `to_le` methods to `DataValue`.

* Added `AtomicCas` and `AtomicRmw` to list of instructions with retrievable
memory flags for `InstructionData::memflags`.

* Enabled `atomic-{cas,rmw}-subword-{big,little}.clif` for interpreter run
tests.
This commit is contained in:
Jan-Justin van Tonder
2023-03-02 12:57:01 +01:00
committed by GitHub
parent 9984e959cd
commit db8fe0108f
10 changed files with 183 additions and 40 deletions

View File

@@ -101,12 +101,55 @@ impl DataValue {
} }
} }
/// Write a [DataValue] to a slice. fn swap_bytes(self) -> Self {
match self {
DataValue::I8(i) => DataValue::I8(i.swap_bytes()),
DataValue::I16(i) => DataValue::I16(i.swap_bytes()),
DataValue::I32(i) => DataValue::I32(i.swap_bytes()),
DataValue::I64(i) => DataValue::I64(i.swap_bytes()),
DataValue::I128(i) => DataValue::I128(i.swap_bytes()),
DataValue::U8(i) => DataValue::U8(i.swap_bytes()),
DataValue::U16(i) => DataValue::U16(i.swap_bytes()),
DataValue::U32(i) => DataValue::U32(i.swap_bytes()),
DataValue::U64(i) => DataValue::U64(i.swap_bytes()),
DataValue::U128(i) => DataValue::U128(i.swap_bytes()),
DataValue::F32(f) => DataValue::F32(Ieee32::with_bits(f.bits().swap_bytes())),
DataValue::F64(f) => DataValue::F64(Ieee64::with_bits(f.bits().swap_bytes())),
DataValue::V128(mut v) => {
v.reverse();
DataValue::V128(v)
}
DataValue::V64(mut v) => {
v.reverse();
DataValue::V64(v)
}
}
}
/// Converts `self` to big endian from target's endianness.
pub fn to_be(self) -> Self {
if cfg!(target_endian = "big") {
self
} else {
self.swap_bytes()
}
}
/// Converts `self` to little endian from target's endianness.
pub fn to_le(self) -> Self {
if cfg!(target_endian = "little") {
self
} else {
self.swap_bytes()
}
}
/// Write a [DataValue] to a slice in native-endian byte order.
/// ///
/// # Panics: /// # Panics:
/// ///
/// Panics if the slice does not have enough space to accommodate the [DataValue] /// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn write_to_slice(&self, dst: &mut [u8]) { pub fn write_to_slice_ne(&self, dst: &mut [u8]) {
match self { match self {
DataValue::I8(i) => dst[..1].copy_from_slice(&i.to_ne_bytes()[..]), DataValue::I8(i) => dst[..1].copy_from_slice(&i.to_ne_bytes()[..]),
DataValue::I16(i) => dst[..2].copy_from_slice(&i.to_ne_bytes()[..]), DataValue::I16(i) => dst[..2].copy_from_slice(&i.to_ne_bytes()[..]),
@@ -121,12 +164,30 @@ impl DataValue {
}; };
} }
/// Read a [DataValue] from a slice using a given [Type]. /// Write a [DataValue] to a slice in big-endian byte order.
/// ///
/// # Panics: /// # Panics:
/// ///
/// Panics if the slice does not have enough space to accommodate the [DataValue] /// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn read_from_slice(src: &[u8], ty: Type) -> Self { pub fn write_to_slice_be(&self, dst: &mut [u8]) {
self.clone().to_be().write_to_slice_ne(dst);
}
/// Write a [DataValue] to a slice in little-endian byte order.
///
/// # Panics:
///
/// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn write_to_slice_le(&self, dst: &mut [u8]) {
self.clone().to_le().write_to_slice_ne(dst);
}
/// Read a [DataValue] from a slice using a given [Type] with native-endian byte order.
///
/// # Panics:
///
/// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn read_from_slice_ne(src: &[u8], ty: Type) -> Self {
match ty { match ty {
types::I8 => DataValue::I8(i8::from_ne_bytes(src[..1].try_into().unwrap())), types::I8 => DataValue::I8(i8::from_ne_bytes(src[..1].try_into().unwrap())),
types::I16 => DataValue::I16(i16::from_ne_bytes(src[..2].try_into().unwrap())), types::I16 => DataValue::I16(i16::from_ne_bytes(src[..2].try_into().unwrap())),
@@ -152,15 +213,33 @@ impl DataValue {
} }
} }
/// Write a [DataValue] to a memory location. /// Read a [DataValue] from a slice using a given [Type] in big-endian byte order.
pub unsafe fn write_value_to(&self, p: *mut u128) { ///
let size = self.ty().bytes() as usize; /// # Panics:
self.write_to_slice(std::slice::from_raw_parts_mut(p as *mut u8, size)); ///
/// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn read_from_slice_be(src: &[u8], ty: Type) -> Self {
DataValue::read_from_slice_ne(src, ty).to_be()
} }
/// Read a [DataValue] from a memory location using a given [Type]. /// Read a [DataValue] from a slice using a given [Type] in little-endian byte order.
///
/// # Panics:
///
/// Panics if the slice does not have enough space to accommodate the [DataValue]
pub fn read_from_slice_le(src: &[u8], ty: Type) -> Self {
DataValue::read_from_slice_ne(src, ty).to_le()
}
/// Write a [DataValue] to a memory location in native-endian byte order.
pub unsafe fn write_value_to(&self, p: *mut u128) {
let size = self.ty().bytes() as usize;
self.write_to_slice_ne(std::slice::from_raw_parts_mut(p as *mut u8, size));
}
/// Read a [DataValue] from a memory location using a given [Type] in native-endian byte order.
pub unsafe fn read_value_from(p: *const u128, ty: Type) -> Self { pub unsafe fn read_value_from(p: *const u128, ty: Type) -> Self {
DataValue::read_from_slice( DataValue::read_from_slice_ne(
std::slice::from_raw_parts(p as *const u8, ty.bytes() as usize), std::slice::from_raw_parts(p as *const u8, ty.bytes() as usize),
ty, ty,
) )

View File

@@ -403,7 +403,9 @@ impl InstructionData {
&InstructionData::Load { flags, .. } &InstructionData::Load { flags, .. }
| &InstructionData::LoadNoOffset { flags, .. } | &InstructionData::LoadNoOffset { flags, .. }
| &InstructionData::Store { flags, .. } | &InstructionData::Store { flags, .. }
| &InstructionData::StoreNoOffset { flags, .. } => Some(flags), | &InstructionData::StoreNoOffset { flags, .. }
| &InstructionData::AtomicCas { flags, .. }
| &InstructionData::AtomicRmw { flags, .. } => Some(flags),
_ => None, _ => None,
} }
} }

View File

@@ -1,3 +1,4 @@
test interpret
test run test run
target s390x target s390x

View File

@@ -1,3 +1,4 @@
test interpret
test run test run
target s390x target s390x
target aarch64 target aarch64

View File

@@ -1,3 +1,4 @@
test interpret
test run test run
target s390x target s390x
target s390x has_mie2 target s390x has_mie2
@@ -431,10 +432,10 @@ block0(v0: i32, v1: i64, v2: i16):
v6 = load.i32 big v3 v6 = load.i32 big v3
return v6 return v6
} }
; run: %atomic_rmw_xchg_little_i16(0x12345678, 0, 0x1111) == 0x11115678 ; run: %atomic_rmw_xchg_big_i16(0x12345678, 0, 0x1111) == 0x11115678
; run: %atomic_rmw_xchg_little_i16(0x12345678, 0, 0xffff) == 0xffff5678 ; run: %atomic_rmw_xchg_big_i16(0x12345678, 0, 0xffff) == 0xffff5678
; run: %atomic_rmw_xchg_little_i16(0x12345678, 2, 0x1111) == 0x12341111 ; run: %atomic_rmw_xchg_big_i16(0x12345678, 2, 0x1111) == 0x12341111
; run: %atomic_rmw_xchg_little_i16(0x12345678, 2, 0xffff) == 0x1234ffff ; run: %atomic_rmw_xchg_big_i16(0x12345678, 2, 0xffff) == 0x1234ffff
function %atomic_rmw_xchg_big_i8(i32, i64, i8) -> i32 { function %atomic_rmw_xchg_big_i8(i32, i64, i8) -> i32 {

View File

@@ -1,3 +1,4 @@
test interpret
test run test run
target s390x target s390x
target s390x has_mie2 target s390x has_mie2

View File

@@ -145,7 +145,7 @@ impl fmt::Debug for TestCase {
let returns = &self.main().signature.returns; let returns = &self.main().signature.returns;
let placeholder_output = returns let placeholder_output = returns
.iter() .iter()
.map(|param| DataValue::read_from_slice(&[0; 16][..], param.value_type)) .map(|param| DataValue::read_from_slice_ne(&[0; 16][..], param.value_type))
.map(|val| format!("{}", val)) .map(|val| format!("{}", val))
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(", "); .join(", ");

View File

@@ -11,8 +11,8 @@ use crate::step::{step, ControlFlow, StepError};
use crate::value::{Value, ValueError}; use crate::value::{Value, ValueError};
use cranelift_codegen::data_value::DataValue; use cranelift_codegen::data_value::DataValue;
use cranelift_codegen::ir::{ use cranelift_codegen::ir::{
ArgumentPurpose, Block, ExternalName, FuncRef, Function, GlobalValue, GlobalValueData, LibCall, ArgumentPurpose, Block, Endianness, ExternalName, FuncRef, Function, GlobalValue,
StackSlot, TrapCode, Type, Value as ValueRef, GlobalValueData, LibCall, MemFlags, StackSlot, TrapCode, Type, Value as ValueRef,
}; };
use log::trace; use log::trace;
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -192,10 +192,16 @@ pub struct InterpreterState<'a> {
pub frame_offset: usize, pub frame_offset: usize,
pub stack: Vec<u8>, pub stack: Vec<u8>,
pub pinned_reg: DataValue, pub pinned_reg: DataValue,
pub native_endianness: Endianness,
} }
impl Default for InterpreterState<'_> { impl Default for InterpreterState<'_> {
fn default() -> Self { fn default() -> Self {
let native_endianness = if cfg!(target_endian = "little") {
Endianness::Little
} else {
Endianness::Big
};
Self { Self {
functions: FunctionStore::default(), functions: FunctionStore::default(),
libcall_handler: |_, _| Err(TrapCode::UnreachableCodeReached), libcall_handler: |_, _| Err(TrapCode::UnreachableCodeReached),
@@ -203,6 +209,7 @@ impl Default for InterpreterState<'_> {
frame_offset: 0, frame_offset: 0,
stack: Vec::with_capacity(1024), stack: Vec::with_capacity(1024),
pinned_reg: DataValue::U64(0), pinned_reg: DataValue::U64(0),
native_endianness,
} }
} }
} }
@@ -308,7 +315,12 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
Address::from_parts(size, AddressRegion::Stack, 0, final_offset) Address::from_parts(size, AddressRegion::Stack, 0, final_offset)
} }
fn checked_load(&self, addr: Address, ty: Type) -> Result<DataValue, MemoryError> { fn checked_load(
&self,
addr: Address,
ty: Type,
mem_flags: MemFlags,
) -> Result<DataValue, MemoryError> {
let load_size = ty.bytes() as usize; let load_size = ty.bytes() as usize;
let addr_start = addr.offset as usize; let addr_start = addr.offset as usize;
let addr_end = addr_start + load_size; let addr_end = addr_start + load_size;
@@ -324,10 +336,18 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
_ => unimplemented!(), _ => unimplemented!(),
}; };
Ok(DataValue::read_from_slice(src, ty)) Ok(match mem_flags.endianness(self.native_endianness) {
Endianness::Big => DataValue::read_from_slice_be(src, ty),
Endianness::Little => DataValue::read_from_slice_le(src, ty),
})
} }
fn checked_store(&mut self, addr: Address, v: DataValue) -> Result<(), MemoryError> { fn checked_store(
&mut self,
addr: Address,
v: DataValue,
mem_flags: MemFlags,
) -> Result<(), MemoryError> {
let store_size = v.ty().bytes() as usize; let store_size = v.ty().bytes() as usize;
let addr_start = addr.offset as usize; let addr_start = addr.offset as usize;
let addr_end = addr_start + store_size; let addr_end = addr_start + store_size;
@@ -343,7 +363,10 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
_ => unimplemented!(), _ => unimplemented!(),
}; };
Ok(v.write_to_slice(dst)) Ok(match mem_flags.endianness(self.native_endianness) {
Endianness::Big => v.write_to_slice_be(dst),
Endianness::Little => v.write_to_slice_le(dst),
})
} }
fn function_address( fn function_address(
@@ -493,9 +516,10 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
global_type, global_type,
}) => { }) => {
let mut addr = Address::try_from(current_val)?; let mut addr = Address::try_from(current_val)?;
let mem_flags = MemFlags::trusted();
// We can forego bounds checking here since its performed in `checked_load` // We can forego bounds checking here since its performed in `checked_load`
addr.offset += offset as u64; addr.offset += offset as u64;
current_val = self.checked_load(addr, global_type)?; current_val = self.checked_load(addr, global_type, mem_flags)?;
} }
// We are done resolving this, return the current value // We are done resolving this, return the current value

View File

@@ -4,7 +4,8 @@ use crate::address::{Address, AddressSize};
use crate::interpreter::LibCallHandler; use crate::interpreter::LibCallHandler;
use cranelift_codegen::data_value::DataValue; use cranelift_codegen::data_value::DataValue;
use cranelift_codegen::ir::{ use cranelift_codegen::ir::{
ExternalName, FuncRef, Function, GlobalValue, LibCall, Signature, StackSlot, Type, Value, ExternalName, FuncRef, Function, GlobalValue, LibCall, MemFlags, Signature, StackSlot, Type,
Value,
}; };
use cranelift_codegen::isa::CallConv; use cranelift_codegen::isa::CallConv;
use cranelift_entity::PrimaryMap; use cranelift_entity::PrimaryMap;
@@ -62,10 +63,20 @@ pub trait State<'a, V> {
) -> Result<Address, MemoryError>; ) -> Result<Address, MemoryError>;
/// Retrieve a value `V` from memory at the given `address`, checking if it belongs either to the /// Retrieve a value `V` from memory at the given `address`, checking if it belongs either to the
/// stack or to one of the heaps; the number of bytes loaded corresponds to the specified [Type]. /// stack or to one of the heaps; the number of bytes loaded corresponds to the specified [Type].
fn checked_load(&self, address: Address, ty: Type) -> Result<V, MemoryError>; fn checked_load(
&self,
address: Address,
ty: Type,
mem_flags: MemFlags,
) -> Result<V, MemoryError>;
/// Store a value `V` into memory at the given `address`, checking if it belongs either to the /// Store a value `V` into memory at the given `address`, checking if it belongs either to the
/// stack or to one of the heaps; the number of bytes stored corresponds to the specified [Type]. /// stack or to one of the heaps; the number of bytes stored corresponds to the specified [Type].
fn checked_store(&mut self, address: Address, v: V) -> Result<(), MemoryError>; fn checked_store(
&mut self,
address: Address,
v: V,
mem_flags: MemFlags,
) -> Result<(), MemoryError>;
/// Compute the address of a function given its name. /// Compute the address of a function given its name.
fn function_address( fn function_address(
@@ -182,11 +193,21 @@ where
unimplemented!() unimplemented!()
} }
fn checked_load(&self, _addr: Address, _ty: Type) -> Result<V, MemoryError> { fn checked_load(
&self,
_addr: Address,
_ty: Type,
_mem_flags: MemFlags,
) -> Result<V, MemoryError> {
unimplemented!() unimplemented!()
} }
fn checked_store(&mut self, _addr: Address, _v: V) -> Result<(), MemoryError> { fn checked_store(
&mut self,
_addr: Address,
_v: V,
_mem_flags: MemFlags,
) -> Result<(), MemoryError> {
unimplemented!() unimplemented!()
} }

View File

@@ -8,7 +8,7 @@ use cranelift_codegen::data_value::DataValue;
use cranelift_codegen::ir::condcodes::{FloatCC, IntCC}; use cranelift_codegen::ir::condcodes::{FloatCC, IntCC};
use cranelift_codegen::ir::{ use cranelift_codegen::ir::{
types, AbiParam, AtomicRmwOp, Block, BlockCall, ExternalName, FuncRef, Function, types, AbiParam, AtomicRmwOp, Block, BlockCall, ExternalName, FuncRef, Function,
InstructionData, Opcode, TrapCode, Type, Value as ValueRef, InstructionData, MemFlags, Opcode, TrapCode, Type, Value as ValueRef,
}; };
use log::trace; use log::trace;
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
@@ -482,8 +482,10 @@ where
}; };
let addr_value = calculate_addr(types::I64, imm(), args()?)?; let addr_value = calculate_addr(types::I64, imm(), args()?)?;
let mem_flags = inst.memflags().expect("instruction to have memory flags");
let loaded = assign_or_memtrap( let loaded = assign_or_memtrap(
Address::try_from(addr_value).and_then(|addr| state.checked_load(addr, load_ty)), Address::try_from(addr_value)
.and_then(|addr| state.checked_load(addr, load_ty, mem_flags)),
); );
match (loaded, kind) { match (loaded, kind) {
@@ -505,33 +507,37 @@ where
}; };
let addr_value = calculate_addr(types::I64, imm(), args_range(1..)?)?; let addr_value = calculate_addr(types::I64, imm(), args_range(1..)?)?;
let mem_flags = inst.memflags().expect("instruction to have memory flags");
let reduced = if let Some(c) = kind { let reduced = if let Some(c) = kind {
arg(0)?.convert(c)? arg(0)?.convert(c)?
} else { } else {
arg(0)? arg(0)?
}; };
continue_or_memtrap( continue_or_memtrap(
Address::try_from(addr_value).and_then(|addr| state.checked_store(addr, reduced)), Address::try_from(addr_value)
.and_then(|addr| state.checked_store(addr, reduced, mem_flags)),
) )
} }
Opcode::StackLoad => { Opcode::StackLoad => {
let load_ty = inst_context.controlling_type().unwrap(); let load_ty = inst_context.controlling_type().unwrap();
let slot = inst.stack_slot().unwrap(); let slot = inst.stack_slot().unwrap();
let offset = sum(imm(), args()?)? as u64; let offset = sum(imm(), args()?)? as u64;
let mem_flags = MemFlags::trusted();
assign_or_memtrap({ assign_or_memtrap({
state state
.stack_address(AddressSize::_64, slot, offset) .stack_address(AddressSize::_64, slot, offset)
.and_then(|addr| state.checked_load(addr, load_ty)) .and_then(|addr| state.checked_load(addr, load_ty, mem_flags))
}) })
} }
Opcode::StackStore => { Opcode::StackStore => {
let arg = arg(0)?; let arg = arg(0)?;
let slot = inst.stack_slot().unwrap(); let slot = inst.stack_slot().unwrap();
let offset = sum(imm(), args_range(1..)?)? as u64; let offset = sum(imm(), args_range(1..)?)? as u64;
let mem_flags = MemFlags::trusted();
continue_or_memtrap({ continue_or_memtrap({
state state
.stack_address(AddressSize::_64, slot, offset) .stack_address(AddressSize::_64, slot, offset)
.and_then(|addr| state.checked_store(addr, arg)) .and_then(|addr| state.checked_store(addr, arg, mem_flags))
}) })
} }
Opcode::StackAddr => { Opcode::StackAddr => {
@@ -1238,7 +1244,9 @@ where
let op = inst.atomic_rmw_op().unwrap(); let op = inst.atomic_rmw_op().unwrap();
let val = arg(1)?; let val = arg(1)?;
let addr = arg(0)?.into_int()? as u64; let addr = arg(0)?.into_int()? as u64;
let loaded = Address::try_from(addr).and_then(|addr| state.checked_load(addr, ctrl_ty)); let mem_flags = inst.memflags().expect("instruction to have memory flags");
let loaded = Address::try_from(addr)
.and_then(|addr| state.checked_load(addr, ctrl_ty, mem_flags));
let prev_val = match loaded { let prev_val = match loaded {
Ok(v) => v, Ok(v) => v,
Err(e) => return Ok(ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e)))), Err(e) => return Ok(ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e)))),
@@ -1265,13 +1273,15 @@ where
) )
.and_then(|v| Value::convert(v, ValueConversionKind::ToSigned)), .and_then(|v| Value::convert(v, ValueConversionKind::ToSigned)),
}?; }?;
let stored = let stored = Address::try_from(addr)
Address::try_from(addr).and_then(|addr| state.checked_store(addr, replace)); .and_then(|addr| state.checked_store(addr, replace, mem_flags));
assign_or_memtrap(stored.map(|_| prev_val_to_assign)) assign_or_memtrap(stored.map(|_| prev_val_to_assign))
} }
Opcode::AtomicCas => { Opcode::AtomicCas => {
let addr = arg(0)?.into_int()? as u64; let addr = arg(0)?.into_int()? as u64;
let loaded = Address::try_from(addr).and_then(|addr| state.checked_load(addr, ctrl_ty)); let mem_flags = inst.memflags().expect("instruction to have memory flags");
let loaded = Address::try_from(addr)
.and_then(|addr| state.checked_load(addr, ctrl_ty, mem_flags));
let loaded_val = match loaded { let loaded_val = match loaded {
Ok(v) => v, Ok(v) => v,
Err(e) => return Ok(ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e)))), Err(e) => return Ok(ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e)))),
@@ -1280,7 +1290,7 @@ where
let val_to_assign = if Value::eq(&loaded_val, &expected_val)? { let val_to_assign = if Value::eq(&loaded_val, &expected_val)? {
let val_to_store = arg(2)?; let val_to_store = arg(2)?;
Address::try_from(addr) Address::try_from(addr)
.and_then(|addr| state.checked_store(addr, val_to_store)) .and_then(|addr| state.checked_store(addr, val_to_store, mem_flags))
.map(|_| loaded_val) .map(|_| loaded_val)
} else { } else {
Ok(loaded_val) Ok(loaded_val)
@@ -1290,17 +1300,20 @@ where
Opcode::AtomicLoad => { Opcode::AtomicLoad => {
let load_ty = inst_context.controlling_type().unwrap(); let load_ty = inst_context.controlling_type().unwrap();
let addr = arg(0)?.into_int()? as u64; let addr = arg(0)?.into_int()? as u64;
let mem_flags = inst.memflags().expect("instruction to have memory flags");
// We are doing a regular load here, this isn't actually thread safe. // We are doing a regular load here, this isn't actually thread safe.
assign_or_memtrap( assign_or_memtrap(
Address::try_from(addr).and_then(|addr| state.checked_load(addr, load_ty)), Address::try_from(addr)
.and_then(|addr| state.checked_load(addr, load_ty, mem_flags)),
) )
} }
Opcode::AtomicStore => { Opcode::AtomicStore => {
let val = arg(0)?; let val = arg(0)?;
let addr = arg(1)?.into_int()? as u64; let addr = arg(1)?.into_int()? as u64;
let mem_flags = inst.memflags().expect("instruction to have memory flags");
// We are doing a regular store here, this isn't actually thread safe. // We are doing a regular store here, this isn't actually thread safe.
continue_or_memtrap( continue_or_memtrap(
Address::try_from(addr).and_then(|addr| state.checked_store(addr, val)), Address::try_from(addr).and_then(|addr| state.checked_store(addr, val, mem_flags)),
) )
} }
Opcode::Fence => { Opcode::Fence => {