From 228fbb390de7e9640c557ca77018b1c735410679 Mon Sep 17 00:00:00 2001 From: Johnnie Birch <45402135+jlb6740@users.noreply.github.com> Date: Tue, 10 Sep 2019 18:17:19 -0700 Subject: [PATCH] Adds perf jitdump support Patch adds support for the perf jitdump file specification. With this patch it should be possible to see profile data for code generated and maped at runtime. Specifically the patch adds support for the JIT_CODE_LOAD and the JIT_DEBUG_INFO record as described in the specification. Dumping jitfiles is enabled with the --jitdump flag. When the -g flag is also used there is an attempt to dump file and line number information where this option would be most useful when the WASM file already includes DWARF debug information. --- misc/wasmtime-py/src/import.rs | 1 + src/bin/wasmtime.rs | 16 +- tests/instantiate.rs | 9 +- wasmtime-api/src/context.rs | 22 +- wasmtime-api/src/instance.rs | 2 + wasmtime-api/src/runtime.rs | 17 +- wasmtime-api/src/trampoline/create_handle.rs | 1 + wasmtime-jit/src/code_memory.rs | 17 +- wasmtime-jit/src/compiler.rs | 14 +- wasmtime-jit/src/context.rs | 9 + wasmtime-jit/src/instantiate.rs | 38 +- wasmtime-runtime/Cargo.toml | 7 + wasmtime-runtime/src/instance.rs | 15 + wasmtime-runtime/src/jit_dump.rs | 673 +++++++++++++++++++ wasmtime-runtime/src/lib.rs | 2 + wasmtime-wasi/src/instantiate.rs | 1 + wasmtime-wast/src/spectest.rs | 1 + 17 files changed, 828 insertions(+), 17 deletions(-) create mode 100644 wasmtime-runtime/src/jit_dump.rs diff --git a/misc/wasmtime-py/src/import.rs b/misc/wasmtime-py/src/import.rs index b9f7eb4b6ced..4db0273a1181 100644 --- a/misc/wasmtime-py/src/import.rs +++ b/misc/wasmtime-py/src/import.rs @@ -326,6 +326,7 @@ pub fn into_instance_from_obj( &data_initializers, signatures.into_boxed_slice(), None, + None, Box::new(import_obj_state), ) .expect("instance")) diff --git a/src/bin/wasmtime.rs b/src/bin/wasmtime.rs index 07a08b7a4355..4e9ed79d9878 100644 --- a/src/bin/wasmtime.rs +++ b/src/bin/wasmtime.rs @@ -62,8 +62,8 @@ including calling the start function if one is present. Additional functions given with --invoke are then called. Usage: - wasmtime [-odg] [--enable-simd] [--wasi-c] [--disable-cache | --cache-config=] [--preload=...] [--env=...] [--dir=...] [--mapdir=...] [...] - wasmtime [-odg] [--enable-simd] [--wasi-c] [--disable-cache | --cache-config=] [--env=...] [--dir=...] [--mapdir=...] --invoke= [...] + wasmtime [-odg] [--enable-simd] [--jitdump] [--wasi-c] [--disable-cache | --cache-config=] [--preload=...] [--env=...] [--dir=...] [--mapdir=...] [...] + wasmtime [-odg] [--enable-simd] [--jitdump] [--wasi-c] [--disable-cache | --cache-config=] [--env=...] [--dir=...] [--mapdir=...] --invoke= [...] wasmtime --create-cache-config [--cache-config=] wasmtime --help | --version @@ -81,6 +81,7 @@ Options: -g generate debug information -d, --debug enable debug output on stderr/stdout --enable-simd enable proposed SIMD instructions + --jitdump generate perf jitdump files for runtime generated code --wasi-c enable the wasi-c implementation of WASI --preload= load an additional wasm module before loading the main module --env= pass an environment variable (\"key=value\") to the program @@ -102,6 +103,7 @@ struct Args { flag_debug: bool, flag_g: bool, flag_enable_simd: bool, + flag_jitdump: bool, flag_invoke: Option, flag_preload: Vec, flag_env: Vec, @@ -276,12 +278,20 @@ fn rmain() -> Result<(), Error> { features.simd = true; } + // Enable Jitdump if requested + let perf_profile = args.flag_jitdump; + // Enable optimization if requested. if args.flag_optimize { flag_builder.set("opt_level", "best")?; } - let config = Config::new(settings::Flags::new(flag_builder), features, debug_info); + let config = Config::new( + settings::Flags::new(flag_builder), + features, + debug_info, + perf_profile, + ); let engine = HostRef::new(Engine::new(config)); let store = HostRef::new(Store::new(engine)); diff --git a/tests/instantiate.rs b/tests/instantiate.rs index adf49456887d..109f5d2a2bd3 100644 --- a/tests/instantiate.rs +++ b/tests/instantiate.rs @@ -42,6 +42,13 @@ fn test_environ_translate() { let mut resolver = NullResolver {}; let mut compiler = Compiler::new(isa); let global_exports = Rc::new(RefCell::new(HashMap::new())); - let instance = instantiate(&mut compiler, &data, &mut resolver, global_exports, false); + let instance = instantiate( + &mut compiler, + &data, + &mut resolver, + global_exports, + false, + false, + ); assert!(instance.is_ok()); } diff --git a/wasmtime-api/src/context.rs b/wasmtime-api/src/context.rs index 76085dd86852..012f34885295 100644 --- a/wasmtime-api/src/context.rs +++ b/wasmtime-api/src/context.rs @@ -11,25 +11,41 @@ pub struct Context { compiler: Rc>, features: Features, debug_info: bool, + perf_profile: bool, } impl Context { - pub fn new(compiler: Compiler, features: Features, debug_info: bool) -> Context { + pub fn new( + compiler: Compiler, + features: Features, + debug_info: bool, + perf_profile: bool, + ) -> Context { Context { compiler: Rc::new(RefCell::new(compiler)), features, debug_info, + perf_profile, } } - pub fn create(flags: settings::Flags, features: Features, debug_info: bool) -> Context { - Context::new(create_compiler(flags), features, debug_info) + pub fn create( + flags: settings::Flags, + features: Features, + debug_info: bool, + perf_profile: bool, + ) -> Context { + Context::new(create_compiler(flags), features, debug_info, perf_profile) } pub(crate) fn debug_info(&self) -> bool { self.debug_info } + pub(crate) fn perf_profile(&self) -> bool { + self.perf_profile + } + pub(crate) fn compiler(&mut self) -> RefMut { self.compiler.borrow_mut() } diff --git a/wasmtime-api/src/instance.rs b/wasmtime-api/src/instance.rs index a3bc0274e476..5e4822918fce 100644 --- a/wasmtime-api/src/instance.rs +++ b/wasmtime-api/src/instance.rs @@ -33,6 +33,7 @@ pub fn instantiate_in_context( ) -> Result<(InstanceHandle, HashSet), Error> { let mut contexts = HashSet::new(); let debug_info = context.debug_info(); + let perf_profile = context.perf_profile(); let mut resolver = SimpleResolver { imports }; let instance = instantiate( &mut context.compiler(), @@ -40,6 +41,7 @@ pub fn instantiate_in_context( &mut resolver, exports, debug_info, + perf_profile, )?; contexts.insert(context); Ok((instance, contexts)) diff --git a/wasmtime-api/src/runtime.rs b/wasmtime-api/src/runtime.rs index 61de46a3b684..0a733d30bbfe 100644 --- a/wasmtime-api/src/runtime.rs +++ b/wasmtime-api/src/runtime.rs @@ -21,22 +21,30 @@ pub struct Config { flags: settings::Flags, features: Features, debug_info: bool, + perf_profile: bool, } impl Config { pub fn default() -> Config { Config { debug_info: false, + perf_profile: false, features: Default::default(), flags: default_flags(), } } - pub fn new(flags: settings::Flags, features: Features, debug_info: bool) -> Config { + pub fn new( + flags: settings::Flags, + features: Features, + debug_info: bool, + perf_profile: bool, + ) -> Config { Config { flags, features, debug_info, + perf_profile, } } @@ -44,6 +52,10 @@ impl Config { self.debug_info } + pub(crate) fn perf_profile(&self) -> bool { + self.perf_profile + } + pub(crate) fn flags(&self) -> &settings::Flags { &self.flags } @@ -92,9 +104,10 @@ impl Store { let flags = engine.borrow().config().flags().clone(); let features = engine.borrow().config().features().clone(); let debug_info = engine.borrow().config().debug_info(); + let perf_profile = engine.borrow().config().perf_profile(); Store { engine, - context: Context::create(flags, features, debug_info), + context: Context::create(flags, features, debug_info, perf_profile), global_exports: Rc::new(RefCell::new(HashMap::new())), signature_cache: HashMap::new(), } diff --git a/wasmtime-api/src/trampoline/create_handle.rs b/wasmtime-api/src/trampoline/create_handle.rs index f765eed99362..b55f27964743 100644 --- a/wasmtime-api/src/trampoline/create_handle.rs +++ b/wasmtime-api/src/trampoline/create_handle.rs @@ -53,6 +53,7 @@ pub(crate) fn create_handle( &data_initializers, signatures.into_boxed_slice(), None, + None, state, ) .expect("instance")) diff --git a/wasmtime-jit/src/code_memory.rs b/wasmtime-jit/src/code_memory.rs index 641577ad9d3b..e3bc80b4f837 100644 --- a/wasmtime-jit/src/code_memory.rs +++ b/wasmtime-jit/src/code_memory.rs @@ -5,7 +5,7 @@ use region; use std::boxed::Box; use std::string::String; use std::vec::Vec; -use wasmtime_runtime::{Mmap, VMFunctionBody}; +use wasmtime_runtime::{JitDumpAgent, Mmap, VMFunctionBody}; /// Memory manager for executable code. pub(crate) struct CodeMemory { @@ -99,4 +99,19 @@ impl CodeMemory { } self.published = self.mmaps.len(); } + + pub fn perf_module_load( + &mut self, + module_name: &str, + jit_dump_agent: &JitDumpAgent, + dbg_image: Option<&[u8]>, + ) -> () { + for map in &mut self.mmaps { + if map.len() > 0 { + jit_dump_agent + .clone() + .module_load(module_name, map.as_ptr(), map.len(), dbg_image); + } + } + } } diff --git a/wasmtime-jit/src/compiler.rs b/wasmtime-jit/src/compiler.rs index f1e87ce2d597..20e3f8741422 100644 --- a/wasmtime-jit/src/compiler.rs +++ b/wasmtime-jit/src/compiler.rs @@ -21,8 +21,8 @@ use wasmtime_environ::{ Relocations, Traps, Tunables, VMOffsets, }; use wasmtime_runtime::{ - get_mut_trap_registry, InstantiationError, SignatureRegistry, TrapRegistrationGuard, - VMFunctionBody, + get_mut_trap_registry, InstantiationError, JitDumpAgent, SignatureRegistry, + TrapRegistrationGuard, VMFunctionBody, }; /// A WebAssembly code JIT compiler. @@ -207,6 +207,16 @@ impl Compiler { self.code_memory.publish(); } + pub(crate) fn perf_module_load( + &mut self, + module_name: &str, + jit_dump_agent: &JitDumpAgent, + dbg_image: Option<&[u8]>, + ) -> () { + self.code_memory + .perf_module_load(module_name, jit_dump_agent, dbg_image); + } + /// Shared signature registry. pub fn signatures(&mut self) -> &mut SignatureRegistry { &mut self.signatures diff --git a/wasmtime-jit/src/context.rs b/wasmtime-jit/src/context.rs index 144f066e75cc..ff35133f1de2 100644 --- a/wasmtime-jit/src/context.rs +++ b/wasmtime-jit/src/context.rs @@ -77,6 +77,7 @@ pub struct Context { compiler: Box, global_exports: Rc>>>, debug_info: bool, + perf_profile: bool, features: Features, } @@ -88,6 +89,7 @@ impl Context { compiler, global_exports: Rc::new(RefCell::new(HashMap::new())), debug_info: false, + perf_profile: false, features: Default::default(), } } @@ -97,6 +99,11 @@ impl Context { self.debug_info } + /// Get debug_info settings. + pub fn perf_profile(&self) -> bool { + self.perf_profile + } + /// Set debug_info settings. pub fn set_debug_info(&mut self, value: bool) { self.debug_info = value; @@ -122,6 +129,7 @@ impl Context { fn instantiate(&mut self, data: &[u8]) -> Result { self.validate(&data).map_err(SetupError::Validate)?; let debug_info = self.debug_info(); + let perf_profile = self.perf_profile(); instantiate( &mut *self.compiler, @@ -129,6 +137,7 @@ impl Context { &mut self.namespace, Rc::clone(&self.global_exports), debug_info, + perf_profile, ) } diff --git a/wasmtime-jit/src/instantiate.rs b/wasmtime-jit/src/instantiate.rs index a35daf74b2a9..421baaf87957 100644 --- a/wasmtime-jit/src/instantiate.rs +++ b/wasmtime-jit/src/instantiate.rs @@ -20,8 +20,8 @@ use wasmtime_environ::{ CompileError, DataInitializer, DataInitializerLocation, Module, ModuleEnvironment, }; use wasmtime_runtime::{ - Export, GdbJitImageRegistration, Imports, InstanceHandle, InstantiationError, VMFunctionBody, - VMSharedSignatureIndex, + Export, GdbJitImageRegistration, Imports, InstanceHandle, InstantiationError, JitDumpAgent, + VMFunctionBody, VMSharedSignatureIndex, }; /// An error condition while setting up a wasm instance, be it validation, @@ -55,6 +55,7 @@ struct RawCompiledModule<'data> { data_initializers: Box<[DataInitializer<'data>]>, signatures: BoxedSlice, dbg_jit_registration: Option, + jit_dump_agent: Option, } impl<'data> RawCompiledModule<'data> { @@ -64,6 +65,7 @@ impl<'data> RawCompiledModule<'data> { data: &'data [u8], resolver: &mut dyn Resolver, debug_info: bool, + perf_profile: bool, ) -> Result { let environ = ModuleEnvironment::new(compiler.frontend_config(), compiler.tunables()); @@ -117,6 +119,22 @@ impl<'data> RawCompiledModule<'data> { // Make all code compiled thus far executable. compiler.publish_compiled_code(); + // Create jitdump files + let jit_dump_agent = if perf_profile { + let agent = JitDumpAgent::new().ok(); + let region_name = String::from("wasm_module"); + match &agent { + Some(agent) => match &dbg_image { + Some(dbg) => compiler.perf_module_load(®ion_name, agent, Some(&dbg)), + _ => compiler.perf_module_load(®ion_name, agent, None), + }, + _ => (), + } + agent + } else { + None + }; + let dbg_jit_registration = if let Some(img) = dbg_image { let mut bytes = Vec::new(); bytes.write_all(&img).expect("all written"); @@ -133,6 +151,7 @@ impl<'data> RawCompiledModule<'data> { data_initializers: translation.data_initializers.into_boxed_slice(), signatures: signatures.into_boxed_slice(), dbg_jit_registration, + jit_dump_agent, }) } } @@ -146,6 +165,7 @@ pub struct CompiledModule { signatures: BoxedSlice, global_exports: Rc>>>, dbg_jit_registration: Option>, + jit_dump_agent: Option, } impl CompiledModule { @@ -156,8 +176,10 @@ impl CompiledModule { resolver: &mut dyn Resolver, global_exports: Rc>>>, debug_info: bool, + perf_profile: bool, ) -> Result { - let raw = RawCompiledModule::<'data>::new(compiler, data, resolver, debug_info)?; + let raw = + RawCompiledModule::<'data>::new(compiler, data, resolver, debug_info, perf_profile)?; Ok(Self::from_parts( raw.module, @@ -171,6 +193,7 @@ impl CompiledModule { .into_boxed_slice(), raw.signatures.clone(), raw.dbg_jit_registration, + raw.jit_dump_agent, )) } @@ -183,6 +206,7 @@ impl CompiledModule { data_initializers: Box<[OwnedDataInitializer]>, signatures: BoxedSlice, dbg_jit_registration: Option, + jit_dump_agent: Option, ) -> Self { Self { module: Rc::new(module), @@ -192,6 +216,7 @@ impl CompiledModule { data_initializers, signatures, dbg_jit_registration: dbg_jit_registration.map(|r| Rc::new(r)), + jit_dump_agent, } } @@ -217,6 +242,7 @@ impl CompiledModule { &data_initializers, self.signatures.clone(), self.dbg_jit_registration.as_ref().map(|r| Rc::clone(&r)), + self.jit_dump_agent.clone(), Box::new(()), ) } @@ -243,7 +269,7 @@ impl OwnedDataInitializer { /// Create a new wasm instance by compiling the wasm module in `data` and instatiating it. /// -/// This is equivalent to createing a `CompiledModule` and calling `instantiate()` on it, +/// This is equivalent to creating a `CompiledModule` and calling `instantiate()` on it, /// but avoids creating an intermediate copy of the data initializers. pub fn instantiate( compiler: &mut Compiler, @@ -251,8 +277,9 @@ pub fn instantiate( resolver: &mut dyn Resolver, global_exports: Rc>>>, debug_info: bool, + perf_profile: bool, ) -> Result { - let raw = RawCompiledModule::new(compiler, data, resolver, debug_info)?; + let raw = RawCompiledModule::new(compiler, data, resolver, debug_info, perf_profile)?; InstanceHandle::new( Rc::new(raw.module), @@ -262,6 +289,7 @@ pub fn instantiate( &*raw.data_initializers, raw.signatures, raw.dbg_jit_registration.map(|r| Rc::new(r)), + raw.jit_dump_agent, Box::new(()), ) .map_err(SetupError::Instantiate) diff --git a/wasmtime-runtime/Cargo.toml b/wasmtime-runtime/Cargo.toml index 82a768511290..b809c7f5e002 100644 --- a/wasmtime-runtime/Cargo.toml +++ b/wasmtime-runtime/Cargo.toml @@ -23,6 +23,12 @@ memoffset = "0.5.1" failure = { version = "0.1.3", default-features = false } failure_derive = { version = "0.1.3", default-features = false } indexmap = "1.0.2" +goblin = "0.0.24" +serde = { version = "1.0.99", features = ["derive"] } +scroll = "0.9.2" +gimli = "0.19.0" +object = "0.12.0" +target-lexicon = "0.4.0" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3.7", features = ["winbase", "memoryapi"] } @@ -33,6 +39,7 @@ cc = "1.0" [features] default = ["std"] std = ["cranelift-codegen/std", "cranelift-wasm/std", "wasmtime-environ/std"] +jitdump = [] [badges] maintenance = { status = "experimental" } diff --git a/wasmtime-runtime/src/instance.rs b/wasmtime-runtime/src/instance.rs index 603851ac9ce1..a0e9da025102 100644 --- a/wasmtime-runtime/src/instance.rs +++ b/wasmtime-runtime/src/instance.rs @@ -4,6 +4,7 @@ use crate::export::Export; use crate::imports::Imports; +use crate::jit_dump::JitDumpAgent; use crate::jit_int::GdbJitImageRegistration; use crate::memory::LinearMemory; use crate::mmap::Mmap; @@ -220,6 +221,9 @@ pub(crate) struct Instance { /// Optional image of JIT'ed code for debugger registration. dbg_jit_registration: Option>, + /// Agent for optional creation of jitdump files. + jit_dump_agent: Option, + /// Additional context used by compiled wasm code. This field is last, and /// represents a dynamically-sized array that extends beyond the nominal /// end of the struct (similar to a flexible array member). @@ -663,6 +667,10 @@ impl Instance { .unwrap_or_else(|| panic!("no table for index {}", table_index.index())) .get_mut(index) } + + pub(crate) fn get_jit_dump_agent(&self) -> &Option { + &self.jit_dump_agent + } } /// A handle holding an `Instance` of a WebAssembly module. @@ -681,6 +689,7 @@ impl InstanceHandle { data_initializers: &[DataInitializer<'_>], vmshared_signatures: BoxedSlice, dbg_jit_registration: Option>, + jit_dump_agent: Option, host_state: Box, ) -> Result { let mut tables = create_tables(&module); @@ -723,6 +732,7 @@ impl InstanceHandle { tables, finished_functions, dbg_jit_registration, + jit_dump_agent, host_state, vmctx: VMContext {}, }; @@ -947,6 +957,11 @@ impl InstanceHandle { ) -> Option<&mut VMCallerCheckedAnyfunc> { self.instance_mut().table_get_mut(table_index, index) } + + /// Returns a reference to the instance's JitDumpAgent + pub fn get_jit_dump_agent(&self) -> &Option { + self.instance().get_jit_dump_agent() + } } impl InstanceHandle { diff --git a/wasmtime-runtime/src/jit_dump.rs b/wasmtime-runtime/src/jit_dump.rs new file mode 100644 index 000000000000..e36793829feb --- /dev/null +++ b/wasmtime-runtime/src/jit_dump.rs @@ -0,0 +1,673 @@ +//! Support for jitdump files which can be used by perf for profiling jitted code. +//! Spec definitions for the output format is as described here: +//! https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/tools/perf/Documentation/jitdump-specification.txt +//! +//! Usage Example: +//! Record +//! sudo perf record -k 1 -e instructions:u target/debug/wasmtime -g -jitdump test.wasm +//! Combine +//! sudo perf inject -v -j -i perf.data -o perf.jit.data +//! Report +//! sudo perf report -i perf.jit.data -F+period,srcline +//! Note: For descriptive results, the WASM file being executed should contain dwarf debug data +use libc::c_int; +#[cfg(not(target_os = "windows"))] +use libc::{c_void, clock_gettime, mmap, open, sysconf}; +use object::Object; +use scroll::{IOwrite, SizeWith, NATIVE}; +use serde::{Deserialize, Serialize}; +#[cfg(not(target_os = "windows"))] +use std::ffi::CString; +use std::fmt::Debug; +use std::fs::File; +use std::io::Write; +#[cfg(not(target_os = "windows"))] +use std::os::unix::io::FromRawFd; +use std::{borrow, mem, process}; +use target_lexicon::Architecture; + +#[cfg(target_pointer_width = "64")] +use goblin::elf64 as elf; + +#[cfg(target_pointer_width = "32")] +use goblin::elf32 as elf; + +/// Defines jitdump record types +#[repr(u32)] +pub enum RecordId { + /// Value 0: JIT_CODE_LOAD: record describing a jitted function + JitCodeLoad = 0, + /// Value 1: JIT_CODE_MOVE: record describing an already jitted function which is moved + _JitCodeMove = 1, + /// Value 2: JIT_CODE_DEBUG_INFO: record describing the debug information for a jitted function + JitCodeDebugInfo = 2, + /// Value 3: JIT_CODE_CLOSE: record marking the end of the jit runtime (optional) + _JitCodeClose = 3, + /// Value 4: JIT_CODE_UNWINDING_INFO: record describing a function unwinding information + _JitCodeUnwindingInfo = 4, +} + +/// Each record starts with this fixed size record header which describes the record that follows +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct RecordHeader { + /// uint32_t id: a value identifying the record type (see below) + id: u32, + /// uint32_t total_size: the size in bytes of the record including the header. + record_size: u32, + /// uint64_t timestamp: a timestamp of when the record was created. + timestamp: u64, +} + +/// The CodeLoadRecord is used for describing jitted functions +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct CodeLoadRecord { + /// Fixed sized header that describes this record + header: RecordHeader, + /// uint32_t pid: OS process id of the runtime generating the jitted code + pid: u32, + /// uint32_t tid: OS thread identification of the runtime thread generating the jitted code + tid: u32, + /// uint64_t vma: virtual address of jitted code start + virtual_address: u64, + /// uint64_t code_addr: code start address for the jitted code. By default vma = code_addr + address: u64, + /// uint64_t code_size: size in bytes of the generated jitted code + size: u64, + /// uint64_t code_index: unique identifier for the jitted code (see below) + index: u64, +} + +/// Describes source line information for a jitted function +#[derive(Serialize, Deserialize, Debug, Default)] +#[repr(C)] +pub struct DebugEntry { + /// uint64_t code_addr: address of function for which the debug information is generated + address: u64, + /// uint32_t line: source file line number (starting at 1) + line: u32, + /// uint32_t discrim: column discriminator, 0 is default + discriminator: u32, + /// char name[n]: source file name in ASCII, including null termination + filename: String, +} + +/// Describes debug information for a jitted function. An array of debug entries are +/// appended to this record during writting. Note, this record must preceed the code +/// load record that describes the same jitted function. +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, IOwrite, SizeWith)] +#[repr(C)] +pub struct DebugInfoRecord { + /// Fixed sized header that describes this record + header: RecordHeader, + /// uint64_t code_addr: address of function for which the debug information is generated + address: u64, + /// uint64_t nr_entry: number of debug entries for the function appended to this record + count: u64, +} + +/// Fixed-sized header for each jitdump file +#[derive(Serialize, Deserialize, Debug, Default, IOwrite, SizeWith)] +#[repr(C)] +pub struct FileHeader { + /// uint32_t magic: a magic number tagging the file type. The value is 4-byte long and represents the + /// string "JiTD" in ASCII form. It is 0x4A695444 or 0x4454694a depending on the endianness. The field can + /// be used to detect the endianness of the file + magic: u32, + /// uint32_t version: a 4-byte value representing the format version. It is currently set to 2 + version: u32, + /// uint32_t total_size: size in bytes of file header + size: u32, + /// uint32_t elf_mach: ELF architecture encoding (ELF e_machine value as specified in /usr/include/elf.h) + e_machine: u32, + /// uint32_t pad1: padding. Reserved for future use + pad1: u32, + /// uint32_t pid: JIT runtime process identification (OS specific) + pid: u32, + /// uint64_t timestamp: timestamp of when the file was created + timestamp: u64, + /// uint64_t flags: a bitmask of flags + flags: u64, +} + +/// Interface for driving the creation of jitdump files +#[derive(Debug)] +pub struct JitDumpAgent { + /// File instance for the jit dump file + jitdump_file: File, + /// Unique identifier for jitted code + code_index: u64, + /// Flag for experimenting with dumping code load record + /// after each function (true) or after each module. This + /// flag is currently set to true. + dump_funcs: bool, +} + +impl JitDumpAgent { + /// Intialize a JitDumpAgent and write out the header + pub fn new() -> Result { + #[cfg(target_os = "windows")] + return Err(Error::NulError); + + #[cfg(not(target_os = "windows"))] + { + let filename = format!("./jit-{}.dump", process::id()); + let mut jitdump_file; + unsafe { + let filename_c = CString::new(filename)?; + let fd = open( + filename_c.as_ptr(), + libc::O_CREAT | libc::O_TRUNC | libc::O_RDWR, + 0666, + ); + let pgsz = sysconf(libc::_SC_PAGESIZE) as usize; + mmap( + 0 as *mut c_void, + pgsz, + libc::PROT_EXEC | libc::PROT_READ, + libc::MAP_PRIVATE, + fd, + 0, + ); + jitdump_file = File::from_raw_fd(fd); + } + JitDumpAgent::write_file_header(&mut jitdump_file)?; + + Ok(Self { + jitdump_file: jitdump_file, + code_index: 0, + dump_funcs: true, + }) + } + } + + /// Returns timestamp from a single source + #[allow(unused_variables)] + fn get_time_stamp(timestamp: &mut u64) -> c_int { + #[cfg(not(target_os = "windows"))] + { + unsafe { + let mut ts = mem::MaybeUninit::zeroed().assume_init(); + clock_gettime(libc::CLOCK_MONOTONIC, &mut ts); + // TODO: What does it mean for either sec or nsec to be negative? + *timestamp = (ts.tv_sec * 1000000000 + ts.tv_nsec) as u64; + } + } + return 0; + } + + /// Returns the ELF machine architecture. + #[allow(dead_code)] + fn get_e_machine() -> u32 { + match target_lexicon::HOST.architecture { + Architecture::X86_64 => elf::header::EM_X86_64 as u32, + Architecture::I686 => elf::header::EM_386 as u32, + Architecture::Arm => elf::header::EM_ARM as u32, + Architecture::Armv4t => elf::header::EM_ARM as u32, + Architecture::Armv5te => elf::header::EM_ARM as u32, + Architecture::Armv7 => elf::header::EM_ARM as u32, + Architecture::Armv7s => elf::header::EM_ARM as u32, + Architecture::Aarch64 => elf::header::EM_AARCH64 as u32, + _ => unimplemented!("unrecognized architecture"), + } + } + + #[allow(dead_code)] + fn write_file_header(file: &mut File) -> Result<(), Error> { + let mut header: FileHeader = Default::default(); + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + header.timestamp = timestamp; + + let e_machine = JitDumpAgent::get_e_machine(); + if e_machine != elf::header::EM_NONE as u32 { + header.e_machine = e_machine; + } + + if cfg!(target_endian = "little") { + header.magic = 0x4A695444 + } else { + header.magic = 0x4454694a + } + header.version = 1; + header.size = mem::size_of::() as u32; + header.pad1 = 0; + header.pid = process::id(); + header.flags = 0; + + file.iowrite_with(header, NATIVE)?; + Ok(()) + } + + fn write_code_load_record( + &mut self, + record_name: &str, + cl_record: CodeLoadRecord, + code_buffer: &[u8], + ) -> Result<(), Error> { + self.jitdump_file.iowrite_with(cl_record, NATIVE)?; + self.jitdump_file.write_all(record_name.as_bytes())?; + self.jitdump_file.write_all(b"\0")?; + self.jitdump_file.write_all(code_buffer)?; + Ok(()) + } + + /// Write DebugInfoRecord to open jit dump file. + /// Must be written before the corresponding CodeLoadRecord. + fn write_debug_info_record(&mut self, dir_record: DebugInfoRecord) -> Result<(), Error> { + self.jitdump_file.iowrite_with(dir_record, NATIVE)?; + Ok(()) + } + + /// Write DebugInfoRecord to open jit dump file. + /// Must be written before the corresponding CodeLoadRecord. + fn write_debug_info_entries(&mut self, die_entries: Vec) -> Result<(), Error> { + for entry in die_entries.iter() { + self.jitdump_file.iowrite_with(entry.address, NATIVE)?; + self.jitdump_file.iowrite_with(entry.line, NATIVE)?; + self.jitdump_file + .iowrite_with(entry.discriminator, NATIVE)?; + self.jitdump_file.write_all(entry.filename.as_bytes())?; + self.jitdump_file.write_all(b"\0")?; + } + Ok(()) + } + + /// Sent when a method is compiled and loaded into memory by the VM. + pub fn module_load( + &mut self, + module_name: &str, + addr: *const u8, + len: usize, + dbg_image: Option<&[u8]>, + ) -> () { + let pid = process::id(); + let tid = pid; // ThreadId does appear to track underlying thread. Using PID. + + if let Some(img) = &dbg_image { + let _ = self.dump_from_debug_image(img, module_name, addr, len, pid, tid); + } else { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); + } + } + + fn dump_code_load_record( + &mut self, + method_name: &str, + addr: *const u8, + len: usize, + timestamp: u64, + pid: u32, + tid: u32, + ) -> () { + let name_len = method_name.len() + 1; + let size_limit = mem::size_of::(); + + let rh = RecordHeader { + id: RecordId::JitCodeLoad as u32, + record_size: size_limit as u32 + name_len as u32 + len as u32, + timestamp: timestamp, + }; + + let clr = CodeLoadRecord { + header: rh, + pid: pid, + tid: tid, + virtual_address: addr as u64, + address: addr as u64, + size: len as u64, + index: self.code_index, + }; + self.code_index += 1; + + unsafe { + let code_buffer: &[u8] = std::slice::from_raw_parts(addr, len); + let _ = self.write_code_load_record(method_name, clr, code_buffer); + } + } + + /// Attempts to dump debuginfo data structures, adding method and line level + /// for the jitted function. + pub fn dump_from_debug_image( + &mut self, + dbg_image: &[u8], + module_name: &str, + addr: *const u8, + len: usize, + pid: u32, + tid: u32, + ) -> Result<(), Error> { + let file = object::File::parse(&dbg_image).unwrap(); + let endian = if file.is_little_endian() { + gimli::RunTimeEndian::Little + } else { + gimli::RunTimeEndian::Big + }; + + let load_section = |id: gimli::SectionId| -> Result, Error> { + Ok(file + .section_data_by_name(id.name()) + .unwrap_or(borrow::Cow::Borrowed(&[][..]))) + }; + + let load_section_sup = |_| Ok(borrow::Cow::Borrowed(&[][..])); + let dwarf_cow = gimli::Dwarf::load(&load_section, &load_section_sup)?; + let borrow_section: &dyn for<'a> Fn( + &'a borrow::Cow<[u8]>, + ) + -> gimli::EndianSlice<'a, gimli::RunTimeEndian> = + &|section| gimli::EndianSlice::new(&*section, endian); + + let dwarf = dwarf_cow.borrow(&borrow_section); + + let mut iter = dwarf.units(); + while let Some(header) = iter.next()? { + let unit = match dwarf.unit(header) { + Ok(unit) => unit, + Err(_err) => { + return Ok(()); + } + }; + self.dump_entries(unit, &dwarf, module_name, addr, len, pid, tid)?; + // TODO: Temp exit to avoid duplicate addresses being covered by only + // processing the top unit + break; + } + if !self.dump_funcs { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid); + } + Ok(()) + } + + fn dump_entries( + &mut self, + unit: gimli::Unit, + dwarf: &gimli::Dwarf, + module_name: &str, + addr: *const u8, + len: usize, + pid: u32, + tid: u32, + ) -> Result<(), Error> { + let mut depth = 0; + let mut entries = unit.entries(); + + while let Some((delta_depth, entry)) = entries.next_dfs()? { + if self.dump_funcs { + let record_header = RecordHeader { + id: RecordId::JitCodeLoad as u32, + record_size: 0, + timestamp: 0, + }; + + let mut clr = CodeLoadRecord { + header: record_header, + pid: pid, + tid: tid, + virtual_address: 0, + address: 0, + size: 0, + index: 0, + }; + let mut clr_name: String = String::from(module_name); + + let mut get_debug_entry = false; + depth += delta_depth; + assert!(depth >= 0); + + if entry.tag() == gimli::constants::DW_TAG_subprogram { + get_debug_entry = true; + + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + if let Some(n) = attr.name().static_string() { + if n == "DW_AT_low_pc" { + clr.address = match attr.value() { + gimli::AttributeValue::Addr(address) => address, + _ => 0, + }; + clr.virtual_address = clr.address; + } else if n == "DW_AT_high_pc" { + clr.size = match attr.value() { + gimli::AttributeValue::Udata(data) => data, + _ => 0, + }; + } else if n == "DW_AT_name" { + clr_name = match attr.value() { + gimli::AttributeValue::DebugStrRef(offset) => { + if let Ok(s) = dwarf.debug_str.get_str(offset) { + clr_name.push_str("::"); + clr_name.push_str(&s.to_string_lossy()?); + clr_name + } else { + clr_name.push_str("::"); + clr_name.push_str("?"); + clr_name + } + } + _ => { + clr_name.push_str("??"); + clr_name + } + }; + } + } + } + } + if get_debug_entry { + // TODO: Temp check to make sure well only formed data is processed. + if clr.address == 0 { + continue; + } + // TODO: Temp check to make sure well only formed data is processed. + if clr_name == "?" { + continue; + } + if clr.address == 0 || clr.size == 0 { + clr.address = addr as u64; + clr.virtual_address = addr as u64; + clr.size = len as u64; + } + clr.header.record_size = mem::size_of::() as u32 + + (clr_name.len() + 1) as u32 + + clr.size as u32; + clr.index = self.code_index; + self.code_index += 1; + self.dump_debug_info(&unit, &dwarf, clr.address, clr.size, None)?; + + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + clr.header.timestamp = timestamp; + + unsafe { + let code_buffer: &[u8] = + std::slice::from_raw_parts(clr.address as *const u8, clr.size as usize); + let _ = self.write_code_load_record(&clr_name, clr, code_buffer); + } + } + } else { + let mut func_name: String = String::from("?"); + let mut func_addr = 0; + let mut func_size = 0; + + let mut get_debug_entry = false; + depth += delta_depth; + assert!(depth >= 0); + if entry.tag() == gimli::constants::DW_TAG_subprogram { + get_debug_entry = true; + + let mut attrs = entry.attrs(); + while let Some(attr) = attrs.next()? { + if let Some(n) = attr.name().static_string() { + if n == "DW_AT_low_pc" { + func_addr = match attr.value() { + gimli::AttributeValue::Addr(address) => address, + _ => 0, + }; + } else if n == "DW_AT_high_pc" { + func_size = match attr.value() { + gimli::AttributeValue::Udata(data) => data, + _ => 0, + }; + } else if n == "DW_AT_name" { + func_name = match attr.value() { + gimli::AttributeValue::DebugStrRef(offset) => { + if let Ok(s) = dwarf.debug_str.get_str(offset) { + func_name.clear(); + func_name.push_str(&s.to_string_lossy()?); + func_name + } else { + func_name.push_str("?"); + func_name + } + } + _ => { + func_name.push_str("??"); + func_name + } + }; + } + } + } + } + if get_debug_entry { + // TODO: Temp check to make sure well only formed data is processed. + if func_addr == 0 { + continue; + } + // TODO: Temp check to make sure well only formed data is processed. + if func_name == "?" { + continue; + } + self.dump_debug_info( + &unit, + &dwarf, + func_addr, + func_size, + Some(func_name.as_str()), + )?; + } + } + } + Ok(()) + } + + fn dump_debug_info( + &mut self, + unit: &gimli::Unit, + dwarf: &gimli::Dwarf, + address: u64, + size: u64, + file_suffix: Option<&str>, + ) -> Result<(), Error> { + let mut timestamp: u64 = 0; + JitDumpAgent::get_time_stamp(&mut timestamp); + if let Some(program) = unit.line_program.clone() { + let mut debug_info_record = DebugInfoRecord { + header: RecordHeader { + id: RecordId::JitCodeDebugInfo as u32, + record_size: 0, + timestamp: timestamp, + }, + address: address, + count: 0, + }; + + let mut debug_entries = Vec::new(); + let mut debug_entries_total_filenames_len = 0; + let mut rows = program.rows(); + while let Some((header, row)) = rows.next_row()? { + let row_file_index = row.file_index() - 1; + let myfile = dwarf + .attr_string( + &unit, + header.file_names()[row_file_index as usize].path_name(), + ) + .unwrap(); + let filename = myfile.to_string_lossy()?; + let line = row.line().unwrap_or(0); + let column = match row.column() { + gimli::ColumnType::Column(column) => column, + gimli::ColumnType::LeftEdge => 0, + }; + + if (row.address() < address) || (row.address() > (address + size)) { + continue; + } + let mut debug_entry = DebugEntry { + address: row.address(), + line: line as u32, + discriminator: column as u32, + filename: filename.to_string(), + }; + + if let Some(suffix) = file_suffix { + debug_entry.filename.push_str("::"); + debug_entry.filename.push_str(suffix); + } + + debug_entries_total_filenames_len += debug_entry.filename.len() + 1; + debug_entries.push(debug_entry); + } + + debug_info_record.count = debug_entries.len() as u64; + + let debug_entries_size = (debug_info_record.count + * (mem::size_of::() as u64 - mem::size_of::() as u64)) + + debug_entries_total_filenames_len as u64; + debug_info_record.header.record_size = + mem::size_of::() as u32 + debug_entries_size as u32; + + let _ = self.write_debug_info_record(debug_info_record); + let _ = self.write_debug_info_entries(debug_entries); + } + Ok(()) + } +} + +impl Clone for JitDumpAgent { + fn clone(&self) -> Self { + Self { + jitdump_file: self.jitdump_file.try_clone().unwrap(), + code_index: self.code_index, + dump_funcs: self.dump_funcs, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Error { + GimliError(gimli::Error), + IOError, + NulError, +} + +impl From for Error { + fn from(err: gimli::Error) -> Self { + Error::GimliError(err) + } +} + +impl From for Error { + fn from(_err: std::io::Error) -> Self { + Error::IOError + } +} + +impl From for Error { + fn from(_err: std::ffi::NulError) -> Self { + Error::NulError + } +} + +trait Reader: gimli::Reader + Send + Sync {} + +impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where + Endian: gimli::Endianity + Send + Sync +{ +} diff --git a/wasmtime-runtime/src/lib.rs b/wasmtime-runtime/src/lib.rs index b3f394bfe66a..f175616ee0da 100644 --- a/wasmtime-runtime/src/lib.rs +++ b/wasmtime-runtime/src/lib.rs @@ -32,6 +32,7 @@ extern crate failure_derive; mod export; mod imports; mod instance; +mod jit_dump; mod jit_int; mod memory; mod mmap; @@ -47,6 +48,7 @@ pub mod libcalls; pub use crate::export::Export; pub use crate::imports::Imports; pub use crate::instance::{InstanceHandle, InstantiationError, LinkError}; +pub use crate::jit_dump::JitDumpAgent; pub use crate::jit_int::GdbJitImageRegistration; pub use crate::mmap::Mmap; pub use crate::sig_registry::SignatureRegistry; diff --git a/wasmtime-wasi/src/instantiate.rs b/wasmtime-wasi/src/instantiate.rs index 0b83681280bd..b22e9f8c8d53 100644 --- a/wasmtime-wasi/src/instantiate.rs +++ b/wasmtime-wasi/src/instantiate.rs @@ -133,6 +133,7 @@ pub fn instantiate_wasi( &data_initializers, signatures.into_boxed_slice(), None, + None, Box::new(wasi_ctx), ) } diff --git a/wasmtime-wast/src/spectest.rs b/wasmtime-wast/src/spectest.rs index e82eb09c13be..62fccda85a2f 100644 --- a/wasmtime-wast/src/spectest.rs +++ b/wasmtime-wast/src/spectest.rs @@ -224,6 +224,7 @@ pub fn instantiate_spectest() -> Result { &data_initializers, signatures.into_boxed_slice(), None, + None, Box::new(()), ) }