Implement registering JIT unwind information on Windows.
This commit implements registering unwind information for JIT functions on Windows so that the operating system can both walk and unwind stacks containing JIT frames. Currently this only works with Cranelift as lightbeam does not emit unwind information yet. This commit also resets the stack guard page on Windows for stack overflow exceptions, allowing reliable stack overflow traps. With these changes, all previously disabled test suite tests (not including the multi-value tests) on Windows are now passing. Fixes #291.
This commit is contained in:
@@ -1,16 +1,18 @@
|
||||
//! Memory management for executable code.
|
||||
|
||||
use crate::function_table::FunctionTable;
|
||||
use alloc::boxed::Box;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::{cmp, mem};
|
||||
use region;
|
||||
use wasmtime_environ::{Compilation, CompiledFunction};
|
||||
use wasmtime_runtime::{Mmap, VMFunctionBody};
|
||||
|
||||
/// Memory manager for executable code.
|
||||
pub struct CodeMemory {
|
||||
current: Mmap,
|
||||
mmaps: Vec<Mmap>,
|
||||
current: (Mmap, FunctionTable),
|
||||
mmaps: Vec<(Mmap, FunctionTable)>,
|
||||
position: usize,
|
||||
published: usize,
|
||||
}
|
||||
@@ -19,30 +21,144 @@ impl CodeMemory {
|
||||
/// Create a new `CodeMemory` instance.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
current: Mmap::new(),
|
||||
current: (Mmap::new(), FunctionTable::new()),
|
||||
mmaps: Vec::new(),
|
||||
position: 0,
|
||||
published: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Allocate a continuous memory block for a single compiled function.
|
||||
/// TODO: Reorganize the code that calls this to emit code directly into the
|
||||
/// mmap region rather than into a Vec that we need to copy in.
|
||||
pub fn allocate_for_function(
|
||||
&mut self,
|
||||
func: &CompiledFunction,
|
||||
) -> Result<&mut [VMFunctionBody], String> {
|
||||
let size = Self::function_allocation_size(func);
|
||||
|
||||
let start = self.position as u32;
|
||||
let (buf, table) = self.allocate(size)?;
|
||||
|
||||
let (_, _, _, vmfunc) = Self::copy_function(func, start, buf, table);
|
||||
|
||||
Ok(vmfunc)
|
||||
}
|
||||
|
||||
/// Allocate a continuous memory block for a compilation.
|
||||
///
|
||||
/// Allocates memory for both the function bodies as well as function unwind data.
|
||||
pub fn allocate_for_compilation(
|
||||
&mut self,
|
||||
compilation: &Compilation,
|
||||
) -> Result<Box<[&mut [VMFunctionBody]]>, String> {
|
||||
let total_len = compilation
|
||||
.into_iter()
|
||||
.fold(0, |acc, func| acc + Self::function_allocation_size(func));
|
||||
|
||||
let mut start = self.position as u32;
|
||||
let (mut buf, mut table) = self.allocate(total_len)?;
|
||||
let mut result = Vec::with_capacity(compilation.len());
|
||||
|
||||
for func in compilation.into_iter() {
|
||||
let (next_start, next_buf, next_table, vmfunc) =
|
||||
Self::copy_function(func, start, buf, table);
|
||||
|
||||
result.push(vmfunc);
|
||||
|
||||
start = next_start;
|
||||
buf = next_buf;
|
||||
table = next_table;
|
||||
}
|
||||
|
||||
Ok(result.into_boxed_slice())
|
||||
}
|
||||
|
||||
/// Make all allocated memory executable.
|
||||
pub fn publish(&mut self) {
|
||||
self.push_current(0)
|
||||
.expect("failed to push current memory map");
|
||||
|
||||
for (m, t) in &mut self.mmaps[self.published..] {
|
||||
if m.len() != 0 {
|
||||
unsafe {
|
||||
region::protect(m.as_mut_ptr(), m.len(), region::Protection::ReadExecute)
|
||||
}
|
||||
.expect("unable to make memory readonly and executable");
|
||||
}
|
||||
|
||||
t.publish(m.as_ptr() as u64)
|
||||
.expect("failed to publish function table");
|
||||
}
|
||||
|
||||
self.published = self.mmaps.len();
|
||||
}
|
||||
|
||||
/// Allocate `size` bytes of memory which can be made executable later by
|
||||
/// calling `publish()`. Note that we allocate the memory as writeable so
|
||||
/// that it can be written to and patched, though we make it readonly before
|
||||
/// actually executing from it.
|
||||
///
|
||||
/// TODO: Add an alignment flag.
|
||||
fn allocate(&mut self, size: usize) -> Result<&mut [u8], String> {
|
||||
if self.current.len() - self.position < size {
|
||||
self.mmaps.push(mem::replace(
|
||||
&mut self.current,
|
||||
Mmap::with_at_least(cmp::max(0x10000, size))?,
|
||||
));
|
||||
self.position = 0;
|
||||
fn allocate(&mut self, size: usize) -> Result<(&mut [u8], &mut FunctionTable), String> {
|
||||
if self.current.0.len() - self.position < size {
|
||||
self.push_current(cmp::max(0x10000, size))?;
|
||||
}
|
||||
|
||||
let old_position = self.position;
|
||||
self.position += size;
|
||||
Ok(&mut self.current.as_mut_slice()[old_position..self.position])
|
||||
|
||||
Ok((
|
||||
&mut self.current.0.as_mut_slice()[old_position..self.position],
|
||||
&mut self.current.1,
|
||||
))
|
||||
}
|
||||
|
||||
/// Calculates the allocation size of the given compiled function.
|
||||
fn function_allocation_size(func: &CompiledFunction) -> usize {
|
||||
if func.unwind_info.is_empty() {
|
||||
func.body.len()
|
||||
} else {
|
||||
// Account for necessary unwind information alignment padding (32-bit)
|
||||
((func.body.len() + 3) & !3) + func.unwind_info.len()
|
||||
}
|
||||
}
|
||||
|
||||
/// Copies the data of the compiled function to the given buffer.
|
||||
///
|
||||
/// This will also add the function to the current function table.
|
||||
fn copy_function<'a>(
|
||||
func: &CompiledFunction,
|
||||
func_start: u32,
|
||||
buf: &'a mut [u8],
|
||||
table: &'a mut FunctionTable,
|
||||
) -> (
|
||||
u32,
|
||||
&'a mut [u8],
|
||||
&'a mut FunctionTable,
|
||||
&'a mut [VMFunctionBody],
|
||||
) {
|
||||
let func_end = func_start + (func.body.len() as u32);
|
||||
|
||||
let (body, remainder) = buf.split_at_mut(func.body.len());
|
||||
body.copy_from_slice(&func.body);
|
||||
let vmfunc = Self::view_as_mut_vmfunc_slice(body);
|
||||
|
||||
if func.unwind_info.is_empty() {
|
||||
return (func_end, remainder, table, vmfunc);
|
||||
}
|
||||
|
||||
// Keep unwind information 32-bit aligned (round up to the nearest 4 byte boundary)
|
||||
let padding = ((func.body.len() + 3) & !3) - func.body.len();
|
||||
let (unwind, remainder) = remainder.split_at_mut(padding + func.unwind_info.len());
|
||||
unwind[padding..].copy_from_slice(&func.unwind_info);
|
||||
|
||||
let unwind_start = func_end + (padding as u32);
|
||||
let unwind_end = unwind_start + (func.unwind_info.len() as u32);
|
||||
|
||||
table.add_function(func_start, func_end, unwind_start);
|
||||
|
||||
(unwind_end, remainder, table, vmfunc)
|
||||
}
|
||||
|
||||
/// Convert mut a slice from u8 to VMFunctionBody.
|
||||
@@ -52,51 +168,28 @@ impl CodeMemory {
|
||||
unsafe { &mut *body_ptr }
|
||||
}
|
||||
|
||||
/// Allocate enough memory to hold a copy of `slice` and copy the data into it.
|
||||
/// TODO: Reorganize the code that calls this to emit code directly into the
|
||||
/// mmap region rather than into a Vec that we need to copy in.
|
||||
pub fn allocate_copy_of_byte_slice(
|
||||
&mut self,
|
||||
slice: &[u8],
|
||||
) -> Result<&mut [VMFunctionBody], String> {
|
||||
let new = self.allocate(slice.len())?;
|
||||
new.copy_from_slice(slice);
|
||||
Ok(Self::view_as_mut_vmfunc_slice(new))
|
||||
}
|
||||
/// Pushes the current Mmap (and function table) and allocates a new Mmap of the given size.
|
||||
fn push_current(&mut self, new_size: usize) -> Result<(), String> {
|
||||
let previous = mem::replace(
|
||||
&mut self.current,
|
||||
(
|
||||
if new_size == 0 {
|
||||
Mmap::new()
|
||||
} else {
|
||||
Mmap::with_at_least(cmp::max(0x10000, new_size))?
|
||||
},
|
||||
FunctionTable::new(),
|
||||
),
|
||||
);
|
||||
|
||||
/// Allocate enough continuous memory block for multiple code blocks. See also
|
||||
/// allocate_copy_of_byte_slice.
|
||||
pub fn allocate_copy_of_byte_slices(
|
||||
&mut self,
|
||||
slices: &[&[u8]],
|
||||
) -> Result<Box<[&mut [VMFunctionBody]]>, String> {
|
||||
let total_len = slices.into_iter().fold(0, |acc, slice| acc + slice.len());
|
||||
let new = self.allocate(total_len)?;
|
||||
let mut tail = new;
|
||||
let mut result = Vec::with_capacity(slices.len());
|
||||
for slice in slices {
|
||||
let (block, next_tail) = tail.split_at_mut(slice.len());
|
||||
block.copy_from_slice(slice);
|
||||
tail = next_tail;
|
||||
result.push(Self::view_as_mut_vmfunc_slice(block));
|
||||
if previous.0.len() > 0 {
|
||||
self.mmaps.push(previous);
|
||||
} else {
|
||||
assert!(previous.1.len() == 0);
|
||||
}
|
||||
Ok(result.into_boxed_slice())
|
||||
}
|
||||
|
||||
/// Make all allocated memory executable.
|
||||
pub fn publish(&mut self) {
|
||||
self.mmaps
|
||||
.push(mem::replace(&mut self.current, Mmap::new()));
|
||||
self.position = 0;
|
||||
|
||||
for m in &mut self.mmaps[self.published..] {
|
||||
if m.len() != 0 {
|
||||
unsafe {
|
||||
region::protect(m.as_mut_ptr(), m.len(), region::Protection::ReadExecute)
|
||||
}
|
||||
.expect("unable to make memory readonly and executable");
|
||||
}
|
||||
}
|
||||
self.published = self.mmaps.len();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
|
||||
use cranelift_wasm::{DefinedFuncIndex, DefinedMemoryIndex, ModuleTranslationState};
|
||||
use wasmtime_debug::{emit_debugsections_image, DebugInfoData};
|
||||
use wasmtime_environ::{
|
||||
Compilation, CompileError, Compiler as _C, FunctionBodyData, Module, ModuleVmctxInfo,
|
||||
Relocations, Traps, Tunables, VMOffsets,
|
||||
Compilation, CompileError, CompiledFunction, Compiler as _C, FunctionBodyData, Module,
|
||||
ModuleVmctxInfo, Relocations, Traps, Tunables, VMOffsets,
|
||||
};
|
||||
use wasmtime_runtime::{
|
||||
get_mut_trap_registry, InstantiationError, SignatureRegistry, TrapRegistrationGuard,
|
||||
@@ -323,7 +323,8 @@ fn make_trampoline(
|
||||
builder.finalize()
|
||||
}
|
||||
|
||||
let mut code_buf: Vec<u8> = Vec::new();
|
||||
let mut code_buf = Vec::new();
|
||||
let mut unwind_info = Vec::new();
|
||||
let mut reloc_sink = RelocSink {};
|
||||
let mut trap_sink = binemit::NullTrapSink {};
|
||||
let mut stackmap_sink = binemit::NullStackmapSink {};
|
||||
@@ -337,8 +338,14 @@ fn make_trampoline(
|
||||
)
|
||||
.map_err(|error| SetupError::Compile(CompileError::Codegen(error)))?;
|
||||
|
||||
context.emit_unwind_info(isa, &mut unwind_info);
|
||||
|
||||
Ok(code_memory
|
||||
.allocate_copy_of_byte_slice(&code_buf)
|
||||
.allocate_for_function(&CompiledFunction {
|
||||
body: code_buf,
|
||||
jt_offsets: context.func.jt_offsets,
|
||||
unwind_info,
|
||||
})
|
||||
.map_err(|message| SetupError::Instantiate(InstantiationError::Resource(message)))?
|
||||
.as_ptr())
|
||||
}
|
||||
@@ -347,14 +354,8 @@ fn allocate_functions(
|
||||
code_memory: &mut CodeMemory,
|
||||
compilation: &Compilation,
|
||||
) -> Result<PrimaryMap<DefinedFuncIndex, *mut [VMFunctionBody]>, String> {
|
||||
// Allocate code for all function in one continuous memory block.
|
||||
// First, collect all function bodies into vector to pass to the
|
||||
// allocate_copy_of_byte_slices.
|
||||
let bodies = compilation
|
||||
.into_iter()
|
||||
.map(|code_and_jt| &code_and_jt.body[..])
|
||||
.collect::<Vec<&[u8]>>();
|
||||
let fat_ptrs = code_memory.allocate_copy_of_byte_slices(&bodies)?;
|
||||
let fat_ptrs = code_memory.allocate_for_compilation(compilation)?;
|
||||
|
||||
// Second, create a PrimaryMap from result vector of pointers.
|
||||
let mut result = PrimaryMap::with_capacity(compilation.len());
|
||||
for i in 0..fat_ptrs.len() {
|
||||
|
||||
134
wasmtime-jit/src/function_table.rs
Normal file
134
wasmtime-jit/src/function_table.rs
Normal file
@@ -0,0 +1,134 @@
|
||||
//! Runtime function table.
|
||||
//!
|
||||
//! This module is primarily used to track JIT functions on Windows for stack walking and unwind.
|
||||
|
||||
/// Represents a runtime function table.
|
||||
///
|
||||
/// The runtime function table is not implemented for non-Windows target platforms.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub(crate) struct FunctionTable;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl FunctionTable {
|
||||
/// Creates a new function table.
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
/// Returns the number of functions in the table, also referred to as its 'length'.
|
||||
///
|
||||
/// For non-Windows platforms, the table will always be empty.
|
||||
pub fn len(&self) -> usize {
|
||||
0
|
||||
}
|
||||
|
||||
/// Adds a function to the table based off of the start offset, end offset, and unwind offset.
|
||||
///
|
||||
/// The offsets are from the "module base", which is provided when the table is published.
|
||||
///
|
||||
/// For non-Windows platforms, this is a no-op.
|
||||
pub fn add_function(&mut self, _start: u32, _end: u32, _unwind: u32) {}
|
||||
|
||||
/// Publishes the function table using the given base address.
|
||||
///
|
||||
/// A published function table will automatically be deleted when it is dropped.
|
||||
///
|
||||
/// For non-Windows platforms, this is a no-op.
|
||||
pub fn publish(&mut self, _base_address: u64) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a runtime function table.
|
||||
///
|
||||
/// This is used to register JIT code with the operating system to enable stack walking and unwinding.
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
pub(crate) struct FunctionTable {
|
||||
functions: Vec<winapi::um::winnt::RUNTIME_FUNCTION>,
|
||||
published: bool,
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "windows", target_arch = "x86_64"))]
|
||||
impl FunctionTable {
|
||||
/// Creates a new function table.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
functions: Vec::new(),
|
||||
published: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of functions in the table, also referred to as its 'length'.
|
||||
pub fn len(&self) -> usize {
|
||||
self.functions.len()
|
||||
}
|
||||
|
||||
/// Adds a function to the table based off of the start offset, end offset, and unwind offset.
|
||||
///
|
||||
/// The offsets are from the "module base", which is provided when the table is published.
|
||||
pub fn add_function(&mut self, start: u32, end: u32, unwind: u32) {
|
||||
use winapi::um::winnt;
|
||||
|
||||
assert!(!self.published, "table has already been published");
|
||||
|
||||
let mut entry = winnt::RUNTIME_FUNCTION::default();
|
||||
|
||||
entry.BeginAddress = start;
|
||||
entry.EndAddress = end;
|
||||
|
||||
unsafe {
|
||||
*entry.u.UnwindInfoAddress_mut() = unwind;
|
||||
}
|
||||
|
||||
self.functions.push(entry);
|
||||
}
|
||||
|
||||
/// Publishes the function table using the given base address.
|
||||
///
|
||||
/// A published function table will automatically be deleted when it is dropped.
|
||||
pub fn publish(&mut self, base_address: u64) -> Result<(), String> {
|
||||
use winapi::um::winnt;
|
||||
|
||||
if self.published {
|
||||
return Err("function table was already published".into());
|
||||
}
|
||||
|
||||
self.published = true;
|
||||
|
||||
if self.functions.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
// Windows heap allocations are 32-bit aligned, but assert just in case
|
||||
assert!(
|
||||
(self.functions.as_mut_ptr() as u64) % 4 == 0,
|
||||
"function table allocation was not aligned"
|
||||
);
|
||||
|
||||
if winnt::RtlAddFunctionTable(
|
||||
self.functions.as_mut_ptr(),
|
||||
self.functions.len() as u32,
|
||||
base_address,
|
||||
) == 0
|
||||
{
|
||||
return Err("failed to add function table".into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl Drop for FunctionTable {
|
||||
fn drop(&mut self) {
|
||||
use winapi::um::winnt;
|
||||
|
||||
if self.published {
|
||||
unsafe {
|
||||
winnt::RtlDeleteFunctionTable(self.functions.as_mut_ptr());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ mod action;
|
||||
mod code_memory;
|
||||
mod compiler;
|
||||
mod context;
|
||||
mod function_table;
|
||||
mod instantiate;
|
||||
mod link;
|
||||
mod namespace;
|
||||
|
||||
Reference in New Issue
Block a user