Skip to content

Commit efa236e

Browse files
authored
Cranelift: implement an "unwinder" crate and exception throws in filetests. (#10919)
This commit introduces the next major piece of machinery (after the previously-landed `try_call` support) that we will eventually use to implement Wasm exceptions in Wasmtime. In particular, it implements a generic unwinder as a new crate that supports (i) walking a stack produced by Cranelift code, (ii) serializing Cranelift exception metadata to compact tables (in a way very similar to address maps in Wasmtime, so they will be mappable directly from disk), (iii) using these serialized tables to find handlers during a stack-walk, and (iv) jumping to handlers (i.e., actually unwinding). This crate is currently used in the filetests runner, and will next be used in Wasmtime. The commit first performs code-motion: it moves stack-walking code from Wasmtime to `cranelift-unwinder`. This itself has no functional effect, but isolates the code that understands contiguous sequences of Cranelift frames ("activations") from that which is specific to Wasmtime's activation delimiters and metadata. It then implements a compact exception-table format. This format uses the `object` crate's mechanisms for directly referencing in-memory arrays of little-endian `u32`s in a way that will allow us to find handlers when mapping exception metadata directly from an ELF section in a `.cwasm` (for example). The format consists of four sorted `u32` arrays in a way that allows us to look up a callsite first, then search its sorted array of handler offsets by tags. It next implements the actual unwind control flow: it contains an assembly stub for each supported architecture that transfers control to a PC, SP, and FP value "up the stack", with payload values placed in the payload registers we have defined per our exception ABI in Cranelift. Finally, it puts these pieces together in the filetest runner. Note that the runtest does a lot "by hand": we don't have entry and exit trampolines as we do in Wasmtime, so the filetest contains three functions, with the middle one invoking the "throw hostcall" and entry and exit trampolines around it grabbing the appropriate entry/exit FPs and exit PC. The dance to call back to host code is also somewhat delicate, as we haven't done this before. The `JITModule`'s linking + relocation support does not seem sufficient to properly define a symbol, so instead we scan for `func_addr` instructions referencing a well-known name (`__cranelift_throw`) and replace them with `iconst`s with the function address at runtime, baking it in. This is somewhat ugly, but it works. All of these filetest-specific details will be handled much more nicely in the Wasmtime version of this functionality, as we have proper abstractions for entry/exit trampolines and hostcalls.
1 parent cb98083 commit efa236e

38 files changed

+1472
-347
lines changed

Cargo.lock

Lines changed: 14 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ wasmtime-fuzzing = { path = "crates/fuzzing" }
249249
wasmtime-jit-icache-coherence = { path = "crates/jit-icache-coherence", version = "=35.0.0" }
250250
wasmtime-wit-bindgen = { path = "crates/wit-bindgen", version = "=35.0.0" }
251251
wasmtime-math = { path = "crates/math", version = "=35.0.0" }
252+
wasmtime-unwinder = { path = "crates/unwinder", version = "=35.0.0" }
252253
test-programs-artifacts = { path = 'crates/test-programs/artifacts' }
253254
wasmtime-test-util = { path = "crates/test-util" }
254255

cranelift/codegen/src/ir/extname.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ impl TestcaseName {
113113
pub(crate) fn new<T: AsRef<[u8]>>(v: T) -> Self {
114114
Self(v.as_ref().into())
115115
}
116+
117+
/// Get the raw test case name as bytes.
118+
pub fn raw(&self) -> &[u8] {
119+
&self.0
120+
}
116121
}
117122

118123
/// The name of an external is either a reference to a user-defined symbol

cranelift/codegen/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ pub mod write;
5959

6060
pub use crate::entity::packed_option;
6161
pub use crate::machinst::buffer::{
62-
FinalizedMachReloc, FinalizedRelocTarget, MachCallSite, MachSrcLoc, MachTextSectionBuilder,
63-
MachTrap, OpenPatchRegion, PatchRegion,
62+
FinalizedMachCallSite, FinalizedMachReloc, FinalizedRelocTarget, MachCallSite, MachSrcLoc,
63+
MachTextSectionBuilder, MachTrap, OpenPatchRegion, PatchRegion,
6464
};
6565
pub use crate::machinst::{
6666
CallInfo, CompiledCode, Final, MachBuffer, MachBufferFinalized, MachInst, MachInstEmit,

cranelift/filetests/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@ cranelift-frontend = { workspace = true }
1919
cranelift-interpreter = { workspace = true }
2020
cranelift-native = { workspace = true }
2121
cranelift-reader = { workspace = true }
22-
cranelift-jit = { workspace = true, features = ["selinux-fix"] }
22+
cranelift-jit = { workspace = true, features = ["selinux-fix", "wasmtime-unwinder"] }
2323
cranelift-module = { workspace = true }
2424
cranelift-control = { workspace = true }
25+
wasmtime-unwinder = { workspace = true, features = ["cranelift"] }
2526
file-per-thread-logger = { workspace = true }
2627
filecheck = { workspace = true }
2728
gimli = { workspace = true, features = ['std'] }
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
test run
2+
set preserve_frame_pointers=true
3+
target x86_64
4+
target aarch64
5+
target riscv64
6+
target s390x
7+
8+
function %entry() -> i64 tail {
9+
fn0 = %main(i64) -> i64 tail
10+
11+
block0:
12+
v1 = get_frame_pointer.i64
13+
v2 = call fn0(v1)
14+
return v2
15+
}
16+
17+
; run: %entry() == 58
18+
19+
function %main(i64) -> i64 tail {
20+
sig0 = (i64, i32, i64, i64) tail
21+
fn0 = %throw(i64, i32, i64, i64) tail
22+
23+
block0(v0: i64):
24+
v1 = iconst.i64 42
25+
v2 = iconst.i64 100
26+
v3 = iconst.i32 1
27+
try_call fn0(v0, v3, v1, v2), sig0, block1(), [ tag1: block2(exn0, exn1) ]
28+
29+
block1:
30+
v4 = iconst.i64 1
31+
return v4
32+
33+
block2(v5: i64, v6: i64):
34+
v7 = isub.i64 v6, v5
35+
return v7
36+
}
37+
38+
39+
function %throw(i64, i32, i64, i64) tail {
40+
sig0 = (i64, i64, i64, i32, i64, i64)
41+
fn0 = %__cranelift_throw(i64, i64, i64, i32, i64, i64)
42+
43+
block0(v0: i64, v1: i32, v2: i64, v3: i64):
44+
v4 = get_frame_pointer.i64
45+
v5 = get_return_address.i64
46+
v6 = load.i64 v5 ; get caller's FP
47+
v7 = func_addr.i64 fn0
48+
call_indirect sig0, v7(v0, v4, v6, v1, v2, v3)
49+
return
50+
}

cranelift/filetests/src/function_runner.rs

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
//! Provides functionality for compiling and running CLIF IR for `run` tests.
22
use anyhow::{Result, anyhow};
33
use core::mem;
4+
use cranelift::prelude::Imm64;
5+
use cranelift_codegen::cursor::{Cursor, FuncCursor};
46
use cranelift_codegen::data_value::DataValue;
57
use cranelift_codegen::ir::{
6-
ExternalName, Function, InstBuilder, Signature, UserExternalName, UserFuncName,
8+
ExternalName, Function, InstBuilder, InstructionData, LibCall, Opcode, Signature,
9+
UserExternalName, UserFuncName,
710
};
811
use cranelift_codegen::isa::{OwnedTargetIsa, TargetIsa};
912
use cranelift_codegen::{CodegenError, Context, ir, settings};
@@ -14,9 +17,10 @@ use cranelift_module::{FuncId, Linkage, Module, ModuleError};
1417
use cranelift_native::builder_with_options;
1518
use cranelift_reader::TestFile;
1619
use pulley_interpreter::interp as pulley;
20+
use std::cell::Cell;
1721
use std::cmp::max;
18-
use std::collections::HashMap;
1922
use std::collections::hash_map::Entry;
23+
use std::collections::{HashMap, HashSet};
2024
use std::ptr::NonNull;
2125
use target_lexicon::Architecture;
2226
use thiserror::Error;
@@ -67,7 +71,7 @@ struct DefinedFunction {
6771
/// let compiled = compiler.compile().unwrap();
6872
/// let trampoline = compiled.get_trampoline(&func).unwrap();
6973
///
70-
/// let returned = trampoline.call(&vec![DataValue::I32(2), DataValue::I32(40)]);
74+
/// let returned = trampoline.call(&compiled, &vec![DataValue::I32(2), DataValue::I32(40)]);
7175
/// assert_eq!(vec![DataValue::I32(42)], returned);
7276
/// ```
7377
pub struct TestFileCompiler {
@@ -255,7 +259,13 @@ impl TestFileCompiler {
255259
}
256260

257261
/// Defines the body of a function
258-
pub fn define_function(&mut self, func: Function, ctrl_plane: &mut ControlPlane) -> Result<()> {
262+
pub fn define_function(
263+
&mut self,
264+
mut func: Function,
265+
ctrl_plane: &mut ControlPlane,
266+
) -> Result<()> {
267+
Self::replace_hostcall_references(&mut func);
268+
259269
let defined_func = self
260270
.defined_functions
261271
.get(&func.name)
@@ -271,6 +281,47 @@ impl TestFileCompiler {
271281
Ok(())
272282
}
273283

284+
fn replace_hostcall_references(func: &mut Function) {
285+
// For every `func_addr` referring to a hostcall that we
286+
// define, replace with an `iconst` with the actual
287+
// address. Then modify the external func references to
288+
// harmless libcall references (that will be unused so
289+
// ignored).
290+
let mut funcrefs_to_remove = HashSet::new();
291+
let mut cursor = FuncCursor::new(func);
292+
while let Some(_block) = cursor.next_block() {
293+
while let Some(inst) = cursor.next_inst() {
294+
match &cursor.func.dfg.insts[inst] {
295+
InstructionData::FuncAddr {
296+
opcode: Opcode::FuncAddr,
297+
func_ref,
298+
} => {
299+
let ext_func = &cursor.func.dfg.ext_funcs[*func_ref];
300+
let hostcall_addr = match &ext_func.name {
301+
ExternalName::TestCase(tc) if tc.raw() == b"__cranelift_throw" => {
302+
Some(__cranelift_throw as usize)
303+
}
304+
_ => None,
305+
};
306+
307+
if let Some(addr) = hostcall_addr {
308+
funcrefs_to_remove.insert(*func_ref);
309+
cursor.func.dfg.insts[inst] = InstructionData::UnaryImm {
310+
opcode: Opcode::Iconst,
311+
imm: Imm64::new(addr as i64),
312+
};
313+
}
314+
}
315+
_ => {}
316+
}
317+
}
318+
}
319+
320+
for to_remove in funcrefs_to_remove {
321+
func.dfg.ext_funcs[to_remove].name = ExternalName::LibCall(LibCall::Probestack);
322+
}
323+
}
324+
274325
/// Creates and registers a trampoline for a function if none exists.
275326
pub fn create_trampoline_for_function(
276327
&mut self,
@@ -356,6 +407,13 @@ impl Drop for CompiledTestFile {
356407
}
357408
}
358409

410+
std::thread_local! {
411+
/// TLS slot used to store a CompiledTestFile reference so that it
412+
/// can be recovered when a hostcall (such as the exception-throw
413+
/// handler) is invoked.
414+
pub static COMPILED_TEST_FILE: Cell<*const CompiledTestFile> = Cell::new(std::ptr::null());
415+
}
416+
359417
/// A callable trampoline
360418
pub struct Trampoline<'a> {
361419
module: &'a JITModule,
@@ -366,16 +424,18 @@ pub struct Trampoline<'a> {
366424

367425
impl<'a> Trampoline<'a> {
368426
/// Call the target function of this trampoline, passing in [DataValue]s using a compiled trampoline.
369-
pub fn call(&self, arguments: &[DataValue]) -> Vec<DataValue> {
427+
pub fn call(&self, compiled: &CompiledTestFile, arguments: &[DataValue]) -> Vec<DataValue> {
370428
let mut values = UnboxedValues::make_arguments(arguments, &self.func_signature);
371429
let arguments_address = values.as_mut_ptr();
372430

373431
let function_ptr = self.module.get_finalized_function(self.func_id);
374432
let trampoline_ptr = self.module.get_finalized_function(self.trampoline_id);
375433

434+
COMPILED_TEST_FILE.set(compiled as *const _);
376435
unsafe {
377436
self.call_raw(trampoline_ptr, function_ptr, arguments_address);
378437
}
438+
COMPILED_TEST_FILE.set(std::ptr::null());
379439

380440
values.collect_returns(&self.func_signature)
381441
}
@@ -563,6 +623,71 @@ fn make_trampoline(name: UserFuncName, signature: &ir::Signature, isa: &dyn Targ
563623
func
564624
}
565625

626+
/// Hostcall invoked directly from a compiled function body to test
627+
/// exception throws.
628+
///
629+
/// This function does not return normally: it either uses the
630+
/// unwinder to jump directly to a Cranelift frame further up the
631+
/// stack, if a handler is found; or it panics, if not.
632+
#[cfg(any(
633+
target_arch = "x86_64",
634+
target_arch = "aarch64",
635+
target_arch = "s390x",
636+
target_arch = "riscv64"
637+
))]
638+
extern "C-unwind" fn __cranelift_throw(
639+
entry_fp: usize,
640+
exit_fp: usize,
641+
exit_pc: usize,
642+
tag: u32,
643+
payload1: usize,
644+
payload2: usize,
645+
) -> ! {
646+
let compiled_test_file = unsafe { &*COMPILED_TEST_FILE.get() };
647+
let unwind_host = wasmtime_unwinder::UnwindHost;
648+
let module_lookup = |pc| {
649+
compiled_test_file
650+
.module
651+
.as_ref()
652+
.unwrap()
653+
.lookup_wasmtime_exception_data(pc)
654+
};
655+
unsafe {
656+
match wasmtime_unwinder::compute_throw_action(
657+
&unwind_host,
658+
module_lookup,
659+
exit_pc,
660+
exit_fp,
661+
entry_fp,
662+
tag,
663+
) {
664+
wasmtime_unwinder::ThrowAction::Handler { pc, sp, fp } => {
665+
wasmtime_unwinder::resume_to_exception_handler(pc, sp, fp, payload1, payload2);
666+
}
667+
wasmtime_unwinder::ThrowAction::None => {
668+
panic!("Expected a handler to exit for throw of tag {tag} at pc {exit_pc:x}");
669+
}
670+
}
671+
}
672+
}
673+
674+
#[cfg(not(any(
675+
target_arch = "x86_64",
676+
target_arch = "aarch64",
677+
target_arch = "s390x",
678+
target_arch = "riscv64"
679+
)))]
680+
extern "C-unwind" fn __cranelift_throw(
681+
_entry_fp: usize,
682+
_exit_fp: usize,
683+
_exit_pc: usize,
684+
_tag: u32,
685+
_payload1: usize,
686+
_payload2: usize,
687+
) -> ! {
688+
panic!("Throw not implemented on platforms without native backends.");
689+
}
690+
566691
#[cfg(target_arch = "x86_64")]
567692
use std::arch::x86_64::__m128i;
568693
#[cfg(target_arch = "x86_64")]
@@ -655,7 +780,7 @@ mod test {
655780
.unwrap();
656781
let compiled = compiler.compile().unwrap();
657782
let trampoline = compiled.get_trampoline(&function).unwrap();
658-
let returned = trampoline.call(&[]);
783+
let returned = trampoline.call(&compiled, &[]);
659784
assert_eq!(returned, vec![DataValue::I8(-1)])
660785
}
661786

cranelift/filetests/src/test_run.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn run_test(
191191
args.extend_from_slice(run_args);
192192

193193
let trampoline = testfile.get_trampoline(func).unwrap();
194-
Ok(trampoline.call(&args))
194+
Ok(trampoline.call(&testfile, &args))
195195
})
196196
.map_err(|s| anyhow::anyhow!("{}", s))?;
197197
}

cranelift/jit/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ cranelift-native = { workspace = true }
1919
cranelift-codegen = { workspace = true, features = ["std"] }
2020
cranelift-entity = { workspace = true }
2121
cranelift-control = { workspace = true }
22+
wasmtime-unwinder = { workspace = true, optional = true, features = ["cranelift"] }
2223
anyhow = { workspace = true }
2324
region = "3.0.2"
2425
libc = { workspace = true }
@@ -39,6 +40,8 @@ features = [
3940
selinux-fix = ['memmap2']
4041
default = []
4142

43+
wasmtime-unwinder = ["dep:wasmtime-unwinder"]
44+
4245
[dev-dependencies]
4346
cranelift = { path = "../umbrella" }
4447
cranelift-frontend = { workspace = true }

0 commit comments

Comments
 (0)