cranelift: Better document and test stack maps

This commit is contained in:
Nick Fitzgerald
2020-06-04 15:24:44 -07:00
parent 00abfcd943
commit 6aac4c891e
6 changed files with 312 additions and 9 deletions

View File

@@ -200,6 +200,7 @@ impl<'a> CodeSink for MemoryCodeSink<'a> {
/// A `RelocSink` implementation that does nothing, which is convenient when
/// compiling code that does not relocate anything.
#[derive(Default)]
pub struct NullRelocSink {}
impl RelocSink for NullRelocSink {
@@ -219,6 +220,7 @@ impl RelocSink for NullRelocSink {
/// A `TrapSink` implementation that does nothing, which is convenient when
/// compiling code that does not rely on trapping semantics.
#[derive(Default)]
pub struct NullTrapSink {}
impl TrapSink for NullTrapSink {

View File

@@ -6,15 +6,68 @@ use alloc::vec::Vec;
type Num = u32;
const NUM_BITS: usize = core::mem::size_of::<Num>() * 8;
/// A stack map is a bitmap with one bit per machine word on the stack. Stack
/// maps are created at `safepoint` instructions and record all live reference
/// values that are on the stack. All slot kinds, except `OutgoingArg` are
/// captured in a stack map. The `OutgoingArg`'s will be captured in the callee
/// function as `IncomingArg`'s.
/// Stack maps record which words in a stack frame contain live GC references at
/// a given instruction pointer.
///
/// The first value in the bitmap is of the lowest addressed slot on the stack.
/// As all stacks in Isa's supported by Cranelift grow down, this means that
/// first value is of the top of the stack and values proceed down the stack.
/// Logically, a set of stack maps for a function record a table of the form:
///
/// ```text
/// +---------------------+-------------------------------------------+
/// | Instruction Pointer | SP-Relative Offsets of Live GC References |
/// +---------------------+-------------------------------------------+
/// | 0x12345678 | 2, 6, 12 |
/// | 0x1234abcd | 2, 6 |
/// | ... | ... |
/// +---------------------+-------------------------------------------+
/// ```
///
/// Where "instruction pointer" is an instruction pointer within the function,
/// and "offsets of live GC references" contains the offsets (in units of words)
/// from the frame's stack pointer where live GC references are stored on the
/// stack. Instruction pointers within the function that do not have an entry in
/// this table are not GC safepoints.
///
/// Because
///
/// * offsets of live GC references are relative from the stack pointer, and
/// * stack frames grow down from higher addresses to lower addresses,
///
/// to get a pointer to a live reference at offset `x` within a stack frame, you
/// add `x` from the frame's stack pointer.
///
/// For example, to calculate the pointer to the live GC reference inside "frame
/// 1" below, you would do `frame_1_sp + x`:
///
/// ```text
/// Stack
/// +-------------------+
/// | Frame 0 |
/// | |
/// | | |
/// | +-------------------+ <--- Frame 0's SP
/// | | Frame 1 |
/// Grows | |
/// down | |
/// | | Live GC reference | --+--
/// | | | |
/// | | | |
/// V | | x = offset of live GC reference
/// | | |
/// | | |
/// +-------------------+ --+-- <--- Frame 1's SP
/// | Frame 2 |
/// | ... |
/// ```
///
/// An individual `Stackmap` is associated with just one instruction pointer
/// within the function, contains the size of the stack frame, and represents
/// the stack frame as a bitmap. There is one bit per word in the stack frame,
/// and if the bit is set, then the word contains a live GC reference.
///
/// Note that a caller's `OutgoingArg` stack slots and callee's `IncomingArg`
/// stack slots overlap, so we must choose which function's stack maps record
/// live GC references in these slots. We record the `IncomingArg`s in the
/// callee's stack map.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "enable-serde", derive(serde::Deserialize, serde::Serialize))]
pub struct Stackmap {
@@ -50,7 +103,7 @@ impl Stackmap {
// Refer to the doc comment for `Stackmap` above to understand the
// bitmap representation used here.
let map_size = (info.frame_size + info.inbound_args_size) as usize;
let map_size = (dbg!(info.frame_size) + dbg!(info.inbound_args_size)) as usize;
let word_size = isa.pointer_bytes() as usize;
let num_words = map_size / word_size;

View File

@@ -0,0 +1,103 @@
test stackmaps
set enable_safepoints=true
target x86_64
function %icall_fast(r64) -> r64 fast {
; check: function %icall_fast
; nextln: ss0 = spill_slot 8, offset -32
fn0 = %none()
block0(v0: r64):
; check: ss0] v0 = spill v2
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 4
; nextln: - live: [0]
function %icall_sys_v(r64) -> r64 system_v {
; check: function %icall_sys_v
; nextln: ss0 = spill_slot 8, offset -32
fn0 = %none()
block0(v0: r64):
; check: ss0] v0 = spill v2
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 4
; nextln: - live: [0]
function %icall_fastcall(r64) -> r64 windows_fastcall {
; check: function %icall_fastcall
; nextln: ss0 = spill_slot 8, offset -32
; nextln: ss1 = incoming_arg 24, offset -24
; nextln: ss2 = explicit_slot 32, offset -64
fn0 = %none()
block0(v0: r64):
; check: ss0] v0 = spill v2
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 8
; nextln: - live: [4]
function %call_fast(r64) -> r64 fast {
; check: function %call_fast
; nextln: ss0 = spill_slot 8, offset -32
fn0 = colocated %none()
block0(v0: r64):
; check: ss0] v0 = spill v1
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 4
; nextln: - live: [0]
function %call_sys_v(r64) -> r64 system_v {
; check: function %call_sys_v
; nextln: ss0 = spill_slot 8, offset -32
fn0 = colocated %none()
block0(v0: r64):
; check: ss0] v0 = spill v1
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 4
; nextln: - live: [0]
function %call_fastcall(r64) -> r64 windows_fastcall {
; check: function %call_fastcall
; nextln: ss0 = spill_slot 8, offset -32
; nextln: ss1 = incoming_arg 24, offset -24
; nextln: ss2 = explicit_slot 32, offset -64
fn0 = colocated %none()
block0(v0: r64):
; check: ss0] v0 = spill v1
; check: safepoint v0
call fn0()
return v0
}
; check: Stack maps:
; nextln:
; nextln: safepoint v0
; nextln: - mapped words: 8
; nextln: - live: [4]

View File

@@ -0,0 +1,31 @@
test stackmaps
set enable_safepoints=true
target x86_64
;; Incoming args get included in stack maps.
function %incoming_args(r64, r64, r64, r64, r64) -> r64 windows_fastcall {
; check: r64 [32]
; nextln: ss0 = incoming_arg 8, offset 32
; nextln: ss1 = incoming_arg 24, offset -24
; nextln: ss2 = explicit_slot 32, offset -64
fn0 = %none()
; nextln: sig0 = () fast
; nextln: fn0 = %none sig0
block0(v0: r64, v1: r64, v2: r64, v3: r64, v4: r64):
; check: v4: r64 [ss0]
call fn0()
; check: safepoint v4
; nextln: call_indirect
return v4
}
; check: Stack maps:
; nextln:
; nextln: safepoint v4
; nextln: - mapped words: 13
; nextln: - live: [12]

View File

@@ -56,6 +56,7 @@ mod test_safepoint;
mod test_shrink;
mod test_simple_gvn;
mod test_simple_preopt;
mod test_stackmaps;
mod test_unwind;
mod test_verifier;
@@ -139,6 +140,7 @@ fn new_subtest(parsed: &TestCommand) -> subtest::SubtestResult<Box<dyn subtest::
"shrink" => test_shrink::subtest(parsed),
"simple-gvn" => test_simple_gvn::subtest(parsed),
"simple_preopt" => test_simple_preopt::subtest(parsed),
"stackmaps" => test_stackmaps::subtest(parsed),
"unwind" => test_unwind::subtest(parsed),
"verifier" => test_verifier::subtest(parsed),
_ => Err(format!("unknown test command '{}'", parsed.command)),

View File

@@ -0,0 +1,112 @@
use crate::subtest::{run_filecheck, Context, SubTest, SubtestResult};
use cranelift_codegen::binemit::{self, Addend, CodeOffset, CodeSink, Reloc, Stackmap};
use cranelift_codegen::ir::*;
use cranelift_codegen::isa::TargetIsa;
use cranelift_codegen::print_errors::pretty_error;
use cranelift_reader::TestCommand;
use std::borrow::Cow;
use std::fmt::Write;
struct TestStackmaps;
pub fn subtest(parsed: &TestCommand) -> SubtestResult<Box<dyn SubTest>> {
assert_eq!(parsed.command, "stackmaps");
if !parsed.options.is_empty() {
Err(format!("No options allowed on {}", parsed))
} else {
Ok(Box::new(TestStackmaps))
}
}
impl SubTest for TestStackmaps {
fn name(&self) -> &'static str {
"stackmaps"
}
fn run(&self, func: Cow<Function>, context: &Context) -> SubtestResult<()> {
let mut comp_ctx = cranelift_codegen::Context::for_function(func.into_owned());
comp_ctx
.compile(context.isa.expect("`test stackmaps` requires an isa"))
.map_err(|e| pretty_error(&comp_ctx.func, context.isa, e))?;
let mut sink = TestStackMapsSink::default();
binemit::emit_function(
&comp_ctx.func,
|func, inst, div, sink, isa| {
if func.dfg[inst].opcode() == Opcode::Safepoint {
writeln!(&mut sink.text, "{}", func.dfg.display_inst(inst, isa)).unwrap();
}
isa.emit_inst(func, inst, div, sink)
},
&mut sink,
context.isa.expect("`test stackmaps` requires an isa"),
);
let mut text = comp_ctx.func.display(context.isa).to_string();
text.push('\n');
text.push_str("Stack maps:\n");
text.push('\n');
text.push_str(&sink.text);
run_filecheck(&text, context)
}
}
#[derive(Default)]
struct TestStackMapsSink {
offset: u32,
text: String,
}
impl CodeSink for TestStackMapsSink {
fn offset(&self) -> CodeOffset {
self.offset
}
fn put1(&mut self, _: u8) {
self.offset += 1;
}
fn put2(&mut self, _: u16) {
self.offset += 2;
}
fn put4(&mut self, _: u32) {
self.offset += 4;
}
fn put8(&mut self, _: u64) {
self.offset += 8;
}
fn reloc_block(&mut self, _: Reloc, _: CodeOffset) {}
fn reloc_external(&mut self, _: SourceLoc, _: Reloc, _: &ExternalName, _: Addend) {}
fn reloc_constant(&mut self, _: Reloc, _: ConstantOffset) {}
fn reloc_jt(&mut self, _: Reloc, _: JumpTable) {}
fn trap(&mut self, _: TrapCode, _: SourceLoc) {}
fn begin_jumptables(&mut self) {}
fn begin_rodata(&mut self) {}
fn end_codegen(&mut self) {}
fn add_stackmap(&mut self, val_list: &[Value], func: &Function, isa: &dyn TargetIsa) {
let map = Stackmap::from_values(&val_list, func, isa);
writeln!(&mut self.text, " - mapped words: {}", map.mapped_words()).unwrap();
write!(&mut self.text, " - live: [").unwrap();
let mut needs_comma_space = false;
for i in 0..(map.mapped_words() as usize) {
if map.get_bit(i) {
if needs_comma_space {
write!(&mut self.text, ", ").unwrap();
}
needs_comma_space = true;
write!(&mut self.text, "{}", i).unwrap();
}
}
writeln!(&mut self.text, "]").unwrap();
}
}