cranelift: Add stack support to the interpreter with virtual addresses (#3187)
* cranelift: Add stack support to the interpreter We also change the approach for heap loads and stores. Previously we would use the offset as the address to the heap. However, this approach does not allow using the load/store instructions to read/write from both the heap and the stack. This commit changes the addressing mechanism of the interpreter. We now return the real addresses from the addressing instructions (stack_addr/heap_addr), and instead check if the address passed into the load/store instructions points to an area in the heap or the stack. * cranelift: Add virtual addresses to cranelift interpreter Adds a Virtual Addressing scheme that was discussed as a better alternative to returning the real addresses. The virtual addresses are split into 4 regions (stack, heap, tables and global values), and the address itself is composed of an `entry` field and an `offset` field. In general the `entry` field corresponds to the instance of the resource (e.g. table5 is entry 5) and the `offset` field is a byte offset inside that entry. There is one exception to this which is the stack, where due to only having one stack, the whole address is an offset field. The number of bits in entry vs offset fields is variable with respect to the `region` and the address size (32bits vs 64bits). This is done because with 32 bit addresses we would have to compromise on heap size, or have a small number of global values / tables. With 64 bit addresses we do not have to compromise on this, but we need to support 32 bit addresses. * cranelift: Remove interpreter trap codes * cranelift: Calculate frame_offset when entering or exiting a frame * cranelift: Add safe read/write interface to DataValue * cranelift: DataValue write full 128bit slot for booleans * cranelift: Use DataValue accessors for trampoline.
This commit is contained in:
@@ -4,7 +4,6 @@ use crate::ir::immediates::{Ieee32, Ieee64, Offset32};
|
||||
use crate::ir::{types, ConstantData, Type};
|
||||
use core::convert::TryInto;
|
||||
use core::fmt::{self, Display, Formatter};
|
||||
use core::ptr;
|
||||
|
||||
/// Represent a data value. Where [Value] is an SSA reference, [DataValue] is the type + value
|
||||
/// that would be referred to by a [Value].
|
||||
@@ -74,36 +73,75 @@ impl DataValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a [DataValue] to a memory location.
|
||||
pub unsafe fn write_value_to(&self, p: *mut u128) {
|
||||
/// Write a [DataValue] to a slice.
|
||||
///
|
||||
/// # Panics:
|
||||
///
|
||||
/// Panics if the slice does not have enough space to accommodate the [DataValue]
|
||||
pub fn write_to_slice(&self, dst: &mut [u8]) {
|
||||
match self {
|
||||
DataValue::B(b) => ptr::write(p, if *b { -1i128 as u128 } else { 0u128 }),
|
||||
DataValue::I8(i) => ptr::write(p as *mut i8, *i),
|
||||
DataValue::I16(i) => ptr::write(p as *mut i16, *i),
|
||||
DataValue::I32(i) => ptr::write(p as *mut i32, *i),
|
||||
DataValue::I64(i) => ptr::write(p as *mut i64, *i),
|
||||
DataValue::F32(f) => ptr::write(p as *mut Ieee32, *f),
|
||||
DataValue::F64(f) => ptr::write(p as *mut Ieee64, *f),
|
||||
DataValue::V128(b) => ptr::write(p as *mut [u8; 16], *b),
|
||||
DataValue::B(true) => dst[..16].copy_from_slice(&[u8::MAX; 16][..]),
|
||||
DataValue::B(false) => dst[..16].copy_from_slice(&[0; 16][..]),
|
||||
DataValue::I8(i) => dst[..1].copy_from_slice(&i.to_le_bytes()[..]),
|
||||
DataValue::I16(i) => dst[..2].copy_from_slice(&i.to_le_bytes()[..]),
|
||||
DataValue::I32(i) => dst[..4].copy_from_slice(&i.to_le_bytes()[..]),
|
||||
DataValue::I64(i) => dst[..8].copy_from_slice(&i.to_le_bytes()[..]),
|
||||
DataValue::F32(f) => dst[..4].copy_from_slice(&f.bits().to_le_bytes()[..]),
|
||||
DataValue::F64(f) => dst[..8].copy_from_slice(&f.bits().to_le_bytes()[..]),
|
||||
DataValue::V128(v) => dst[..16].copy_from_slice(&v[..]),
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Read a [DataValue] from a slice using a given [Type].
|
||||
///
|
||||
/// # Panics:
|
||||
///
|
||||
/// Panics if the slice does not have enough space to accommodate the [DataValue]
|
||||
pub fn read_from_slice(src: &[u8], ty: Type) -> Self {
|
||||
match ty {
|
||||
types::I8 => DataValue::I8(i8::from_le_bytes(src[..1].try_into().unwrap())),
|
||||
types::I16 => DataValue::I16(i16::from_le_bytes(src[..2].try_into().unwrap())),
|
||||
types::I32 => DataValue::I32(i32::from_le_bytes(src[..4].try_into().unwrap())),
|
||||
types::I64 => DataValue::I64(i64::from_le_bytes(src[..8].try_into().unwrap())),
|
||||
types::F32 => DataValue::F32(Ieee32::with_bits(u32::from_le_bytes(
|
||||
src[..4].try_into().unwrap(),
|
||||
))),
|
||||
types::F64 => DataValue::F64(Ieee64::with_bits(u64::from_le_bytes(
|
||||
src[..8].try_into().unwrap(),
|
||||
))),
|
||||
_ if ty.is_bool() => {
|
||||
// Only `ty.bytes()` are guaranteed to be written
|
||||
// so we can only test the first n bytes of `src`
|
||||
|
||||
let size = ty.bytes() as usize;
|
||||
DataValue::B(src[..size].iter().any(|&i| i != 0))
|
||||
}
|
||||
_ if ty.is_vector() && ty.bytes() == 16 => {
|
||||
DataValue::V128(src[..16].try_into().unwrap())
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a [DataValue] to a memory location.
|
||||
pub unsafe fn write_value_to(&self, p: *mut u128) {
|
||||
// Since `DataValue` does not have type info for bools we always
|
||||
// write out a full 16 byte slot.
|
||||
let size = match self.ty() {
|
||||
ty if ty.is_bool() => 16,
|
||||
ty => ty.bytes() as usize,
|
||||
};
|
||||
|
||||
self.write_to_slice(std::slice::from_raw_parts_mut(p as *mut u8, size));
|
||||
}
|
||||
|
||||
/// Read a [DataValue] from a memory location using a given [Type].
|
||||
pub unsafe fn read_value_from(p: *const u128, ty: Type) -> Self {
|
||||
match ty {
|
||||
types::I8 => DataValue::I8(ptr::read(p as *const i8)),
|
||||
types::I16 => DataValue::I16(ptr::read(p as *const i16)),
|
||||
types::I32 => DataValue::I32(ptr::read(p as *const i32)),
|
||||
types::I64 => DataValue::I64(ptr::read(p as *const i64)),
|
||||
types::F32 => DataValue::F32(ptr::read(p as *const Ieee32)),
|
||||
types::F64 => DataValue::F64(ptr::read(p as *const Ieee64)),
|
||||
_ if ty.is_bool() => DataValue::B(ptr::read(p) != 0),
|
||||
_ if ty.is_vector() && ty.bytes() == 16 => {
|
||||
DataValue::V128(ptr::read(p as *const [u8; 16]))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
DataValue::read_from_slice(
|
||||
std::slice::from_raw_parts(p as *const u8, ty.bytes() as usize),
|
||||
ty,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -421,6 +421,13 @@ impl Function {
|
||||
self.dfg[dst] = self.dfg[src].clone();
|
||||
self.layout.remove_inst(src);
|
||||
}
|
||||
|
||||
/// Size occupied by all stack slots associated with this function.
|
||||
///
|
||||
/// Does not include any padding necessary due to offsets
|
||||
pub fn stack_size(&self) -> u32 {
|
||||
self.stack_slots.values().map(|ss| ss.size).sum()
|
||||
}
|
||||
}
|
||||
|
||||
/// Additional annotations for function display.
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::ir::{
|
||||
self,
|
||||
condcodes::{FloatCC, IntCC},
|
||||
trapcode::TrapCode,
|
||||
types, Block, FuncRef, JumpTable, MemFlags, SigRef, Type, Value,
|
||||
types, Block, FuncRef, JumpTable, MemFlags, SigRef, StackSlot, Type, Value,
|
||||
};
|
||||
use crate::isa;
|
||||
|
||||
@@ -411,6 +411,15 @@ impl InstructionData {
|
||||
}
|
||||
}
|
||||
|
||||
/// If this instruction references a stack slot, return it
|
||||
pub fn stack_slot(&self) -> Option<StackSlot> {
|
||||
match self {
|
||||
&InstructionData::StackStore { stack_slot, .. }
|
||||
| &InstructionData::StackLoad { stack_slot, .. } => Some(stack_slot),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return information about a call instruction.
|
||||
///
|
||||
/// Any instruction that can call another function reveals its call signature here.
|
||||
|
||||
85
cranelift/filetests/filetests/runtests/stack-addr-32.clif
Normal file
85
cranelift/filetests/filetests/runtests/stack-addr-32.clif
Normal file
@@ -0,0 +1,85 @@
|
||||
test interpret
|
||||
|
||||
function %stack_addr_iadd(i64) -> b1 {
|
||||
ss0 = explicit_slot 16
|
||||
|
||||
block0(v0: i64):
|
||||
v1 = stack_addr.i32 ss0
|
||||
v2 = iadd_imm.i32 v1, 8
|
||||
|
||||
stack_store.i64 v0, ss0+8
|
||||
v3 = load.i64 v2
|
||||
|
||||
v5 = iadd_imm.i64 v0, 20
|
||||
store.i64 v5, v2
|
||||
v6 = stack_load.i64 ss0+8
|
||||
|
||||
v7 = icmp eq v0, v3
|
||||
v8 = icmp eq v5, v6
|
||||
v9 = band v7, v8
|
||||
return v9
|
||||
}
|
||||
; run: %stack_addr_iadd(0) == true
|
||||
; run: %stack_addr_iadd(1) == true
|
||||
; run: %stack_addr_iadd(-1) == true
|
||||
|
||||
|
||||
function %stack_addr_32(i64) -> b1 {
|
||||
ss0 = explicit_slot 24
|
||||
|
||||
block0(v0: i64):
|
||||
v1 = stack_addr.i32 ss0
|
||||
stack_store.i64 v0, ss0
|
||||
v2 = load.i64 v1
|
||||
v3 = icmp eq v0, v2
|
||||
|
||||
v4 = stack_addr.i32 ss0+8
|
||||
store.i64 v0, v4
|
||||
v5 = stack_load.i64 ss0+8
|
||||
v6 = icmp eq v0, v5
|
||||
|
||||
v7 = stack_addr.i32 ss0+16
|
||||
store.i64 v0, v7
|
||||
v8 = load.i64 v7
|
||||
v9 = icmp eq v0, v8
|
||||
|
||||
v10 = band v3, v6
|
||||
v11 = band v10, v9
|
||||
return v11
|
||||
}
|
||||
; run: %stack_addr_32(0) == true
|
||||
; run: %stack_addr_32(1) == true
|
||||
; run: %stack_addr_32(-1) == true
|
||||
|
||||
|
||||
|
||||
function %addr32_64(i64) -> b1 {
|
||||
ss0 = explicit_slot 16
|
||||
|
||||
block0(v0: i64):
|
||||
v1 = stack_addr.i32 ss0+8
|
||||
v2 = stack_addr.i64 ss0+8
|
||||
|
||||
store.i64 v0, v1
|
||||
v3 = load.i64 v2
|
||||
|
||||
v4 = icmp eq v3, v0
|
||||
|
||||
return v4
|
||||
}
|
||||
; run: %addr32_64(0) == true
|
||||
; run: %addr32_64(1) == true
|
||||
; run: %addr32_64(-1) == true
|
||||
|
||||
|
||||
function %multi_slot_different_addrs() -> b1 {
|
||||
ss0 = explicit_slot 8
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = stack_addr.i32 ss0
|
||||
v1 = stack_addr.i32 ss1
|
||||
v2 = icmp ne v0, v1
|
||||
return v2
|
||||
}
|
||||
; run: %multi_slot_diffe() == true
|
||||
56
cranelift/filetests/filetests/runtests/stack-addr-64.clif
Normal file
56
cranelift/filetests/filetests/runtests/stack-addr-64.clif
Normal file
@@ -0,0 +1,56 @@
|
||||
test interpret
|
||||
test run
|
||||
target x86_64 machinst
|
||||
target s390x
|
||||
target aarch64
|
||||
|
||||
|
||||
function %stack_addr_iadd(i64) -> b1 {
|
||||
ss0 = explicit_slot 16
|
||||
|
||||
block0(v0: i64):
|
||||
v1 = stack_addr.i64 ss0
|
||||
v2 = iadd_imm.i64 v1, 8
|
||||
|
||||
stack_store.i64 v0, ss0+8
|
||||
v3 = load.i64 v2
|
||||
|
||||
v5 = iadd_imm.i64 v0, 20
|
||||
store.i64 v5, v2
|
||||
v6 = stack_load.i64 ss0+8
|
||||
|
||||
v7 = icmp eq v0, v3
|
||||
v8 = icmp eq v5, v6
|
||||
v9 = band v7, v8
|
||||
return v9
|
||||
}
|
||||
; run: %stack_addr_iadd(0) == true
|
||||
; run: %stack_addr_iadd(1) == true
|
||||
; run: %stack_addr_iadd(-1) == true
|
||||
|
||||
function %stack_addr_64(i64) -> b1 {
|
||||
ss0 = explicit_slot 24
|
||||
|
||||
block0(v0: i64):
|
||||
v1 = stack_addr.i64 ss0
|
||||
stack_store.i64 v0, ss0
|
||||
v2 = load.i64 v1
|
||||
v3 = icmp eq v0, v2
|
||||
|
||||
v4 = stack_addr.i64 ss0+8
|
||||
store.i64 v0, v4
|
||||
v5 = stack_load.i64 ss0+8
|
||||
v6 = icmp eq v0, v5
|
||||
|
||||
v7 = stack_addr.i64 ss0+16
|
||||
store.i64 v0, v7
|
||||
v8 = load.i64 v7
|
||||
v9 = icmp eq v0, v8
|
||||
|
||||
v10 = band v3, v6
|
||||
v11 = band v10, v9
|
||||
return v11
|
||||
}
|
||||
; run: %stack_addr_64(0) == true
|
||||
; run: %stack_addr_64(1) == true
|
||||
; run: %stack_addr_64(-1) == true
|
||||
130
cranelift/filetests/filetests/runtests/stack.clif
Normal file
130
cranelift/filetests/filetests/runtests/stack.clif
Normal file
@@ -0,0 +1,130 @@
|
||||
test interpret
|
||||
test run
|
||||
target x86_64 machinst
|
||||
target s390x
|
||||
target aarch64
|
||||
|
||||
function %stack_simple(i64) -> i64 {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0(v0: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
v1 = stack_load.i64 ss0
|
||||
return v1
|
||||
}
|
||||
; run: %stack_simple(0) == 0
|
||||
; run: %stack_simple(1) == 1
|
||||
; run: %stack_simple(-1) == -1
|
||||
|
||||
|
||||
function %slot_offset(i64) -> i64 {
|
||||
ss0 = explicit_slot 8, offset 8
|
||||
|
||||
block0(v0: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
v1 = stack_load.i64 ss0
|
||||
return v1
|
||||
}
|
||||
; run: %slot_offset(0) == 0
|
||||
; run: %slot_offset(1) == 1
|
||||
; run: %slot_offset(-1) == -1
|
||||
|
||||
function %stack_offset(i64) -> i64 {
|
||||
ss0 = explicit_slot 16
|
||||
|
||||
block0(v0: i64):
|
||||
stack_store.i64 v0, ss0+8
|
||||
v1 = stack_load.i64 ss0+8
|
||||
return v1
|
||||
}
|
||||
; run: %stack_offset(0) == 0
|
||||
; run: %stack_offset(1) == 1
|
||||
; run: %stack_offset(-1) == -1
|
||||
|
||||
|
||||
function %offset_unaligned(i64) -> i64 {
|
||||
ss0 = explicit_slot 11
|
||||
|
||||
block0(v0: i64):
|
||||
stack_store.i64 v0, ss0+3
|
||||
v1 = stack_load.i64 ss0+3
|
||||
return v1
|
||||
}
|
||||
; run: %offset_unaligned(0) == 0
|
||||
; run: %offset_unaligned(1) == 1
|
||||
; run: %offset_unaligned(-1) == -1
|
||||
|
||||
|
||||
|
||||
function %multi_slot_stack(i64, i64) -> i64 {
|
||||
ss0 = explicit_slot 8
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0(v0: i64, v1: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
v2 = stack_load.i64 ss0
|
||||
v3 = stack_load.i64 ss1
|
||||
v4 = iadd.i64 v2, v3
|
||||
return v4
|
||||
}
|
||||
; run: %multi_slot_stack(0, 1) == 1
|
||||
; run: %multi_slot_stack(1, 2) == 3
|
||||
|
||||
|
||||
|
||||
function %multi_slot_out_of_bounds_writes(i8, i64) -> i8, i64 {
|
||||
ss0 = explicit_slot 1
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0(v0: i8, v1: i64):
|
||||
stack_store.i8 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
v2 = stack_load.i8 ss0
|
||||
v3 = stack_load.i64 ss1
|
||||
return v2, v3
|
||||
}
|
||||
; run: %multi_slot_out_o(10, 1) == [10, 1]
|
||||
; run: %multi_slot_out_o(0, 2) == [0, 2]
|
||||
|
||||
|
||||
function %multi_slot_offset_writes(i8, i64) -> i8, i64 {
|
||||
ss0 = explicit_slot 8, offset 8
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0(v0: i8, v1: i64):
|
||||
stack_store.i8 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
v2 = stack_load.i8 ss0
|
||||
v3 = stack_load.i64 ss1
|
||||
return v2, v3
|
||||
}
|
||||
; run: %multi_slot_offse(0, 1) == [0, 1]
|
||||
; run: %multi_slot_offse(1, 2) == [1, 2]
|
||||
|
||||
function %slot_offset_negative(i64, i64) -> i64, i64 {
|
||||
ss0 = explicit_slot 8
|
||||
ss1 = explicit_slot 8, offset -8
|
||||
|
||||
block0(v0: i64, v1: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
v2 = stack_load.i64 ss0
|
||||
v3 = stack_load.i64 ss1
|
||||
return v2, v3
|
||||
}
|
||||
; run: %slot_offset_nega(0, 1) == [0, 1]
|
||||
; run: %slot_offset_nega(2, 3) == [2, 3]
|
||||
|
||||
|
||||
function %huge_slots(i64) -> i64 {
|
||||
ss0 = explicit_slot 1048576 ; 1MB Slot
|
||||
|
||||
block0(v0: i64):
|
||||
stack_store.i64 v0, ss0+1048568 ; Store at 1MB - 8bytes
|
||||
v1 = stack_load.i64 ss0+1048568
|
||||
return v1
|
||||
}
|
||||
; run: %huge_slots(0) == 0
|
||||
; run: %huge_slots(1) == 1
|
||||
; run: %huge_slots(-1) == -1
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Provides functionality for compiling and running CLIF IR for `run` tests.
|
||||
use core::{mem, ptr};
|
||||
use core::mem;
|
||||
use cranelift_codegen::binemit::{NullRelocSink, NullStackMapSink, NullTrapSink};
|
||||
use cranelift_codegen::data_value::DataValue;
|
||||
use cranelift_codegen::ir::immediates::{Ieee32, Ieee64};
|
||||
use cranelift_codegen::ir::{condcodes::IntCC, Function, InstBuilder, Signature, Type};
|
||||
use cranelift_codegen::isa::{BackendVariant, TargetIsa};
|
||||
use cranelift_codegen::{ir, settings, CodegenError, Context};
|
||||
@@ -204,7 +203,7 @@ impl UnboxedValues {
|
||||
param.value_type
|
||||
);
|
||||
unsafe {
|
||||
Self::write_value_to(arg, slot);
|
||||
arg.write_value_to(slot);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,50 +223,12 @@ impl UnboxedValues {
|
||||
|
||||
// Extract the returned values from this vector.
|
||||
for (slot, param) in self.0.iter().zip(&signature.returns) {
|
||||
let value = unsafe { Self::read_value_from(slot, param.value_type) };
|
||||
let value = unsafe { DataValue::read_value_from(slot, param.value_type) };
|
||||
returns.push(value);
|
||||
}
|
||||
|
||||
returns
|
||||
}
|
||||
|
||||
/// Write a [DataValue] to a memory location.
|
||||
unsafe fn write_value_to(v: &DataValue, p: *mut u128) {
|
||||
match v {
|
||||
DataValue::B(b) => ptr::write(p as *mut bool, *b),
|
||||
DataValue::I8(i) => ptr::write(p as *mut i8, *i),
|
||||
DataValue::I16(i) => ptr::write(p as *mut i16, *i),
|
||||
DataValue::I32(i) => ptr::write(p as *mut i32, *i),
|
||||
DataValue::I64(i) => ptr::write(p as *mut i64, *i),
|
||||
DataValue::F32(f) => ptr::write(p as *mut Ieee32, *f),
|
||||
DataValue::F64(f) => ptr::write(p as *mut Ieee64, *f),
|
||||
DataValue::V128(b) => ptr::write(p as *mut [u8; 16], *b),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a [DataValue] from a memory location using a given [Type].
|
||||
unsafe fn read_value_from(p: *const u128, ty: Type) -> DataValue {
|
||||
match ty {
|
||||
ir::types::I8 => DataValue::I8(ptr::read(p as *const i8)),
|
||||
ir::types::I16 => DataValue::I16(ptr::read(p as *const i16)),
|
||||
ir::types::I32 => DataValue::I32(ptr::read(p as *const i32)),
|
||||
ir::types::I64 => DataValue::I64(ptr::read(p as *const i64)),
|
||||
ir::types::F32 => DataValue::F32(ptr::read(p as *const Ieee32)),
|
||||
ir::types::F64 => DataValue::F64(ptr::read(p as *const Ieee64)),
|
||||
_ if ty.is_bool() => match ty.bytes() {
|
||||
1 => DataValue::B(ptr::read(p as *const i8) != 0),
|
||||
2 => DataValue::B(ptr::read(p as *const i16) != 0),
|
||||
4 => DataValue::B(ptr::read(p as *const i32) != 0),
|
||||
8 => DataValue::B(ptr::read(p as *const i64) != 0),
|
||||
_ => unimplemented!(),
|
||||
},
|
||||
_ if ty.is_vector() && ty.bytes() == 16 => {
|
||||
DataValue::V128(ptr::read(p as *const [u8; 16]))
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile a [Function] to its executable bytes in memory.
|
||||
|
||||
297
cranelift/interpreter/src/address.rs
Normal file
297
cranelift/interpreter/src/address.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
//! Virtual Addressing Scheme for the Interpreter
|
||||
//!
|
||||
//! The interpreter uses virtual memory addresses for its memory operations. These addresses
|
||||
//! are obtained by the various `_addr` instructions (e.g. `stack_addr`) and can be either 32 or 64
|
||||
//! bits.
|
||||
//!
|
||||
//! Addresses are composed of 3 fields: "region", "entry" and offset.
|
||||
//!
|
||||
//! "region" refers to the type of memory that this address points to.
|
||||
//! "entry" refers to which instance of this memory the address points to (e.g table1 would be
|
||||
//! "entry" 1 of a `Table` region address).
|
||||
//! The last field is the "offset", which refers to the offset within the entry.
|
||||
//!
|
||||
//! The address has the "region" field as the 2 most significant bits. The following bits
|
||||
//! are the "entry" field, the amount of "entry" bits depends on the size of the address and
|
||||
//! the "region" of the address. The remaining bits belong to the "offset" field
|
||||
//!
|
||||
//! An example address could be a 32 bit address, in the `heap` region, which has 2 "entry" bits
|
||||
//! this address would have 32 - 2 - 2 = 28 offset bits.
|
||||
//!
|
||||
//! The only exception to this is the "stack" region, where, because we only have a single "stack"
|
||||
//! we have 0 "entry" bits, and thus is all offset.
|
||||
//!
|
||||
//! | address size | address kind | region value (2 bits) | entry bits (#) | offset bits (#) |
|
||||
//! |--------------|--------------|-----------------------|----------------|-----------------|
|
||||
//! | 32 | Stack | 0b00 | 0 | 30 |
|
||||
//! | 32 | Heap | 0b01 | 2 | 28 |
|
||||
//! | 32 | Table | 0b10 | 5 | 25 |
|
||||
//! | 32 | GlobalValue | 0b11 | 6 | 24 |
|
||||
//! | 64 | Stack | 0b00 | 0 | 62 |
|
||||
//! | 64 | Heap | 0b01 | 6 | 56 |
|
||||
//! | 64 | Table | 0b10 | 10 | 52 |
|
||||
//! | 64 | GlobalValue | 0b11 | 12 | 50 |
|
||||
|
||||
use crate::state::MemoryError;
|
||||
use cranelift_codegen::data_value::DataValue;
|
||||
use cranelift_codegen::ir::{types, Type};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum AddressSize {
|
||||
_32,
|
||||
_64,
|
||||
}
|
||||
|
||||
impl AddressSize {
|
||||
pub fn bits(&self) -> u64 {
|
||||
match self {
|
||||
AddressSize::_64 => 64,
|
||||
AddressSize::_32 => 32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Type> for AddressSize {
|
||||
type Error = MemoryError;
|
||||
|
||||
fn try_from(ty: Type) -> Result<Self, Self::Error> {
|
||||
match ty {
|
||||
types::I64 => Ok(AddressSize::_64),
|
||||
types::I32 => Ok(AddressSize::_32),
|
||||
_ => Err(MemoryError::InvalidAddressType(ty)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual Address region
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum AddressRegion {
|
||||
Stack,
|
||||
Heap,
|
||||
Table,
|
||||
GlobalValue,
|
||||
}
|
||||
|
||||
impl AddressRegion {
|
||||
pub fn decode(bits: u64) -> Self {
|
||||
assert!(bits < 4);
|
||||
match bits {
|
||||
0 => AddressRegion::Stack,
|
||||
1 => AddressRegion::Heap,
|
||||
2 => AddressRegion::Table,
|
||||
3 => AddressRegion::GlobalValue,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(self) -> u64 {
|
||||
match self {
|
||||
AddressRegion::Stack => 0,
|
||||
AddressRegion::Heap => 1,
|
||||
AddressRegion::Table => 2,
|
||||
AddressRegion::GlobalValue => 3,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Address {
|
||||
pub size: AddressSize,
|
||||
pub region: AddressRegion,
|
||||
pub entry: u64,
|
||||
pub offset: u64,
|
||||
}
|
||||
|
||||
impl Address {
|
||||
pub fn from_parts(
|
||||
size: AddressSize,
|
||||
region: AddressRegion,
|
||||
entry: u64,
|
||||
offset: u64,
|
||||
) -> Result<Self, MemoryError> {
|
||||
let entry_bits = Address::entry_bits(size, region);
|
||||
let offset_bits = Address::offset_bits(size, region);
|
||||
|
||||
let max_entries = (1 << entry_bits) - 1;
|
||||
let max_offset = (1 << offset_bits) - 1;
|
||||
|
||||
if entry > max_entries {
|
||||
return Err(MemoryError::InvalidEntry {
|
||||
entry,
|
||||
max: max_entries,
|
||||
});
|
||||
}
|
||||
|
||||
if offset > max_offset {
|
||||
return Err(MemoryError::InvalidOffset {
|
||||
offset,
|
||||
max: max_offset,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Address {
|
||||
size,
|
||||
region,
|
||||
entry,
|
||||
offset,
|
||||
})
|
||||
}
|
||||
|
||||
fn entry_bits(size: AddressSize, region: AddressRegion) -> u64 {
|
||||
match (size, region) {
|
||||
// We only have one stack, so the whole address is offset
|
||||
(_, AddressRegion::Stack) => 0,
|
||||
|
||||
(AddressSize::_32, AddressRegion::Heap) => 2,
|
||||
(AddressSize::_32, AddressRegion::Table) => 5,
|
||||
(AddressSize::_32, AddressRegion::GlobalValue) => 6,
|
||||
|
||||
(AddressSize::_64, AddressRegion::Heap) => 6,
|
||||
(AddressSize::_64, AddressRegion::Table) => 10,
|
||||
(AddressSize::_64, AddressRegion::GlobalValue) => 12,
|
||||
}
|
||||
}
|
||||
|
||||
fn offset_bits(size: AddressSize, region: AddressRegion) -> u64 {
|
||||
let region_bits = 2;
|
||||
let entry_bits = Address::entry_bits(size, region);
|
||||
size.bits() - entry_bits - region_bits
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Address> for DataValue {
|
||||
type Error = MemoryError;
|
||||
|
||||
fn try_from(addr: Address) -> Result<Self, Self::Error> {
|
||||
let entry_bits = Address::entry_bits(addr.size, addr.region);
|
||||
let offset_bits = Address::offset_bits(addr.size, addr.region);
|
||||
|
||||
let entry = addr.entry << offset_bits;
|
||||
let region = addr.region.encode() << (entry_bits + offset_bits);
|
||||
|
||||
let value = region | entry | addr.offset;
|
||||
Ok(match addr.size {
|
||||
AddressSize::_32 => DataValue::I32(value as u32 as i32),
|
||||
AddressSize::_64 => DataValue::I64(value as i64),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<DataValue> for Address {
|
||||
type Error = MemoryError;
|
||||
|
||||
fn try_from(value: DataValue) -> Result<Self, Self::Error> {
|
||||
let addr = match value {
|
||||
DataValue::U32(v) => v as u64,
|
||||
DataValue::I32(v) => v as u32 as u64,
|
||||
DataValue::U64(v) => v,
|
||||
DataValue::I64(v) => v as u64,
|
||||
_ => {
|
||||
return Err(MemoryError::InvalidAddress(value));
|
||||
}
|
||||
};
|
||||
|
||||
let size = match value {
|
||||
DataValue::U32(_) | DataValue::I32(_) => AddressSize::_32,
|
||||
DataValue::U64(_) | DataValue::I64(_) => AddressSize::_64,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let region = AddressRegion::decode(addr >> (size.bits() - 2));
|
||||
|
||||
let entry_bits = Address::entry_bits(size, region);
|
||||
let offset_bits = Address::offset_bits(size, region);
|
||||
|
||||
let entry = (addr >> offset_bits) & ((1 << entry_bits) - 1);
|
||||
let offset = addr & ((1 << offset_bits) - 1);
|
||||
|
||||
Address::from_parts(size, region, entry, offset)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u64> for Address {
|
||||
type Error = MemoryError;
|
||||
|
||||
fn try_from(value: u64) -> Result<Self, Self::Error> {
|
||||
let dv = if value > u32::MAX as u64 {
|
||||
DataValue::U64(value)
|
||||
} else {
|
||||
DataValue::U32(value as u32)
|
||||
};
|
||||
|
||||
Address::try_from(dv)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[test]
|
||||
fn address_region_roundtrip_encode_decode() {
|
||||
let all_regions = [
|
||||
AddressRegion::Stack,
|
||||
AddressRegion::Heap,
|
||||
AddressRegion::Table,
|
||||
AddressRegion::GlobalValue,
|
||||
];
|
||||
|
||||
for region in all_regions {
|
||||
assert_eq!(AddressRegion::decode(region.encode()), region);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn address_roundtrip() {
|
||||
let test_addresses = [
|
||||
(AddressSize::_32, AddressRegion::Stack, 0, 0),
|
||||
(AddressSize::_32, AddressRegion::Stack, 0, 1),
|
||||
(AddressSize::_32, AddressRegion::Stack, 0, 1024),
|
||||
(AddressSize::_32, AddressRegion::Stack, 0, 0x3FFF_FFFF),
|
||||
(AddressSize::_32, AddressRegion::Heap, 0, 0),
|
||||
(AddressSize::_32, AddressRegion::Heap, 1, 1),
|
||||
(AddressSize::_32, AddressRegion::Heap, 3, 1024),
|
||||
(AddressSize::_32, AddressRegion::Heap, 3, 0x0FFF_FFFF),
|
||||
(AddressSize::_32, AddressRegion::Table, 0, 0),
|
||||
(AddressSize::_32, AddressRegion::Table, 1, 1),
|
||||
(AddressSize::_32, AddressRegion::Table, 31, 0x1FF_FFFF),
|
||||
(AddressSize::_32, AddressRegion::GlobalValue, 0, 0),
|
||||
(AddressSize::_32, AddressRegion::GlobalValue, 1, 1),
|
||||
(AddressSize::_32, AddressRegion::GlobalValue, 63, 0xFF_FFFF),
|
||||
(AddressSize::_64, AddressRegion::Stack, 0, 0),
|
||||
(AddressSize::_64, AddressRegion::Stack, 0, 1),
|
||||
(
|
||||
AddressSize::_64,
|
||||
AddressRegion::Stack,
|
||||
0,
|
||||
0x3FFFFFFF_FFFFFFFF,
|
||||
),
|
||||
(AddressSize::_64, AddressRegion::Heap, 0, 0),
|
||||
(AddressSize::_64, AddressRegion::Heap, 1, 1),
|
||||
(AddressSize::_64, AddressRegion::Heap, 3, 1024),
|
||||
(AddressSize::_64, AddressRegion::Heap, 3, 0x0FFF_FFFF),
|
||||
(AddressSize::_64, AddressRegion::Table, 0, 0),
|
||||
(AddressSize::_64, AddressRegion::Table, 1, 1),
|
||||
(AddressSize::_64, AddressRegion::Table, 31, 0x1FF_FFFF),
|
||||
(AddressSize::_64, AddressRegion::GlobalValue, 0, 0),
|
||||
(AddressSize::_64, AddressRegion::GlobalValue, 1, 1),
|
||||
(AddressSize::_64, AddressRegion::GlobalValue, 63, 0xFF_FFFF),
|
||||
];
|
||||
|
||||
for (size, region, entry, offset) in test_addresses {
|
||||
let original = Address {
|
||||
size,
|
||||
region,
|
||||
entry,
|
||||
offset,
|
||||
};
|
||||
|
||||
let dv: DataValue = original.clone().try_into().unwrap();
|
||||
let addr = dv.try_into().unwrap();
|
||||
|
||||
assert_eq!(original, addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module partially contains the logic for interpreting Cranelift IR.
|
||||
|
||||
use crate::address::{Address, AddressRegion, AddressSize};
|
||||
use crate::environment::{FuncIndex, FunctionStore};
|
||||
use crate::frame::Frame;
|
||||
use crate::instruction::DfgInstructionContext;
|
||||
@@ -10,10 +11,11 @@ use crate::step::{step, ControlFlow, StepError};
|
||||
use crate::value::ValueError;
|
||||
use cranelift_codegen::data_value::DataValue;
|
||||
use cranelift_codegen::ir::condcodes::{FloatCC, IntCC};
|
||||
use cranelift_codegen::ir::{Block, FuncRef, Function, Type, Value as ValueRef};
|
||||
use cranelift_codegen::ir::{Block, FuncRef, Function, StackSlot, Type, Value as ValueRef};
|
||||
use log::trace;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Debug;
|
||||
use std::iter;
|
||||
use thiserror::Error;
|
||||
|
||||
/// The Cranelift interpreter; this contains some high-level functions to control the interpreter's
|
||||
@@ -80,6 +82,7 @@ impl<'a> Interpreter<'a> {
|
||||
self.state
|
||||
.current_frame_mut()
|
||||
.set_all(parameters, arguments.to_vec());
|
||||
|
||||
self.block(first_block)
|
||||
}
|
||||
|
||||
@@ -173,6 +176,9 @@ pub enum InterpreterError {
|
||||
pub struct InterpreterState<'a> {
|
||||
pub functions: FunctionStore<'a>,
|
||||
pub frame_stack: Vec<Frame<'a>>,
|
||||
/// Number of bytes from the bottom of the stack where the current frame's stack space is
|
||||
pub frame_offset: usize,
|
||||
pub stack: Vec<u8>,
|
||||
pub heap: Vec<u8>,
|
||||
pub iflags: HashSet<IntCC>,
|
||||
pub fflags: HashSet<FloatCC>,
|
||||
@@ -183,6 +189,8 @@ impl Default for InterpreterState<'_> {
|
||||
Self {
|
||||
functions: FunctionStore::default(),
|
||||
frame_stack: vec![],
|
||||
frame_offset: 0,
|
||||
stack: Vec::with_capacity(1024),
|
||||
heap: vec![0; 1024],
|
||||
iflags: HashSet::new(),
|
||||
fflags: HashSet::new(),
|
||||
@@ -222,10 +230,27 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
|
||||
}
|
||||
|
||||
fn push_frame(&mut self, function: &'a Function) {
|
||||
if let Some(frame) = self.frame_stack.iter().last() {
|
||||
self.frame_offset += frame.function.stack_size() as usize;
|
||||
}
|
||||
|
||||
// Grow the stack by the space necessary for this frame
|
||||
self.stack
|
||||
.extend(iter::repeat(0).take(function.stack_size() as usize));
|
||||
|
||||
self.frame_stack.push(Frame::new(function));
|
||||
}
|
||||
fn pop_frame(&mut self) {
|
||||
self.frame_stack.pop();
|
||||
if let Some(frame) = self.frame_stack.pop() {
|
||||
// Shorten the stack after exiting the frame
|
||||
self.stack
|
||||
.truncate(self.stack.len() - frame.function.stack_size() as usize);
|
||||
|
||||
// Reset frame_offset to the start of this function
|
||||
if let Some(frame) = self.frame_stack.iter().last() {
|
||||
self.frame_offset -= frame.function.stack_size() as usize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_value(&self, name: ValueRef) -> Option<DataValue> {
|
||||
@@ -257,30 +282,74 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
|
||||
self.fflags.clear()
|
||||
}
|
||||
|
||||
fn load_heap(&self, offset: usize, ty: Type) -> Result<DataValue, MemoryError> {
|
||||
if offset + 16 < self.heap.len() {
|
||||
let pointer = self.heap[offset..offset + 16].as_ptr() as *const _ as *const u128;
|
||||
Ok(unsafe { DataValue::read_value_from(pointer, ty) })
|
||||
} else {
|
||||
Err(MemoryError::InsufficientMemory(offset, self.heap.len()))
|
||||
fn stack_address(
|
||||
&self,
|
||||
size: AddressSize,
|
||||
slot: StackSlot,
|
||||
offset: u64,
|
||||
) -> Result<Address, MemoryError> {
|
||||
let stack_slots = &self.get_current_function().stack_slots;
|
||||
let stack_slot = &stack_slots[slot];
|
||||
|
||||
// offset must be `0 <= Offset < sizeof(SS)`
|
||||
if offset >= stack_slot.size as u64 {
|
||||
return Err(MemoryError::InvalidOffset {
|
||||
offset,
|
||||
max: stack_slot.size as u64,
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate the offset from the current frame to the requested stack slot
|
||||
let slot_offset: u64 = stack_slots
|
||||
.keys()
|
||||
.filter(|k| k < &slot)
|
||||
.map(|k| stack_slots[k].size as u64)
|
||||
.sum();
|
||||
|
||||
let final_offset = self.frame_offset as u64 + slot_offset + offset;
|
||||
Address::from_parts(size, AddressRegion::Stack, 0, final_offset)
|
||||
}
|
||||
|
||||
fn store_heap(&mut self, offset: usize, v: DataValue) -> Result<(), MemoryError> {
|
||||
if offset + 16 < self.heap.len() {
|
||||
let pointer = self.heap[offset..offset + 16].as_mut_ptr() as *mut _ as *mut u128;
|
||||
Ok(unsafe { v.write_value_to(pointer) })
|
||||
} else {
|
||||
Err(MemoryError::InsufficientMemory(offset, self.heap.len()))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_stack(&self, _offset: usize, _ty: Type) -> Result<DataValue, MemoryError> {
|
||||
fn heap_address(&self, _size: AddressSize, _offset: u64) -> Result<Address, MemoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn store_stack(&mut self, _offset: usize, _v: DataValue) -> Result<(), MemoryError> {
|
||||
unimplemented!()
|
||||
fn checked_load(&self, addr: Address, ty: Type) -> Result<DataValue, MemoryError> {
|
||||
let load_size = ty.bytes() as usize;
|
||||
|
||||
let src = match addr.region {
|
||||
AddressRegion::Stack => {
|
||||
let addr_start = addr.offset as usize;
|
||||
let addr_end = addr_start + load_size;
|
||||
if addr_end > self.stack.len() {
|
||||
return Err(MemoryError::OutOfBoundsLoad { addr, load_size });
|
||||
}
|
||||
|
||||
&self.stack[addr_start..addr_end]
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
Ok(DataValue::read_from_slice(src, ty))
|
||||
}
|
||||
|
||||
fn checked_store(&mut self, addr: Address, v: DataValue) -> Result<(), MemoryError> {
|
||||
let store_size = v.ty().bytes() as usize;
|
||||
|
||||
let dst = match addr.region {
|
||||
AddressRegion::Stack => {
|
||||
let addr_start = addr.offset as usize;
|
||||
let addr_end = addr_start + store_size;
|
||||
if addr_end > self.stack.len() {
|
||||
return Err(MemoryError::OutOfBoundsStore { addr, store_size });
|
||||
}
|
||||
|
||||
&mut self.stack[addr_start..addr_end]
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
Ok(v.write_to_slice(dst))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +357,6 @@ impl<'a> State<'a, DataValue> for InterpreterState<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::step::CraneliftTrap;
|
||||
use cranelift_codegen::ir::immediates::Ieee32;
|
||||
use cranelift_codegen::ir::TrapCode;
|
||||
use cranelift_reader::parse_functions;
|
||||
|
||||
@@ -332,12 +400,12 @@ mod tests {
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let result = Interpreter::new(state).call_by_name("%test", &[]).unwrap();
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%test", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
match result {
|
||||
ControlFlow::Trap(CraneliftTrap::User(TrapCode::IntegerDivisionByZero)) => {}
|
||||
_ => panic!("Unexpected ControlFlow: {:?}", result),
|
||||
}
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::IntegerDivisionByZero));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -395,20 +463,6 @@ mod tests {
|
||||
assert_eq!(result, vec![DataValue::I32(0)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_heap_roundtrip() -> Result<(), MemoryError> {
|
||||
let mut state = InterpreterState::default();
|
||||
let mut roundtrip = |dv: DataValue| {
|
||||
state.store_heap(0, dv.clone())?;
|
||||
assert_eq!(dv, state.load_heap(0, dv.ty())?);
|
||||
Ok(())
|
||||
};
|
||||
|
||||
roundtrip(DataValue::B(true))?;
|
||||
roundtrip(DataValue::I64(42))?;
|
||||
roundtrip(DataValue::F32(Ieee32::from(0.42)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_flags() {
|
||||
let mut state = InterpreterState::default();
|
||||
@@ -461,4 +515,209 @@ mod tests {
|
||||
.unwrap_return();
|
||||
assert_eq!(result, vec![DataValue::I32(2)]);
|
||||
}
|
||||
|
||||
// Verifies that writing to the stack on a called function does not overwrite the parents
|
||||
// stack slots.
|
||||
#[test]
|
||||
fn stack_slots_multi_functions() {
|
||||
let code = "
|
||||
function %callee(i64, i64) -> i64 {
|
||||
ss0 = explicit_slot 8
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0(v0: i64, v1: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
v2 = stack_load.i64 ss0
|
||||
v3 = stack_load.i64 ss1
|
||||
v4 = iadd.i64 v2, v3
|
||||
return v4
|
||||
}
|
||||
|
||||
function %caller(i64, i64, i64, i64) -> i64 {
|
||||
fn0 = %callee(i64, i64) -> i64
|
||||
ss0 = explicit_slot 8
|
||||
ss1 = explicit_slot 8
|
||||
|
||||
block0(v0: i64, v1: i64, v2: i64, v3: i64):
|
||||
stack_store.i64 v0, ss0
|
||||
stack_store.i64 v1, ss1
|
||||
|
||||
v4 = call fn0(v2, v3)
|
||||
|
||||
v5 = stack_load.i64 ss0
|
||||
v6 = stack_load.i64 ss1
|
||||
|
||||
v7 = iadd.i64 v4, v5
|
||||
v8 = iadd.i64 v7, v6
|
||||
|
||||
return v8
|
||||
}";
|
||||
|
||||
let mut env = FunctionStore::default();
|
||||
let funcs = parse_functions(code).unwrap().to_vec();
|
||||
funcs.iter().for_each(|f| env.add(f.name.to_string(), f));
|
||||
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let result = Interpreter::new(state)
|
||||
.call_by_name(
|
||||
"%caller",
|
||||
&[
|
||||
DataValue::I64(3),
|
||||
DataValue::I64(5),
|
||||
DataValue::I64(7),
|
||||
DataValue::I64(11),
|
||||
],
|
||||
)
|
||||
.unwrap()
|
||||
.unwrap_return();
|
||||
|
||||
assert_eq!(result, vec![DataValue::I64(26)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_slot_write_traps() {
|
||||
let code = "
|
||||
function %stack_write() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = iconst.i64 10
|
||||
stack_store.i64 v0, ss0+8
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_write", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_out_of_slot_write_traps() {
|
||||
let code = "
|
||||
function %stack_write() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = iconst.i64 10
|
||||
stack_store.i64 v0, ss0+4
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_write", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_slot_read_traps() {
|
||||
let code = "
|
||||
function %stack_load() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = stack_load.i64 ss0+8
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_load", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_out_of_slot_read_traps() {
|
||||
let code = "
|
||||
function %stack_load() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = stack_load.i64 ss0+4
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_load", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_out_of_slot_read_by_addr_traps() {
|
||||
let code = "
|
||||
function %stack_load() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = stack_addr.i64 ss0
|
||||
v1 = iconst.i64 4
|
||||
v2 = iadd.i64 v0, v1
|
||||
v3 = load.i64 v2
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_load", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_out_of_slot_write_by_addr_traps() {
|
||||
let code = "
|
||||
function %stack_store() {
|
||||
ss0 = explicit_slot 8
|
||||
|
||||
block0:
|
||||
v0 = stack_addr.i64 ss0
|
||||
v1 = iconst.i64 4
|
||||
v2 = iadd.i64 v0, v1
|
||||
store.i64 v1, v2
|
||||
return
|
||||
}";
|
||||
|
||||
let func = parse_functions(code).unwrap().into_iter().next().unwrap();
|
||||
let mut env = FunctionStore::default();
|
||||
env.add(func.name.to_string(), &func);
|
||||
let state = InterpreterState::default().with_function_store(env);
|
||||
let trap = Interpreter::new(state)
|
||||
.call_by_name("%stack_store", &[])
|
||||
.unwrap()
|
||||
.unwrap_trap();
|
||||
|
||||
assert_eq!(trap, CraneliftTrap::User(TrapCode::HeapOutOfBounds));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
//!
|
||||
//! This module is a project for interpreting Cranelift IR.
|
||||
|
||||
pub mod address;
|
||||
pub mod environment;
|
||||
pub mod frame;
|
||||
pub mod instruction;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
//! Cranelift instructions modify the state of the machine; the [State] trait describes these
|
||||
//! ways this can happen.
|
||||
use crate::address::{Address, AddressSize};
|
||||
use cranelift_codegen::data_value::DataValue;
|
||||
use cranelift_codegen::ir::condcodes::{FloatCC, IntCC};
|
||||
use cranelift_codegen::ir::{FuncRef, Function, Type, Value};
|
||||
use cranelift_codegen::ir::{FuncRef, Function, StackSlot, Type, Value};
|
||||
use cranelift_entity::PrimaryMap;
|
||||
use smallvec::SmallVec;
|
||||
use thiserror::Error;
|
||||
@@ -57,25 +59,37 @@ pub trait State<'a, V> {
|
||||
/// Clear all [IntCC] and [FloatCC] flags.
|
||||
fn clear_flags(&mut self);
|
||||
|
||||
/// Retrieve a value `V` from the heap at the given `offset`; the number of bytes loaded
|
||||
/// corresponds to the specified [Type].
|
||||
fn load_heap(&self, offset: usize, ty: Type) -> Result<V, MemoryError>;
|
||||
/// Store a value `V` into the heap at the given `offset`. The [Type] of `V` will determine
|
||||
/// the number of bytes stored.
|
||||
fn store_heap(&mut self, offset: usize, v: V) -> Result<(), MemoryError>;
|
||||
|
||||
/// Retrieve a value `V` from the stack at the given `offset`; the number of bytes loaded
|
||||
/// corresponds to the specified [Type].
|
||||
fn load_stack(&self, offset: usize, ty: Type) -> Result<V, MemoryError>;
|
||||
/// Store a value `V` on the stack at the given `offset`. The [Type] of `V` will determine
|
||||
/// the number of bytes stored.
|
||||
fn store_stack(&mut self, offset: usize, v: V) -> Result<(), MemoryError>;
|
||||
/// Computes the stack address for this stack slot, including an offset.
|
||||
fn stack_address(
|
||||
&self,
|
||||
size: AddressSize,
|
||||
slot: StackSlot,
|
||||
offset: u64,
|
||||
) -> Result<Address, MemoryError>;
|
||||
/// Computes a heap address
|
||||
fn heap_address(&self, size: AddressSize, offset: u64) -> Result<Address, MemoryError>;
|
||||
/// 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].
|
||||
fn checked_load(&self, address: Address, ty: Type) -> Result<V, MemoryError>;
|
||||
/// 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].
|
||||
fn checked_store(&mut self, address: Address, v: V) -> Result<(), MemoryError>;
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MemoryError {
|
||||
#[error("insufficient memory: asked for address {0} in memory of size {1}")]
|
||||
InsufficientMemory(usize, usize),
|
||||
#[error("Invalid DataValue passed as an address: {0}")]
|
||||
InvalidAddress(DataValue),
|
||||
#[error("Invalid type for address: {0}")]
|
||||
InvalidAddressType(Type),
|
||||
#[error("Requested an the entry {entry} but only {max} entries are allowed")]
|
||||
InvalidEntry { entry: u64, max: u64 },
|
||||
#[error("Requested an offset of {offset} but max was {max}")]
|
||||
InvalidOffset { offset: u64, max: u64 },
|
||||
#[error("Load of {load_size} bytes is larger than available size at address {addr:?}")]
|
||||
OutOfBoundsLoad { addr: Address, load_size: usize },
|
||||
#[error("Store of {store_size} bytes is larger than available size at address {addr:?}")]
|
||||
OutOfBoundsStore { addr: Address, store_size: usize },
|
||||
}
|
||||
|
||||
/// This dummy state allows interpretation over an immutable mapping of values in a single frame.
|
||||
@@ -128,19 +142,24 @@ where
|
||||
|
||||
fn clear_flags(&mut self) {}
|
||||
|
||||
fn load_heap(&self, _offset: usize, _ty: Type) -> Result<V, MemoryError> {
|
||||
fn stack_address(
|
||||
&self,
|
||||
_size: AddressSize,
|
||||
_slot: StackSlot,
|
||||
_offset: u64,
|
||||
) -> Result<Address, MemoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn store_heap(&mut self, _offset: usize, _v: V) -> Result<(), MemoryError> {
|
||||
fn heap_address(&self, _size: AddressSize, _offset: u64) -> Result<Address, MemoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn load_stack(&self, _offset: usize, _ty: Type) -> Result<V, MemoryError> {
|
||||
fn checked_load(&self, _addr: Address, _ty: Type) -> Result<V, MemoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn store_stack(&mut self, _offset: usize, _v: V) -> Result<(), MemoryError> {
|
||||
fn checked_store(&mut self, _addr: Address, _v: V) -> Result<(), MemoryError> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! The [step] function interprets a single Cranelift instruction given its [State] and
|
||||
//! [InstructionContext]; the interpretation is generic over [Value]s.
|
||||
use crate::address::{Address, AddressSize};
|
||||
use crate::instruction::InstructionContext;
|
||||
use crate::state::{MemoryError, State};
|
||||
use crate::value::{Value, ValueConversionKind, ValueError, ValueResult};
|
||||
use cranelift_codegen::data_value::DataValue;
|
||||
use cranelift_codegen::ir::condcodes::{FloatCC, IntCC};
|
||||
use cranelift_codegen::ir::{
|
||||
types, Block, FuncRef, Function, InstructionData, Opcode, TrapCode, Value as ValueRef,
|
||||
@@ -87,6 +89,37 @@ where
|
||||
Err(e) => Err(e),
|
||||
};
|
||||
|
||||
let memerror_to_trap = |e: MemoryError| match e {
|
||||
MemoryError::InvalidAddress(_) => TrapCode::HeapOutOfBounds,
|
||||
MemoryError::InvalidAddressType(_) => TrapCode::HeapOutOfBounds,
|
||||
MemoryError::InvalidOffset { .. } => TrapCode::HeapOutOfBounds,
|
||||
MemoryError::InvalidEntry { .. } => TrapCode::HeapOutOfBounds,
|
||||
MemoryError::OutOfBoundsStore { .. } => TrapCode::HeapOutOfBounds,
|
||||
MemoryError::OutOfBoundsLoad { .. } => TrapCode::HeapOutOfBounds,
|
||||
};
|
||||
|
||||
// Assigns or traps depending on the value of the result
|
||||
let assign_or_memtrap = |res| match res {
|
||||
Ok(v) => assign(v),
|
||||
Err(e) => ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e))),
|
||||
};
|
||||
|
||||
// Continues or traps depending on the value of the result
|
||||
let continue_or_memtrap = |res| match res {
|
||||
Ok(_) => ControlFlow::Continue,
|
||||
Err(e) => ControlFlow::Trap(CraneliftTrap::User(memerror_to_trap(e))),
|
||||
};
|
||||
|
||||
let calculate_addr = |imm: V, args: SmallVec<[V; 1]>| -> ValueResult<u64> {
|
||||
let imm = imm.convert(ValueConversionKind::ZeroExtend(ctrl_ty))?;
|
||||
let args = args
|
||||
.into_iter()
|
||||
.map(|v| v.convert(ValueConversionKind::ZeroExtend(ctrl_ty)))
|
||||
.collect::<ValueResult<SmallVec<[V; 1]>>>()?;
|
||||
|
||||
Ok(sum(imm, args)? as u64)
|
||||
};
|
||||
|
||||
// Interpret a binary instruction with the given `op`, assigning the resulting value to the
|
||||
// instruction's results.
|
||||
let binary = |op: fn(V, V) -> ValueResult<V>,
|
||||
@@ -250,7 +283,6 @@ where
|
||||
| Opcode::Uload32x2Complex
|
||||
| Opcode::Sload32x2
|
||||
| Opcode::Sload32x2Complex => {
|
||||
let address = sum(imm(), args()?)? as usize;
|
||||
let ctrl_ty = inst_context.controlling_type().unwrap();
|
||||
let (load_ty, kind) = match inst.opcode() {
|
||||
Opcode::Load | Opcode::LoadComplex => (ctrl_ty, None),
|
||||
@@ -286,13 +318,20 @@ where
|
||||
| Opcode::Sload32x2Complex => unimplemented!(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let loaded = state.load_heap(address, load_ty)?;
|
||||
let extended = if let Some(c) = kind {
|
||||
loaded.convert(c)?
|
||||
} else {
|
||||
loaded
|
||||
};
|
||||
ControlFlow::Assign(smallvec!(extended))
|
||||
|
||||
let addr_value = calculate_addr(imm(), args()?)?;
|
||||
let loaded = assign_or_memtrap(
|
||||
Address::try_from(addr_value).and_then(|addr| state.checked_load(addr, load_ty)),
|
||||
);
|
||||
|
||||
match (loaded, kind) {
|
||||
(ControlFlow::Assign(ret), Some(c)) => ControlFlow::Assign(
|
||||
ret.into_iter()
|
||||
.map(|loaded| loaded.convert(c.clone()))
|
||||
.collect::<ValueResult<SmallVec<[V; 1]>>>()?,
|
||||
),
|
||||
(cf, _) => cf,
|
||||
}
|
||||
}
|
||||
Opcode::Store
|
||||
| Opcode::StoreComplex
|
||||
@@ -302,7 +341,6 @@ where
|
||||
| Opcode::Istore16Complex
|
||||
| Opcode::Istore32
|
||||
| Opcode::Istore32Complex => {
|
||||
let address = sum(imm(), args_range(1..)?)? as usize;
|
||||
let kind = match inst.opcode() {
|
||||
Opcode::Store | Opcode::StoreComplex => None,
|
||||
Opcode::Istore8 | Opcode::Istore8Complex => {
|
||||
@@ -316,27 +354,49 @@ where
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let addr_value = calculate_addr(imm(), args_range(1..)?)?;
|
||||
let reduced = if let Some(c) = kind {
|
||||
arg(0)?.convert(c)?
|
||||
} else {
|
||||
arg(0)?
|
||||
};
|
||||
state.store_heap(address, reduced)?;
|
||||
ControlFlow::Continue
|
||||
continue_or_memtrap(
|
||||
Address::try_from(addr_value).and_then(|addr| state.checked_store(addr, reduced)),
|
||||
)
|
||||
}
|
||||
Opcode::StackLoad => {
|
||||
let address = sum(imm(), args_range(1..)?)? as usize;
|
||||
let load_ty = inst_context.controlling_type().unwrap();
|
||||
let loaded = state.load_stack(address, load_ty)?;
|
||||
ControlFlow::Assign(smallvec!(loaded))
|
||||
let slot = inst.stack_slot().unwrap();
|
||||
let offset = sum(imm(), args()?)? as u64;
|
||||
assign_or_memtrap({
|
||||
state
|
||||
.stack_address(AddressSize::_64, slot, offset)
|
||||
.and_then(|addr| state.checked_load(addr, load_ty))
|
||||
})
|
||||
}
|
||||
Opcode::StackStore => {
|
||||
let address = sum(imm(), args_range(1..)?)? as usize;
|
||||
let arg0 = arg(0)?;
|
||||
state.store_stack(address, arg0)?;
|
||||
ControlFlow::Continue
|
||||
let arg = arg(0)?;
|
||||
let slot = inst.stack_slot().unwrap();
|
||||
let offset = sum(imm(), args_range(1..)?)? as u64;
|
||||
continue_or_memtrap({
|
||||
state
|
||||
.stack_address(AddressSize::_64, slot, offset)
|
||||
.and_then(|addr| state.checked_store(addr, arg))
|
||||
})
|
||||
}
|
||||
Opcode::StackAddr => {
|
||||
let load_ty = inst_context.controlling_type().unwrap();
|
||||
let slot = inst.stack_slot().unwrap();
|
||||
let offset = sum(imm(), args()?)? as u64;
|
||||
assign_or_memtrap({
|
||||
AddressSize::try_from(load_ty).and_then(|addr_size| {
|
||||
let addr = state.stack_address(addr_size, slot, offset)?;
|
||||
let dv = DataValue::try_from(addr)?;
|
||||
Ok(dv.into())
|
||||
})
|
||||
})
|
||||
}
|
||||
Opcode::StackAddr => unimplemented!("StackAddr"),
|
||||
Opcode::GlobalValue => unimplemented!("GlobalValue"),
|
||||
Opcode::SymbolValue => unimplemented!("SymbolValue"),
|
||||
Opcode::TlsValue => unimplemented!("TlsValue"),
|
||||
@@ -724,6 +784,16 @@ impl<'a, V> ControlFlow<'a, V> {
|
||||
panic!("expected the control flow to be in the return state")
|
||||
}
|
||||
}
|
||||
|
||||
/// For convenience, we can unwrap the [ControlFlow] state assuming that it is a
|
||||
/// [ControlFlow::Trap], panicking otherwise.
|
||||
pub fn unwrap_trap(self) -> CraneliftTrap {
|
||||
if let ControlFlow::Trap(trap) = self {
|
||||
trap
|
||||
} else {
|
||||
panic!("expected the control flow to be a trap")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
|
||||
@@ -92,7 +92,7 @@ impl Display for ValueTypeClass {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ValueConversionKind {
|
||||
/// Throw a [ValueError] if an exact conversion to [Type] is not possible; e.g. in `i32` to
|
||||
/// `i16`, convert `0x00001234` to `0x1234`.
|
||||
@@ -234,6 +234,7 @@ impl Value for DataValue {
|
||||
ValueConversionKind::Exact(ty) => match (self, ty) {
|
||||
// TODO a lot to do here: from bmask to ireduce to raw_bitcast...
|
||||
(DataValue::I64(n), types::I32) => DataValue::I32(i32::try_from(n)?),
|
||||
(DataValue::I64(n), types::I64) => DataValue::I64(n),
|
||||
(DataValue::B(b), t) if t.is_bool() => DataValue::B(b),
|
||||
(dv, _) => unimplemented!("conversion: {} -> {:?}", dv.ty(), kind),
|
||||
},
|
||||
@@ -273,14 +274,17 @@ impl Value for DataValue {
|
||||
(types::I32, types::I64) => unimplemented!(),
|
||||
_ => unimplemented!("conversion: {} -> {:?}", self.ty(), kind),
|
||||
},
|
||||
ValueConversionKind::ZeroExtend(ty) => match (self.ty(), ty) {
|
||||
(types::I8, types::I16) => unimplemented!(),
|
||||
(types::I8, types::I32) => unimplemented!(),
|
||||
(types::I8, types::I64) => unimplemented!(),
|
||||
(types::I16, types::I32) => unimplemented!(),
|
||||
(types::I16, types::I64) => unimplemented!(),
|
||||
(types::I32, types::I64) => unimplemented!(),
|
||||
_ => unimplemented!("conversion: {} -> {:?}", self.ty(), kind),
|
||||
ValueConversionKind::ZeroExtend(ty) => match (self, ty) {
|
||||
(DataValue::I8(_), types::I16) => unimplemented!(),
|
||||
(DataValue::I8(_), types::I32) => unimplemented!(),
|
||||
(DataValue::I8(_), types::I64) => unimplemented!(),
|
||||
(DataValue::I16(_), types::I32) => unimplemented!(),
|
||||
(DataValue::I16(_), types::I64) => unimplemented!(),
|
||||
(DataValue::U32(n), types::I64) => DataValue::U64(n as u64),
|
||||
(DataValue::I32(n), types::I64) => DataValue::I64(n as u32 as i64),
|
||||
(DataValue::U64(n), types::I64) => DataValue::U64(n),
|
||||
(DataValue::I64(n), types::I64) => DataValue::I64(n),
|
||||
(dv, _) => unimplemented!("conversion: {} -> {:?}", dv.ty(), kind),
|
||||
},
|
||||
ValueConversionKind::ToUnsigned => match self {
|
||||
DataValue::I8(n) => DataValue::U8(n as u8),
|
||||
@@ -340,7 +344,8 @@ impl Value for DataValue {
|
||||
}
|
||||
|
||||
fn add(self, other: Self) -> ValueResult<Self> {
|
||||
binary_match!(wrapping_add(&self, &other); [I8, I16, I32, I64, I128]) // TODO: floats must handle NaNs, +/-0
|
||||
// TODO: floats must handle NaNs, +/-0
|
||||
binary_match!(wrapping_add(&self, &other); [I8, I16, I32, I64, I128, U8, U16, U32, U64, U128])
|
||||
}
|
||||
|
||||
fn sub(self, other: Self) -> ValueResult<Self> {
|
||||
@@ -396,7 +401,7 @@ impl Value for DataValue {
|
||||
}
|
||||
|
||||
fn and(self, other: Self) -> ValueResult<Self> {
|
||||
binary_match!(&(&self, &other); [I8, I16, I32, I64])
|
||||
binary_match!(&(&self, &other); [B, I8, I16, I32, I64])
|
||||
}
|
||||
|
||||
fn or(self, other: Self) -> ValueResult<Self> {
|
||||
|
||||
Reference in New Issue
Block a user