Extract jit_int.rs and most of jitdump_linux.rs for use outside of wasmtime (#2744)

* Extract gdb jit_int into wasmtime-jit-debug

* Move a big chunk of the jitdump code to wasmtime-jit-debug

* Fix doc markdown in perf_jitdump.rs
This commit is contained in:
bjorn3
2022-02-22 18:23:44 +01:00
committed by GitHub
parent 2616c28957
commit 4ed353a7e1
11 changed files with 392 additions and 289 deletions

View File

@@ -14,124 +14,14 @@
use crate::{CompiledModule, ProfilingAgent};
use anyhow::Result;
use object::{Object, ObjectSection};
use std::fmt::Debug;
use std::fs::{File, OpenOptions};
use std::io::Write;
use std::ptr;
use std::sync::Mutex;
use std::{borrow, mem, process};
use target_lexicon::Architecture;
use wasmtime_environ::EntityRef;
use wasmtime_jit_debug::perf_jitdump::*;
use object::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(Debug, Default, Clone, Copy)]
#[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,
}
unsafe impl object::Pod for RecordHeader {}
/// The CodeLoadRecord is used for describing jitted functions
#[derive(Debug, Default, Clone, Copy)]
#[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,
}
unsafe impl object::Pod for CodeLoadRecord {}
/// Describes source line information for a jitted function
#[derive(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(Debug, Default, Clone, Copy)]
#[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,
}
unsafe impl object::Pod for DebugInfoRecord {}
/// Fixed-sized header for each jitdump file
#[derive(Debug, Default, Clone, Copy)]
#[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,
}
unsafe impl object::Pod for FileHeader {}
/// Interface for driving the creation of jitdump files
pub struct JitDumpAgent {
// Note that we use a mutex internally to serialize writing out to our
@@ -141,13 +31,7 @@ pub struct JitDumpAgent {
}
struct State {
/// File instance for the jit dump file
jitdump_file: File,
map_addr: usize,
/// Unique identifier for jitted code
code_index: u64,
jitdump_file: JitDumpFile,
/// Flag for experimenting with dumping code load record
/// after each function (true) or after each module. This
@@ -159,42 +43,23 @@ impl JitDumpAgent {
/// Intialize a JitDumpAgent and write out the header
pub fn new() -> Result<Self> {
let filename = format!("./jit-{}.dump", process::id());
let jitdump_file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&filename)?;
// After we make our `*.dump` file we execute an `mmap` syscall,
// specifically with executable permissions, to map it into our address
// space. This is required so `perf inject` will work later. The `perf
// inject` command will see that an mmap syscall happened, and it'll see
// the filename we mapped, and that'll trigger it to actually read and
// parse the file.
//
// To match what some perf examples are doing we keep this `mmap` alive
// until this agent goes away.
let map_addr = unsafe {
let ptr = rustix::io::mmap(
ptr::null_mut(),
rustix::process::page_size(),
rustix::io::ProtFlags::EXEC | rustix::io::ProtFlags::READ,
rustix::io::MapFlags::PRIVATE,
&jitdump_file,
0,
)?;
ptr as usize
let e_machine = match target_lexicon::HOST.architecture {
Architecture::X86_64 => elf::EM_X86_64 as u32,
Architecture::X86_32(_) => elf::EM_386 as u32,
Architecture::Arm(_) => elf::EM_ARM as u32,
Architecture::Aarch64(_) => elf::EM_AARCH64 as u32,
Architecture::S390x => elf::EM_S390 as u32,
_ => unimplemented!("unrecognized architecture"),
};
let mut state = State {
jitdump_file,
map_addr,
code_index: 0,
dump_funcs: true,
};
state.write_file_header()?;
let jitdump_file = JitDumpFile::new(filename, e_machine)?;
Ok(JitDumpAgent {
state: Mutex::new(state),
state: Mutex::new(State {
jitdump_file,
dump_funcs: true,
}),
})
}
}
@@ -212,80 +77,6 @@ impl ProfilingAgent for JitDumpAgent {
}
impl State {
/// Returns timestamp from a single source
fn get_time_stamp(&self) -> u64 {
// We need to use `CLOCK_MONOTONIC` on Linux which is what `Instant`
// conveniently also uses, but `Instant` doesn't allow us to get access
// to nanoseconds as an internal detail, so we calculate the nanoseconds
// ourselves here.
let ts = rustix::time::clock_gettime(rustix::time::ClockId::Monotonic);
// TODO: What does it mean for either sec or nsec to be negative?
(ts.tv_sec * 1_000_000_000 + ts.tv_nsec) as u64
}
/// Returns the ELF machine architecture.
fn get_e_machine(&self) -> u32 {
match target_lexicon::HOST.architecture {
Architecture::X86_64 => elf::EM_X86_64 as u32,
Architecture::X86_32(_) => elf::EM_386 as u32,
Architecture::Arm(_) => elf::EM_ARM as u32,
Architecture::Aarch64(_) => elf::EM_AARCH64 as u32,
Architecture::S390x => elf::EM_S390 as u32,
_ => unimplemented!("unrecognized architecture"),
}
}
fn write_file_header(&mut self) -> Result<()> {
let header = FileHeader {
timestamp: self.get_time_stamp(),
e_machine: self.get_e_machine(),
magic: 0x4A695444,
version: 1,
size: mem::size_of::<FileHeader>() as u32,
pad1: 0,
pid: process::id(),
flags: 0,
};
self.jitdump_file.write_all(object::bytes_of(&header))?;
Ok(())
}
fn write_code_load_record(
&mut self,
record_name: &str,
cl_record: CodeLoadRecord,
code_buffer: &[u8],
) -> Result<()> {
self.jitdump_file.write_all(object::bytes_of(&cl_record))?;
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<()> {
self.jitdump_file.write_all(object::bytes_of(&dir_record))?;
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<DebugEntry>) -> Result<()> {
for entry in die_entries.iter() {
self.jitdump_file
.write_all(object::bytes_of(&entry.address))?;
self.jitdump_file.write_all(object::bytes_of(&entry.line))?;
self.jitdump_file
.write_all(object::bytes_of(&entry.discriminator))?;
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: &CompiledModule, dbg_image: Option<&[u8]>) {
let pid = process::id();
@@ -301,18 +92,28 @@ impl State {
);
}
} else {
let timestamp = self.get_time_stamp();
let timestamp = self.jitdump_file.get_time_stamp();
let name = super::debug_name(module, idx);
self.dump_code_load_record(&name, addr, len, timestamp, pid, tid);
if let Err(err) = self
.jitdump_file
.dump_code_load_record(&name, addr, len, timestamp, pid, tid)
{
println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err);
}
}
}
// Note: these are the trampolines into exported functions.
for (idx, func, len) in module.trampolines() {
let (addr, len) = (func as usize as *const u8, len);
let timestamp = self.get_time_stamp();
let timestamp = self.jitdump_file.get_time_stamp();
let name = format!("wasm::trampoline[{}]", idx.index());
self.dump_code_load_record(&name, addr, len, timestamp, pid, tid);
if let Err(err) = self
.jitdump_file
.dump_code_load_record(&name, addr, len, timestamp, pid, tid)
{
println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err);
}
}
}
@@ -324,44 +125,12 @@ impl State {
pid: u32,
tid: u32,
) {
let timestamp = self.get_time_stamp();
self.dump_code_load_record(name, addr, size, 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::<CodeLoadRecord>();
let rh = RecordHeader {
id: RecordId::JitCodeLoad as u32,
record_size: size_limit as u32 + name_len as u32 + len as u32,
timestamp,
};
let clr = CodeLoadRecord {
header: rh,
pid,
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);
if let Err(err) = self.write_code_load_record(method_name, clr, code_buffer) {
println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err);
}
let timestamp = self.jitdump_file.get_time_stamp();
if let Err(err) = self
.jitdump_file
.dump_code_load_record(&name, addr, size, timestamp, pid, tid)
{
println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err);
}
}
@@ -414,8 +183,13 @@ impl State {
break;
}
if !self.dump_funcs {
let timestamp = self.get_time_stamp();
self.dump_code_load_record(module_name, addr, len, timestamp, pid, tid);
let timestamp = self.jitdump_file.get_time_stamp();
if let Err(err) =
self.jitdump_file
.dump_code_load_record(module_name, addr, len, timestamp, pid, tid)
{
println!("Jitdump: write_code_load_failed_record failed: {:?}\n", err);
}
}
Ok(())
}
@@ -510,16 +284,17 @@ impl State {
clr.header.record_size = mem::size_of::<CodeLoadRecord>() as u32
+ (clr_name.len() + 1) as u32
+ clr.size as u32;
clr.index = self.code_index;
self.code_index += 1;
clr.index = self.jitdump_file.next_code_index();
self.dump_debug_info(&unit, &dwarf, clr.address, clr.size, None)?;
clr.header.timestamp = self.get_time_stamp();
clr.header.timestamp = self.jitdump_file.get_time_stamp();
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);
let _ =
self.jitdump_file
.write_code_load_record(&clr_name, clr, code_buffer);
}
}
} else {
@@ -597,7 +372,7 @@ impl State {
size: u64,
file_suffix: Option<&str>,
) -> Result<()> {
let timestamp = self.get_time_stamp();
let timestamp = self.jitdump_file.get_time_stamp();
if let Some(program) = unit.line_program.clone() {
let mut debug_info_record = DebugInfoRecord {
header: RecordHeader {
@@ -654,21 +429,13 @@ impl State {
debug_info_record.header.record_size =
mem::size_of::<DebugInfoRecord>() as u32 + debug_entries_size as u32;
let _ = self.write_debug_info_record(debug_info_record);
let _ = self.write_debug_info_entries(debug_entries);
let _ = self.jitdump_file.write_debug_info_record(debug_info_record);
let _ = self.jitdump_file.write_debug_info_entries(debug_entries);
}
Ok(())
}
}
impl Drop for State {
fn drop(&mut self) {
unsafe {
rustix::io::munmap(self.map_addr as *mut _, rustix::process::page_size()).unwrap();
}
}
}
trait Reader: gimli::Reader<Offset = usize> + Send + Sync {}
impl<'input, Endian> Reader for gimli::EndianSlice<'input, Endian> where