Files
wasmtime/cranelift/codegen/src/stack_layout.rs

242 lines
8.8 KiB
Rust

//! Computing stack layout.
use crate::ir::stackslot::{StackOffset, StackSize, StackSlotKind};
use crate::ir::{StackLayoutInfo, StackSlots};
use crate::result::{CodegenError, CodegenResult};
use core::cmp::{max, min};
/// Compute the stack frame layout.
///
/// Determine the total size of this stack frame and assign offsets to all `Spill` and `Explicit`
/// stack slots.
///
/// The total frame size will be a multiple of `alignment` which must be a power of two, unless the
/// function doesn't perform any call.
///
/// Returns the total stack frame size which is also saved in `frame.frame_size`.
///
/// If the stack frame is too big, returns an `ImplLimitExceeded` error.
pub fn layout_stack(
frame: &mut StackSlots,
is_leaf: bool,
alignment: StackSize,
) -> CodegenResult<StackSize> {
// Each object and the whole stack frame must fit in 2 GB such that any relative offset within
// the frame fits in a `StackOffset`.
let max_size = StackOffset::max_value() as StackSize;
debug_assert!(alignment.is_power_of_two() && alignment <= max_size);
// We assume a stack that grows toward lower addresses as implemented by modern ISAs. The
// stack layout from high to low addresses will be:
//
// 1. incoming arguments.
// 2. spills + explicits + struct returns.
// 3. outgoing arguments.
//
// The incoming arguments can have both positive and negative offsets. A negative offset
// incoming arguments is usually the x86 return address pushed by the call instruction, but
// it can also be fixed stack slots pushed by an externally generated prologue.
//
// Both incoming and outgoing argument slots have fixed offsets that are treated as
// reserved zones by the layout algorithm.
//
// If a function only has incoming arguments and does not perform any calls, then it doesn't
// require the stack to be aligned.
let mut incoming_min = 0;
let mut incoming_max = 0;
let mut outgoing_max = 0;
let mut min_align = alignment;
let mut must_align = !is_leaf;
for slot in frame.values() {
if slot.size > max_size {
return Err(CodegenError::ImplLimitExceeded);
}
match slot.kind {
StackSlotKind::IncomingArg => {
incoming_min = min(incoming_min, slot.offset.unwrap());
incoming_max = max(incoming_max, slot.offset.unwrap() + slot.size as i32);
}
StackSlotKind::OutgoingArg => {
let offset = slot
.offset
.unwrap()
.checked_add(slot.size as StackOffset)
.ok_or(CodegenError::ImplLimitExceeded)?;
outgoing_max = max(outgoing_max, offset);
must_align = true;
}
StackSlotKind::StructReturnSlot
| StackSlotKind::SpillSlot
| StackSlotKind::ExplicitSlot
| StackSlotKind::EmergencySlot => {
// Determine the smallest alignment of any explicit or spill slot.
min_align = slot.alignment(min_align);
must_align = true;
}
}
}
// Lay out spill slots, struct return slots, and explicit slots below the
// incoming arguments. The offset is negative, growing downwards. Start with
// the smallest alignments for better packing.
let mut offset = incoming_min;
debug_assert!(min_align.is_power_of_two());
while min_align <= alignment {
for slot in frame.values_mut() {
// Pick out explicit and spill slots with exact alignment `min_align`.
match slot.kind {
StackSlotKind::SpillSlot
| StackSlotKind::StructReturnSlot
| StackSlotKind::ExplicitSlot
| StackSlotKind::EmergencySlot => {
if slot.alignment(alignment) != min_align {
continue;
}
}
StackSlotKind::IncomingArg | StackSlotKind::OutgoingArg => continue,
}
offset = offset
.checked_sub(slot.size as StackOffset)
.ok_or(CodegenError::ImplLimitExceeded)?;
// Aligning the negative offset can never cause overflow. We're only clearing bits.
offset &= -(min_align as StackOffset);
slot.offset = Some(offset);
}
// Move on to the next higher alignment.
min_align *= 2;
}
// Finally, make room for the outgoing arguments.
offset = offset
.checked_sub(outgoing_max)
.ok_or(CodegenError::ImplLimitExceeded)?;
if must_align {
offset &= -(alignment as StackOffset);
}
// Set the computed layout information for the frame
let frame_size = (offset as StackSize).wrapping_neg();
let inbound_args_size = incoming_max as u32;
frame.layout_info = Some(StackLayoutInfo {
frame_size,
inbound_args_size,
});
Ok(frame_size)
}
#[cfg(test)]
mod tests {
use super::layout_stack;
use crate::ir::stackslot::StackOffset;
use crate::ir::types;
use crate::ir::{StackSlotData, StackSlotKind, StackSlots};
use crate::result::CodegenError;
#[test]
fn layout() {
let sss = &mut StackSlots::new();
// For all these test cases, assume it will call.
let is_leaf = false;
// An empty layout should have 0-sized stack frame.
assert_eq!(layout_stack(sss, is_leaf, 1), Ok(0));
assert_eq!(layout_stack(sss, is_leaf, 16), Ok(0));
// Same for incoming arguments with non-negative offsets.
let in0 = sss.make_incoming_arg(8, 0);
let in1 = sss.make_incoming_arg(8, 8);
assert_eq!(layout_stack(sss, is_leaf, 1), Ok(0));
assert_eq!(layout_stack(sss, is_leaf, 16), Ok(0));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
// Add some spill slots.
let ss0 = sss.make_spill_slot(types::I64);
let ss1 = sss.make_spill_slot(types::I32);
assert_eq!(layout_stack(sss, is_leaf, 1), Ok(12));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[ss0].offset, Some(-8));
assert_eq!(sss[ss1].offset, Some(-12));
assert_eq!(layout_stack(sss, is_leaf, 16), Ok(16));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[ss0].offset, Some(-16));
assert_eq!(sss[ss1].offset, Some(-4));
// An incoming argument with negative offset counts towards the total frame size, but it
// should still pack nicely with the spill slots.
let in2 = sss.make_incoming_arg(4, -4);
assert_eq!(layout_stack(sss, is_leaf, 1), Ok(16));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[in2].offset, Some(-4));
assert_eq!(sss[ss0].offset, Some(-12));
assert_eq!(sss[ss1].offset, Some(-16));
assert_eq!(layout_stack(sss, is_leaf, 16), Ok(16));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[in2].offset, Some(-4));
assert_eq!(sss[ss0].offset, Some(-16));
assert_eq!(sss[ss1].offset, Some(-8));
// Finally, make sure there is room for the outgoing args.
let out0 = sss.get_outgoing_arg(4, 0);
assert_eq!(layout_stack(sss, is_leaf, 1), Ok(20));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[in2].offset, Some(-4));
assert_eq!(sss[ss0].offset, Some(-12));
assert_eq!(sss[ss1].offset, Some(-16));
assert_eq!(sss[out0].offset, Some(0));
assert_eq!(layout_stack(sss, is_leaf, 16), Ok(32));
assert_eq!(sss[in0].offset, Some(0));
assert_eq!(sss[in1].offset, Some(8));
assert_eq!(sss[in2].offset, Some(-4));
assert_eq!(sss[ss0].offset, Some(-16));
assert_eq!(sss[ss1].offset, Some(-8));
assert_eq!(sss[out0].offset, Some(0));
// Also test that an unsupported offset is rejected.
sss.get_outgoing_arg(1, StackOffset::max_value() - 1);
assert_eq!(
layout_stack(sss, is_leaf, 1),
Err(CodegenError::ImplLimitExceeded)
);
}
#[test]
fn slot_kinds() {
let sss = &mut StackSlots::new();
// Add some slots of various kinds.
let ss0 = sss.make_spill_slot(types::I32);
let ss1 = sss.push(StackSlotData::new(
StackSlotKind::ExplicitSlot,
types::I32.bytes(),
));
let ss2 = sss.get_emergency_slot(types::I32, &[]);
assert_eq!(layout_stack(sss, true, 1), Ok(12));
assert_eq!(sss[ss0].offset, Some(-4));
assert_eq!(sss[ss1].offset, Some(-8));
assert_eq!(sss[ss2].offset, Some(-12));
}
}