From 6aac4c891edccfbcee61b8a9e557b0bb0b90b8eb Mon Sep 17 00:00:00 2001 From: Nick Fitzgerald Date: Thu, 4 Jun 2020 15:24:44 -0700 Subject: [PATCH] cranelift: Better document and test stack maps --- cranelift/codegen/src/binemit/memorysink.rs | 2 + cranelift/codegen/src/binemit/stackmap.rs | 71 +++++++++-- .../filetests/filetests/stackmaps/call.clif | 103 ++++++++++++++++ .../filetests/stackmaps/incoming_args.clif | 31 +++++ cranelift/filetests/src/lib.rs | 2 + cranelift/filetests/src/test_stackmaps.rs | 112 ++++++++++++++++++ 6 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 cranelift/filetests/filetests/stackmaps/call.clif create mode 100644 cranelift/filetests/filetests/stackmaps/incoming_args.clif create mode 100644 cranelift/filetests/src/test_stackmaps.rs diff --git a/cranelift/codegen/src/binemit/memorysink.rs b/cranelift/codegen/src/binemit/memorysink.rs index 110017bf55..a8621924e2 100644 --- a/cranelift/codegen/src/binemit/memorysink.rs +++ b/cranelift/codegen/src/binemit/memorysink.rs @@ -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 { diff --git a/cranelift/codegen/src/binemit/stackmap.rs b/cranelift/codegen/src/binemit/stackmap.rs index 27d87f896f..4db9b92929 100644 --- a/cranelift/codegen/src/binemit/stackmap.rs +++ b/cranelift/codegen/src/binemit/stackmap.rs @@ -6,15 +6,68 @@ use alloc::vec::Vec; type Num = u32; const NUM_BITS: usize = core::mem::size_of::() * 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; diff --git a/cranelift/filetests/filetests/stackmaps/call.clif b/cranelift/filetests/filetests/stackmaps/call.clif new file mode 100644 index 0000000000..cf46c61284 --- /dev/null +++ b/cranelift/filetests/filetests/stackmaps/call.clif @@ -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] diff --git a/cranelift/filetests/filetests/stackmaps/incoming_args.clif b/cranelift/filetests/filetests/stackmaps/incoming_args.clif new file mode 100644 index 0000000000..cb48f4455c --- /dev/null +++ b/cranelift/filetests/filetests/stackmaps/incoming_args.clif @@ -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] + diff --git a/cranelift/filetests/src/lib.rs b/cranelift/filetests/src/lib.rs index 05a6a80da3..839272cead 100644 --- a/cranelift/filetests/src/lib.rs +++ b/cranelift/filetests/src/lib.rs @@ -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 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)), diff --git a/cranelift/filetests/src/test_stackmaps.rs b/cranelift/filetests/src/test_stackmaps.rs new file mode 100644 index 0000000000..fe1688603c --- /dev/null +++ b/cranelift/filetests/src/test_stackmaps.rs @@ -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> { + 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, 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(); + } +}