Implement emitting Windows unwind information for fastcall functions. (#1155)

* Implement emitting Windows unwind information for fastcall functions.

This commit implements emitting Windows unwind information for x64 fastcall
calling convention functions.

The unwind information can be used to construct a Windows function table at
runtime for JIT'd code, enabling stack walking and unwinding by the operating
system.

* Address code review feedback.

This commit addresses code review feedback:

* Remove unnecessary unsafe code.
* Emit the unwind information always as little endian.
* Fix comments.

A dependency from cranelift-codegen to the byteorder crate was added.
The byteorder crate is a no-dependencies crate with a reasonable
abstraction for writing binary data for a specific endianness.

* Address code review feedback.

* Disable default features for the `byteorder` crate.
* Add a comment regarding the Windows ABI unwind code numerical values.
* Panic if we encounter a Windows function with a prologue greater than 256
  bytes in size.
This commit is contained in:
Peter Huene
2019-11-05 13:14:30 -08:00
committed by Dan Gohman
parent 2eea366530
commit 8923bac7e8
11 changed files with 979 additions and 4 deletions

View File

@@ -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

View File

@@ -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<u8>) {
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.

View File

@@ -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<Inst>,
}
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.

View File

@@ -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<u8>) {
// No-op by default
}
}

View File

@@ -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<u8>) {
// 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");
}
}

View File

@@ -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<u8>) {
abi::emit_unwind_info(func, self, mem);
}
}
impl fmt::Display for Isa {

View File

@@ -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<u8>) -> 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::<LittleEndian>((*size / 8) as u16)?;
} else {
mem.write_u8((1 << 4) | (UnwindOperation::LargeStackAlloc as u8))?;
mem.write_u32::<LittleEndian>(*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<RegUnit>,
frame_register_offset: u8,
unwind_codes: Vec<UnwindCode>,
}
impl UnwindInfo {
pub fn try_from_func(
func: &Function,
isa: &dyn TargetIsa,
frame_register: Option<RegUnit>,
) -> Option<Self> {
// 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<u8>) -> 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::<LittleEndian>(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<StackSlotData>) -> 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
}
}

View File

@@ -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 = []

View File

@@ -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: }

View File

@@ -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<Box<dyn subtest::
"verifier" => 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)),
}
}

View File

@@ -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<Box<dyn SubTest>> {
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<ir::Function>, 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<UnwindCode>,
}
impl UnwindInfo {
fn from_cursor(cursor: &mut Cursor<&[u8]>) -> std::io::Result<Self> {
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<Self> {
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::<LittleEndian>()?),
1 => UnwindValue::U32(cursor.read_u32::<LittleEndian>()?),
_ => panic!("unexpected stack alloc info value"),
},
UnwindOperation::SaveNonvolatileRegister => {
UnwindValue::U16(cursor.read_u16::<LittleEndian>()?)
}
UnwindOperation::SaveNonvolatileRegisterFar => {
UnwindValue::U32(cursor.read_u32::<LittleEndian>()?)
}
UnwindOperation::SaveXmm128 => UnwindValue::U16(cursor.read_u16::<LittleEndian>()?),
UnwindOperation::SaveXmm128Far => UnwindValue::U32(cursor.read_u32::<LittleEndian>()?),
_ => UnwindValue::None,
};
Ok(Self {
offset,
op,
info,
value,
})
}
}
#[derive(Debug)]
enum UnwindOperation {
PushNonvolatileRegister,
LargeStackAlloc,
SmallStackAlloc,
SetFramePointer,
SaveNonvolatileRegister,
SaveNonvolatileRegisterFar,
SaveXmm128,
SaveXmm128Far,
PushMachineFrame,
}
impl From<u8> 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),
}