diff --git a/cranelift/codegen/Cargo.toml b/cranelift/codegen/Cargo.toml index 3ae2e23053..1d45b5b50a 100644 --- a/cranelift/codegen/Cargo.toml +++ b/cranelift/codegen/Cargo.toml @@ -22,6 +22,7 @@ log = { version = "0.4.6", default-features = false } serde = { version = "1.0.94", features = ["derive"], optional = true } smallvec = { version = "1.0.0" } thiserror = "1.0.4" +byteorder = { version = "1.3.2", default-features = false } # It is a goal of the cranelift-codegen crate to have minimal external dependencies. # Please don't add any unless they are essential to the task of creating binary # machine code. Integration tests that need external dependencies can be diff --git a/cranelift/codegen/src/context.rs b/cranelift/codegen/src/context.rs index ccf216d214..353b469bb6 100644 --- a/cranelift/codegen/src/context.rs +++ b/cranelift/codegen/src/context.rs @@ -193,6 +193,16 @@ impl Context { sink.info } + /// Emit unwind information. + /// + /// Requires that the function layout be calculated (see `relax_branches`). + /// + /// Only some calling conventions (e.g. Windows fastcall) will have unwind information. + /// This is a no-op if the function has no unwind information. + pub fn emit_unwind_info(&self, isa: &dyn TargetIsa, mem: &mut Vec) { + isa.emit_unwind_info(&self.func, mem); + } + /// Run the verifier on the function. /// /// Also check that the dominator tree and control flow graph are consistent with the function. diff --git a/cranelift/codegen/src/ir/function.rs b/cranelift/codegen/src/ir/function.rs index 00827240d9..913c2d7be4 100644 --- a/cranelift/codegen/src/ir/function.rs +++ b/cranelift/codegen/src/ir/function.rs @@ -83,6 +83,11 @@ pub struct Function { /// Track the original source location for each instruction. The source locations are not /// interpreted by Cranelift, only preserved. pub srclocs: SourceLocs, + + /// Instruction that marks the end (inclusive) of the function's prologue. + /// + /// This is used for some calling conventions to track the end of unwind information. + pub prologue_end: Option, } impl Function { @@ -104,6 +109,7 @@ impl Function { offsets: SecondaryMap::new(), jt_offsets: SecondaryMap::new(), srclocs: SecondaryMap::new(), + prologue_end: None, } } @@ -123,6 +129,7 @@ impl Function { self.offsets.clear(); self.jt_offsets.clear(); self.srclocs.clear(); + self.prologue_end = None; } /// Create a new empty, anonymous function with a Fast calling convention. diff --git a/cranelift/codegen/src/isa/mod.rs b/cranelift/codegen/src/isa/mod.rs index 66dde9621a..15f9d59c60 100644 --- a/cranelift/codegen/src/isa/mod.rs +++ b/cranelift/codegen/src/isa/mod.rs @@ -64,6 +64,7 @@ use crate::settings; use crate::settings::SetResult; use crate::timing; use alloc::boxed::Box; +use alloc::vec::Vec; use core::fmt; use target_lexicon::{triple, Architecture, PointerWidth, Triple}; use thiserror::Error; @@ -377,4 +378,11 @@ pub trait TargetIsa: fmt::Display + Sync { /// IntCC condition for Unsigned Subtraction Overflow (Borrow/Carry). fn unsigned_sub_overflow_condition(&self) -> ir::condcodes::IntCC; + + /// Emit unwind information for the given function. + /// + /// Only some calling conventions (e.g. Windows fastcall) will have unwind information. + fn emit_unwind_info(&self, _func: &ir::Function, _mem: &mut Vec) { + // No-op by default + } } diff --git a/cranelift/codegen/src/isa/x86/abi.rs b/cranelift/codegen/src/isa/x86/abi.rs index 6bd766724e..d78af9b448 100644 --- a/cranelift/codegen/src/isa/x86/abi.rs +++ b/cranelift/codegen/src/isa/x86/abi.rs @@ -3,6 +3,7 @@ use super::super::settings as shared_settings; use super::registers::{FPR, GPR, RU}; use super::settings as isa_settings; +use super::unwind::UnwindInfo; use crate::abi::{legalize_args, ArgAction, ArgAssigner, ValueConversion}; use crate::cursor::{Cursor, CursorPosition, EncCursor}; use crate::ir; @@ -16,6 +17,7 @@ use crate::isa::{CallConv, RegClass, RegUnit, TargetIsa}; use crate::regalloc::RegisterSet; use crate::result::CodegenResult; use crate::stack_layout::layout_stack; +use alloc::vec::Vec; use core::i32; use target_lexicon::{PointerWidth, Triple}; @@ -269,7 +271,7 @@ fn callee_saved_gprs(isa: &dyn TargetIsa, call_conv: CallConv) -> &'static [RU] if call_conv.extends_windows_fastcall() { // "registers RBX, RBP, RDI, RSI, RSP, R12, R13, R14, R15 are considered nonvolatile // and must be saved and restored by a function that uses them." - // as per https://msdn.microsoft.com/en-us/library/6t169e9c.aspx + // as per https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention // RSP & RSB are not listed below, since they are restored automatically during // a function call. If that wasn't the case, function calls (RET) would not work. &[ @@ -372,7 +374,7 @@ fn baldrdash_prologue_epilogue(func: &mut ir::Function, isa: &dyn TargetIsa) -> } /// Implementation of the fastcall-based Win64 calling convention described at [1] -/// [1] https://msdn.microsoft.com/en-us/library/ms235286.aspx +/// [1] https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention fn fastcall_prologue_epilogue(func: &mut ir::Function, isa: &dyn TargetIsa) -> CodegenResult<()> { if isa.triple().pointer_width().unwrap() != PointerWidth::U64 { panic!("TODO: windows-fastcall: x86-32 not implemented yet"); @@ -580,11 +582,11 @@ fn insert_common_prologue( if !isa.flags().probestack_func_adjusts_sp() { let result = pos.func.dfg.inst_results(call)[0]; pos.func.locations[result] = rax_val; - pos.ins().adjust_sp_down(result); + pos.func.prologue_end = Some(pos.ins().adjust_sp_down(result)); } } else { // Simply decrement the stack pointer. - pos.ins().adjust_sp_down_imm(Imm64::new(stack_size)); + pos.func.prologue_end = Some(pos.ins().adjust_sp_down_imm(Imm64::new(stack_size))); } } } @@ -658,3 +660,11 @@ fn insert_common_epilogue( pos.func.dfg.append_inst_arg(inst, csr_ret); } } + +pub fn emit_unwind_info(func: &ir::Function, isa: &dyn TargetIsa, mem: &mut Vec) { + // Assumption: RBP is being used as the frame pointer + // In the future, Windows fastcall codegen should usually omit the frame pointer + if let Some(info) = UnwindInfo::try_from_func(func, isa, Some(RU::rbp.into())) { + info.emit(mem).expect("failed to emit unwind information"); + } +} diff --git a/cranelift/codegen/src/isa/x86/mod.rs b/cranelift/codegen/src/isa/x86/mod.rs index 19cc41c99d..cfba3fef48 100644 --- a/cranelift/codegen/src/isa/x86/mod.rs +++ b/cranelift/codegen/src/isa/x86/mod.rs @@ -5,6 +5,7 @@ mod binemit; mod enc_tables; mod registers; pub mod settings; +mod unwind; use super::super::settings as shared_settings; #[cfg(feature = "testing_hooks")] @@ -18,6 +19,7 @@ use crate::regalloc; use crate::result::CodegenResult; use crate::timing; use alloc::boxed::Box; +use alloc::vec::Vec; use core::fmt; use target_lexicon::{PointerWidth, Triple}; @@ -150,6 +152,13 @@ impl TargetIsa for Isa { fn unsigned_sub_overflow_condition(&self) -> ir::condcodes::IntCC { ir::condcodes::IntCC::UnsignedLessThan } + + /// Emit unwind information for the given function. + /// + /// Only some calling conventions (e.g. Windows fastcall) will have unwind information. + fn emit_unwind_info(&self, func: &ir::Function, mem: &mut Vec) { + abi::emit_unwind_info(func, self, mem); + } } impl fmt::Display for Isa { diff --git a/cranelift/codegen/src/isa/x86/unwind.rs b/cranelift/codegen/src/isa/x86/unwind.rs new file mode 100644 index 0000000000..40caf44b63 --- /dev/null +++ b/cranelift/codegen/src/isa/x86/unwind.rs @@ -0,0 +1,508 @@ +//! Unwind information for x64 Windows. + +use super::registers::RU; +use crate::ir::{Function, InstructionData, Opcode}; +use crate::isa::{CallConv, RegUnit, TargetIsa}; +use alloc::vec::Vec; +use byteorder::{LittleEndian, WriteBytesExt}; + +/// Maximum (inclusive) size of a "small" stack allocation +const SMALL_ALLOC_MAX_SIZE: u32 = 128; +/// Maximum (inclusive) size of a "large" stack allocation that can represented in 16-bits +const LARGE_ALLOC_16BIT_MAX_SIZE: u32 = 524280; + +/// The supported unwind codes for the x64 Windows ABI. +/// +/// See: https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64 +/// Only what is needed to describe the prologues generated by the Cranelift x86 ISA are represented here. +/// Note: the Cranelift x86 ISA RU enum matches the Windows unwind GPR encoding values. +#[derive(Debug, PartialEq, Eq)] +enum UnwindCode { + PushRegister { offset: u8, reg: RegUnit }, + StackAlloc { offset: u8, size: u32 }, + SetFramePointer { offset: u8, sp_offset: u8 }, +} + +impl UnwindCode { + fn emit(&self, mem: &mut Vec) -> std::io::Result<()> { + enum UnwindOperation { + PushNonvolatileRegister, + LargeStackAlloc, + SmallStackAlloc, + SetFramePointer, + } + + match self { + Self::PushRegister { offset, reg } => { + mem.write_u8(*offset)?; + mem.write_u8( + ((*reg as u8) << 4) | (UnwindOperation::PushNonvolatileRegister as u8), + )?; + } + Self::StackAlloc { offset, size } => { + // Stack allocations on Windows must be a multiple of 8 and be at least 1 slot + assert!(*size >= 8); + assert!((*size % 8) == 0); + + mem.write_u8(*offset)?; + if *size <= SMALL_ALLOC_MAX_SIZE { + mem.write_u8( + ((((*size - 8) / 8) as u8) << 4) | UnwindOperation::SmallStackAlloc as u8, + )?; + } else if *size <= LARGE_ALLOC_16BIT_MAX_SIZE { + mem.write_u8(UnwindOperation::LargeStackAlloc as u8)?; + mem.write_u16::((*size / 8) as u16)?; + } else { + mem.write_u8((1 << 4) | (UnwindOperation::LargeStackAlloc as u8))?; + mem.write_u32::(*size)?; + } + } + Self::SetFramePointer { offset, sp_offset } => { + mem.write_u8(*offset)?; + mem.write_u8((*sp_offset << 4) | (UnwindOperation::SetFramePointer as u8))?; + } + }; + + Ok(()) + } + + fn node_count(&self) -> usize { + match self { + Self::StackAlloc { size, .. } => { + if *size <= SMALL_ALLOC_MAX_SIZE { + 1 + } else if *size <= LARGE_ALLOC_16BIT_MAX_SIZE { + 2 + } else { + 3 + } + } + _ => 1, + } + } +} + +/// Represents Windows x64 unwind information. +/// +/// For information about Windows x64 unwind info, see: +/// https://docs.microsoft.com/en-us/cpp/build/exception-handling-x64 +#[derive(Debug, PartialEq, Eq)] +pub struct UnwindInfo { + flags: u8, + prologue_size: u8, + frame_register: Option, + frame_register_offset: u8, + unwind_codes: Vec, +} + +impl UnwindInfo { + pub fn try_from_func( + func: &Function, + isa: &dyn TargetIsa, + frame_register: Option, + ) -> Option { + // Only Windows fastcall is supported for unwind information + if func.signature.call_conv != CallConv::WindowsFastcall || func.prologue_end.is_none() { + return None; + } + + let prologue_end = func.prologue_end.unwrap(); + let entry_block = func.layout.ebbs().nth(0).expect("missing entry block"); + + // Stores the stack size when SP is not adjusted via an immediate value + let mut stack_size = None; + let mut prologue_size = 0; + let mut unwind_codes = Vec::new(); + let mut found_end = false; + + for (offset, inst, size) in func.inst_offsets(entry_block, &isa.encoding_info()) { + // x64 ABI prologues cannot exceed 255 bytes in length + if (offset + size) > 255 { + panic!("function prologues cannot exceed 255 bytes in size for Windows x64"); + } + + prologue_size += size; + + let unwind_offset = (offset + size) as u8; + + match func.dfg[inst] { + InstructionData::Unary { opcode, arg } => { + match opcode { + Opcode::X86Push => { + unwind_codes.push(UnwindCode::PushRegister { + offset: unwind_offset, + reg: func.locations[arg].unwrap_reg(), + }); + } + Opcode::AdjustSpDown => { + // This is used when calling a stack check function + // We need to track the assignment to RAX which has the size of the stack + unwind_codes.push(UnwindCode::StackAlloc { + offset: unwind_offset, + size: stack_size + .expect("expected a previous stack size instruction"), + }); + } + _ => {} + } + } + InstructionData::CopySpecial { src, dst, .. } => { + if let Some(frame_register) = frame_register { + if src == (RU::rsp as RegUnit) && dst == frame_register { + unwind_codes.push(UnwindCode::SetFramePointer { + offset: unwind_offset, + sp_offset: 0, + }); + } + } + } + InstructionData::UnaryImm { opcode, imm } => { + match opcode { + Opcode::Iconst => { + let imm: i64 = imm.into(); + assert!(imm <= std::u32::MAX as i64); + assert!(stack_size.is_none()); + + // This instruction should only appear in a prologue to pass an + // argument of the stack size to a stack check function. + // Record the stack size so we know what it is when we encounter the adjustment + // instruction (which will adjust via the register assigned to this instruction). + stack_size = Some(imm as u32); + } + Opcode::AdjustSpDownImm => { + let imm: i64 = imm.into(); + assert!(imm <= std::u32::MAX as i64); + + unwind_codes.push(UnwindCode::StackAlloc { + offset: unwind_offset, + size: imm as u32, + }); + } + _ => {} + } + } + _ => {} + }; + + if inst == prologue_end { + found_end = true; + break; + } + } + + if !found_end { + return None; + } + + Some(Self { + flags: 0, // this assumes cranelift functions have no SEH handlers + prologue_size: prologue_size as u8, + frame_register, + frame_register_offset: 0, + unwind_codes, + }) + } + + pub fn size(&self) -> usize { + let node_count = self.node_count(); + + // Calculation of the size requires no SEH handler or chained info + assert!(self.flags == 0); + + // Size of fixed part of UNWIND_INFO is 4 bytes + // Then comes the UNWIND_CODE nodes (2 bytes each) + // Then comes 2 bytes of padding for the unwind codes if necessary + // Next would come the SEH data, but we assert above that the function doesn't have SEH data + + 4 + (node_count * 2) + if (node_count & 1) == 1 { 2 } else { 0 } + } + + pub fn node_count(&self) -> usize { + self.unwind_codes + .iter() + .fold(0, |nodes, c| nodes + c.node_count()) + } + + pub fn emit(&self, mem: &mut Vec) -> std::io::Result<()> { + const UNWIND_INFO_VERSION: u8 = 1; + + let size = self.size(); + let offset = mem.len(); + + // Ensure the memory is 32-bit aligned + assert_eq!(offset % 4, 0); + + mem.reserve(offset + size); + + let node_count = self.node_count(); + assert!(node_count <= 256); + + mem.write_u8((self.flags << 3) | UNWIND_INFO_VERSION)?; + mem.write_u8(self.prologue_size)?; + mem.write_u8(node_count as u8)?; + + if let Some(reg) = self.frame_register { + mem.write_u8((self.frame_register_offset << 4) | reg as u8)?; + } else { + mem.write_u8(0)?; + } + + // Unwind codes are written in reverse order (prologue offset descending) + for code in self.unwind_codes.iter().rev() { + code.emit(mem)?; + } + + // To keep a 32-bit alignment, emit 2 bytes of padding if there's an odd number of 16-bit nodes + if (node_count & 1) == 1 { + mem.write_u16::(0)?; + } + + // Ensure the correct number of bytes was emitted + assert_eq!(mem.len() - offset, size); + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cursor::{Cursor, FuncCursor}; + use crate::ir::{ExternalName, InstBuilder, Signature, StackSlotData, StackSlotKind}; + use crate::isa::{lookup, CallConv}; + use crate::settings::{builder, Flags}; + use crate::Context; + use std::str::FromStr; + use target_lexicon::triple; + + #[test] + fn test_wrong_calling_convention() { + let isa = lookup(triple!("x86_64")) + .expect("expect x86 ISA") + .finish(Flags::new(builder())); + + let mut context = Context::for_function(create_function(CallConv::SystemV, None)); + + context.compile(&*isa).expect("expected compilation"); + + assert_eq!(UnwindInfo::try_from_func(&context.func, &*isa, None), None); + } + + #[test] + fn test_small_alloc() { + let isa = lookup(triple!("x86_64")) + .expect("expect x86 ISA") + .finish(Flags::new(builder())); + + let mut context = Context::for_function(create_function( + CallConv::WindowsFastcall, + Some(StackSlotData::new(StackSlotKind::ExplicitSlot, 64)), + )); + + context.compile(&*isa).expect("expected compilation"); + + let unwind = UnwindInfo::try_from_func(&context.func, &*isa, Some(RU::rbp.into())) + .expect("expected unwind info"); + + assert_eq!( + unwind, + UnwindInfo { + flags: 0, + prologue_size: 9, + frame_register: Some(RU::rbp.into()), + frame_register_offset: 0, + unwind_codes: vec![ + UnwindCode::PushRegister { + offset: 2, + reg: RU::rbp.into() + }, + UnwindCode::SetFramePointer { + offset: 5, + sp_offset: 0 + }, + UnwindCode::StackAlloc { + offset: 9, + size: 64 + 32 + } + ] + } + ); + + assert_eq!(unwind.size(), 12); + + let mut mem = Vec::new(); + unwind + .emit(&mut mem) + .expect("failed to emit unwind information"); + + assert_eq!( + mem, + [ + 0x01, // Version and flags (version 1, no flags) + 0x09, // Prologue size + 0x03, // Unwind code count (1 for stack alloc, 1 for save frame reg, 1 for push reg) + 0x05, // Frame register + offset (RBP with 0 offset) + 0x09, // Prolog offset + 0xB2, // Operation 2 (small stack alloc), size = 0xB slots (e.g. (0xB * 8) + 8 = 96 (64 + 32) bytes) + 0x05, // Prolog offset + 0x03, // Operation 3 (save frame register), stack pointer offset = 0 + 0x02, // Prolog offset + 0x50, // Operation 0 (save nonvolatile register), reg = 5 (RBP) + 0x00, // Padding byte + 0x00, // Padding byte + ] + ); + } + + #[test] + fn test_medium_alloc() { + let isa = lookup(triple!("x86_64")) + .expect("expect x86 ISA") + .finish(Flags::new(builder())); + + let mut context = Context::for_function(create_function( + CallConv::WindowsFastcall, + Some(StackSlotData::new(StackSlotKind::ExplicitSlot, 10000)), + )); + + context.compile(&*isa).expect("expected compilation"); + + let unwind = UnwindInfo::try_from_func(&context.func, &*isa, Some(RU::rbp.into())) + .expect("expected unwind info"); + + assert_eq!( + unwind, + UnwindInfo { + flags: 0, + prologue_size: 27, + frame_register: Some(RU::rbp.into()), + frame_register_offset: 0, + unwind_codes: vec![ + UnwindCode::PushRegister { + offset: 2, + reg: RU::rbp.into() + }, + UnwindCode::SetFramePointer { + offset: 5, + sp_offset: 0 + }, + UnwindCode::StackAlloc { + offset: 27, + size: 10000 + 32 + } + ] + } + ); + + assert_eq!(unwind.size(), 12); + + let mut mem = Vec::new(); + unwind + .emit(&mut mem) + .expect("failed to emit unwind information"); + + assert_eq!( + mem, + [ + 0x01, // Version and flags (version 1, no flags) + 0x1B, // Prologue size + 0x04, // Unwind code count (2 for stack alloc, 1 for save frame reg, 1 for push reg) + 0x05, // Frame register + offset (RBP with 0 offset) + 0x1B, // Prolog offset + 0x01, // Operation 1 (large stack alloc), size is scaled 16-bits (info = 0) + 0xE6, // Low size byte + 0x04, // High size byte (e.g. 0x04E6 * 8 = 100032 (10000 + 32) bytes) + 0x05, // Prolog offset + 0x03, // Operation 3 (save frame register), stack pointer offset = 0 + 0x02, // Prolog offset + 0x50, // Operation 0 (push nonvolatile register), reg = 5 (RBP) + ] + ); + } + + #[test] + fn test_large_alloc() { + let isa = lookup(triple!("x86_64")) + .expect("expect x86 ISA") + .finish(Flags::new(builder())); + + let mut context = Context::for_function(create_function( + CallConv::WindowsFastcall, + Some(StackSlotData::new(StackSlotKind::ExplicitSlot, 1000000)), + )); + + context.compile(&*isa).expect("expected compilation"); + + let unwind = UnwindInfo::try_from_func(&context.func, &*isa, Some(RU::rbp.into())) + .expect("expected unwind info"); + + assert_eq!( + unwind, + UnwindInfo { + flags: 0, + prologue_size: 27, + frame_register: Some(RU::rbp.into()), + frame_register_offset: 0, + unwind_codes: vec![ + UnwindCode::PushRegister { + offset: 2, + reg: RU::rbp.into() + }, + UnwindCode::SetFramePointer { + offset: 5, + sp_offset: 0 + }, + UnwindCode::StackAlloc { + offset: 27, + size: 1000000 + 32 + } + ] + } + ); + + assert_eq!(unwind.size(), 16); + + let mut mem = Vec::new(); + unwind + .emit(&mut mem) + .expect("failed to emit unwind information"); + + assert_eq!( + mem, + [ + 0x01, // Version and flags (version 1, no flags) + 0x1B, // Prologue size + 0x05, // Unwind code count (3 for stack alloc, 1 for save frame reg, 1 for push reg) + 0x05, // Frame register + offset (RBP with 0 offset) + 0x1B, // Prolog offset + 0x11, // Operation 1 (large stack alloc), size is unscaled 32-bits (info = 1) + 0x60, // Byte 1 of size + 0x42, // Byte 2 of size + 0x0F, // Byte 3 of size + 0x00, // Byte 4 of size (size is 0xF4260 = 1000032 (1000000 + 32) bytes) + 0x05, // Prolog offset + 0x03, // Operation 3 (save frame register), stack pointer offset = 0 + 0x02, // Prolog offset + 0x50, // Operation 0 (push nonvolatile register), reg = 5 (RBP) + 0x00, // Padding byte + 0x00, // Padding byte + ] + ); + } + + fn create_function(call_conv: CallConv, stack_slot: Option) -> Function { + let mut func = + Function::with_name_signature(ExternalName::user(0, 0), Signature::new(call_conv)); + + let ebb0 = func.dfg.make_ebb(); + let mut pos = FuncCursor::new(&mut func); + pos.insert_ebb(ebb0); + pos.ins().return_(&[]); + + if let Some(stack_slot) = stack_slot { + func.stack_slots.push(stack_slot); + } + + func + } +} diff --git a/cranelift/filetests/Cargo.toml b/cranelift/filetests/Cargo.toml index c76ee4d478..2f59146613 100644 --- a/cranelift/filetests/Cargo.toml +++ b/cranelift/filetests/Cargo.toml @@ -20,6 +20,7 @@ log = "0.4.6" memmap = "0.7.0" num_cpus = "1.8.0" region = "2.1.2" +byteorder = { version = "1.3.2", default-features = false } [features] basic-blocks = [] diff --git a/cranelift/filetests/filetests/isa/x86/windows_fastcall_x64_unwind.clif b/cranelift/filetests/filetests/isa/x86/windows_fastcall_x64_unwind.clif new file mode 100644 index 0000000000..6782d8cdce --- /dev/null +++ b/cranelift/filetests/filetests/isa/x86/windows_fastcall_x64_unwind.clif @@ -0,0 +1,221 @@ +test unwind +set opt_level=speed_and_size +set is_pic +target x86_64 haswell + +; check that there is no unwind information for a system_v function +function %not_fastcall() system_v { +ebb0: + return +} +; sameln: No unwind information. + +; check the unwind information with a function with no args +function %no_args() windows_fastcall { +ebb0: + return +} +; sameln: UnwindInfo { +; nextln: version: 1, +; nextln: flags: 0, +; nextln: prologue_size: 8, +; nextln: unwind_code_count_raw: 3, +; nextln: frame_register: 5, +; nextln: frame_register_offset: 0, +; nextln: unwind_codes: [ +; nextln: UnwindCode { +; nextln: offset: 8, +; nextln: op: SmallStackAlloc, +; nextln: info: 3, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 4, +; nextln: op: SetFramePointer, +; nextln: info: 0, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 1, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 5, +; nextln: value: None, +; nextln: }, +; nextln: ], +; nextln: } + +; check a function with medium-sized stack alloc +function %medium_stack() windows_fastcall { + ss0 = explicit_slot 100000 +ebb0: + return +} +; sameln: UnwindInfo { +; nextln: version: 1, +; nextln: flags: 0, +; nextln: prologue_size: 17, +; nextln: unwind_code_count_raw: 4, +; nextln: frame_register: 5, +; nextln: frame_register_offset: 0, +; nextln: unwind_codes: [ +; nextln: UnwindCode { +; nextln: offset: 17, +; nextln: op: LargeStackAlloc, +; nextln: info: 0, +; nextln: value: U16( +; nextln: 12504, +; nextln: ), +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 4, +; nextln: op: SetFramePointer, +; nextln: info: 0, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 1, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 5, +; nextln: value: None, +; nextln: }, +; nextln: ], +; nextln: } + +; check a function with large-sized stack alloc +function %large_stack() windows_fastcall { + ss0 = explicit_slot 524288 +ebb0: + return +} +; sameln: UnwindInfo { +; nextln: version: 1, +; nextln: flags: 0, +; nextln: prologue_size: 17, +; nextln: unwind_code_count_raw: 5, +; nextln: frame_register: 5, +; nextln: frame_register_offset: 0, +; nextln: unwind_codes: [ +; nextln: UnwindCode { +; nextln: offset: 17, +; nextln: op: LargeStackAlloc, +; nextln: info: 1, +; nextln: value: U32( +; nextln: 524320, +; nextln: ), +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 4, +; nextln: op: SetFramePointer, +; nextln: info: 0, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 1, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 5, +; nextln: value: None, +; nextln: }, +; nextln: ], +; nextln: } + +; check a function that has CSRs +function %lots_of_registers(i64, i64) windows_fastcall { +ebb0(v0: i64, v1: i64): + v2 = load.i32 v0+0 + v3 = load.i32 v0+8 + v4 = load.i32 v0+16 + v5 = load.i32 v0+24 + v6 = load.i32 v0+32 + v7 = load.i32 v0+40 + v8 = load.i32 v0+48 + v9 = load.i32 v0+56 + v10 = load.i32 v0+64 + v11 = load.i32 v0+72 + v12 = load.i32 v0+80 + v13 = load.i32 v0+88 + v14 = load.i32 v0+96 + store.i32 v2, v1+0 + store.i32 v3, v1+8 + store.i32 v4, v1+16 + store.i32 v5, v1+24 + store.i32 v6, v1+32 + store.i32 v7, v1+40 + store.i32 v8, v1+48 + store.i32 v9, v1+56 + store.i32 v10, v1+64 + store.i32 v11, v1+72 + store.i32 v12, v1+80 + store.i32 v13, v1+88 + store.i32 v14, v1+96 + return +} +; sameln: UnwindInfo { +; nextln: version: 1, +; nextln: flags: 0, +; nextln: prologue_size: 19, +; nextln: unwind_code_count_raw: 10, +; nextln: frame_register: 5, +; nextln: frame_register_offset: 0, +; nextln: unwind_codes: [ +; nextln: UnwindCode { +; nextln: offset: 19, +; nextln: op: SmallStackAlloc, +; nextln: info: 4, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 15, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 15, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 13, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 14, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 11, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 13, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 9, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 12, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 7, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 7, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 6, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 6, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 5, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 3, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 4, +; nextln: op: SetFramePointer, +; nextln: info: 0, +; nextln: value: None, +; nextln: }, +; nextln: UnwindCode { +; nextln: offset: 1, +; nextln: op: PushNonvolatileRegister, +; nextln: info: 5, +; nextln: value: None, +; nextln: }, +; nextln: ], +; nextln: } diff --git a/cranelift/filetests/src/lib.rs b/cranelift/filetests/src/lib.rs index 073fc24492..ded3acd16c 100644 --- a/cranelift/filetests/src/lib.rs +++ b/cranelift/filetests/src/lib.rs @@ -54,6 +54,7 @@ mod test_safepoint; mod test_shrink; mod test_simple_gvn; mod test_simple_preopt; +mod test_unwind; mod test_verifier; /// The result of running the test in a file. @@ -135,6 +136,7 @@ fn new_subtest(parsed: &TestCommand) -> subtest::SubtestResult test_verifier::subtest(parsed), "preopt" => test_preopt::subtest(parsed), "safepoint" => test_safepoint::subtest(parsed), + "unwind" => test_unwind::subtest(parsed), _ => Err(format!("unknown test command '{}'", parsed.command)), } } diff --git a/cranelift/filetests/src/test_unwind.rs b/cranelift/filetests/src/test_unwind.rs new file mode 100644 index 0000000000..55fcbcdd9c --- /dev/null +++ b/cranelift/filetests/src/test_unwind.rs @@ -0,0 +1,198 @@ +//! Test command for verifying the unwind emitted for each function. +//! +//! The `unwind` test command runs each function through the full code generator pipeline. +#![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_ptr_alignment))] + +use crate::subtest::{run_filecheck, Context, SubTest, SubtestResult}; +use byteorder::{LittleEndian, ReadBytesExt}; +use cranelift_codegen; +use cranelift_codegen::ir; +use cranelift_reader::TestCommand; +use std::borrow::Cow; +use std::fmt::Write; +use std::io::Cursor; + +struct TestUnwind; + +pub fn subtest(parsed: &TestCommand) -> SubtestResult> { + assert_eq!(parsed.command, "unwind"); + if !parsed.options.is_empty() { + Err(format!("No options allowed on {}", parsed)) + } else { + Ok(Box::new(TestUnwind)) + } +} + +impl SubTest for TestUnwind { + fn name(&self) -> &'static str { + "unwind" + } + + fn is_mutating(&self) -> bool { + false + } + + fn needs_isa(&self) -> bool { + true + } + + fn run(&self, func: Cow, context: &Context) -> SubtestResult<()> { + let isa = context.isa.expect("unwind needs an ISA"); + let mut comp_ctx = cranelift_codegen::Context::for_function(func.into_owned()); + + comp_ctx.compile(isa).expect("failed to compile function"); + + let mut mem = Vec::new(); + comp_ctx.emit_unwind_info(isa, &mut mem); + + let mut text = String::new(); + if mem.is_empty() { + writeln!(text, "No unwind information.").unwrap(); + } else { + print_unwind_info(&mut text, &mem); + } + + run_filecheck(&text, context) + } +} + +fn print_unwind_info(text: &mut String, mem: &[u8]) { + let info = UnwindInfo::from_cursor(&mut Cursor::new(mem)).expect("failed to read unwind info"); + + // Assert correct alignment and padding of the unwind information + assert!(mem.len() % 4 == 0); + assert_eq!( + mem.len(), + 4 + ((info.unwind_code_count_raw as usize) * 2) + + if (info.unwind_code_count_raw & 1) == 1 { + 2 + } else { + 0 + } + ); + + writeln!(text, "{:#?}", info).unwrap(); +} + +#[derive(Debug)] +struct UnwindInfo { + pub version: u8, + pub flags: u8, + pub prologue_size: u8, + pub unwind_code_count_raw: u8, + pub frame_register: u8, + pub frame_register_offset: u8, + pub unwind_codes: Vec, +} + +impl UnwindInfo { + fn from_cursor(cursor: &mut Cursor<&[u8]>) -> std::io::Result { + let version_and_flags = cursor.read_u8()?; + let prologue_size = cursor.read_u8()?; + let unwind_code_count_raw = cursor.read_u8()?; + let frame_register_and_offset = cursor.read_u8()?; + let mut unwind_codes = Vec::new(); + + let mut i = 0; + while i < unwind_code_count_raw { + let code = UnwindCode::from_cursor(cursor)?; + + i += match &code.value { + UnwindValue::None => 1, + UnwindValue::U16(_) => 2, + UnwindValue::U32(_) => 3, + }; + + unwind_codes.push(code); + } + + Ok(Self { + version: version_and_flags & 0x3, + flags: (version_and_flags & 0xF8) >> 3, + prologue_size, + unwind_code_count_raw, + frame_register: frame_register_and_offset & 0xF, + frame_register_offset: (frame_register_and_offset & 0xF0) >> 4, + unwind_codes, + }) + } +} + +#[derive(Debug)] +struct UnwindCode { + pub offset: u8, + pub op: UnwindOperation, + pub info: u8, + pub value: UnwindValue, +} + +impl UnwindCode { + fn from_cursor(cursor: &mut Cursor<&[u8]>) -> std::io::Result { + let offset = cursor.read_u8()?; + let op_and_info = cursor.read_u8()?; + let op = UnwindOperation::from(op_and_info & 0xF); + let info = (op_and_info & 0xF0) >> 4; + + let value = match op { + UnwindOperation::LargeStackAlloc => match info { + 0 => UnwindValue::U16(cursor.read_u16::()?), + 1 => UnwindValue::U32(cursor.read_u32::()?), + _ => panic!("unexpected stack alloc info value"), + }, + UnwindOperation::SaveNonvolatileRegister => { + UnwindValue::U16(cursor.read_u16::()?) + } + UnwindOperation::SaveNonvolatileRegisterFar => { + UnwindValue::U32(cursor.read_u32::()?) + } + UnwindOperation::SaveXmm128 => UnwindValue::U16(cursor.read_u16::()?), + UnwindOperation::SaveXmm128Far => UnwindValue::U32(cursor.read_u32::()?), + _ => UnwindValue::None, + }; + + Ok(Self { + offset, + op, + info, + value, + }) + } +} + +#[derive(Debug)] +enum UnwindOperation { + PushNonvolatileRegister, + LargeStackAlloc, + SmallStackAlloc, + SetFramePointer, + SaveNonvolatileRegister, + SaveNonvolatileRegisterFar, + SaveXmm128, + SaveXmm128Far, + PushMachineFrame, +} + +impl From for UnwindOperation { + fn from(value: u8) -> Self { + // The numerical value is specified as part of the Windows x64 ABI + match value { + 0 => Self::PushNonvolatileRegister, + 1 => Self::LargeStackAlloc, + 2 => Self::SmallStackAlloc, + 3 => Self::SetFramePointer, + 4 => Self::SaveNonvolatileRegister, + 5 => Self::SaveNonvolatileRegisterFar, + 6 => Self::SaveXmm128, + 7 => Self::SaveXmm128Far, + 8 => Self::PushMachineFrame, + _ => panic!("unsupported unwind operation"), + } + } +} + +#[derive(Debug)] +enum UnwindValue { + None, + U16(u16), + U32(u32), +}