From d764a058f7426cbe1ba1167801c64be603455aa6 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 07:58:33 +0200 Subject: [PATCH 1/7] add mock host for tests --- src/lib.rs | 3 ++ src/runtime/mock.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++ src/runtime/mod.rs | 2 ++ 3 files changed, 79 insertions(+) create mode 100644 src/runtime/mock.rs diff --git a/src/lib.rs b/src/lib.rs index e178811c..403a41fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,6 +127,9 @@ #[cfg(feature = "alloc")] extern crate alloc; +#[cfg(all(test, not(target_family = "wasm")))] +extern crate std; + mod primitives; mod runtime; diff --git a/src/runtime/mock.rs b/src/runtime/mock.rs new file mode 100644 index 00000000..baf18e9a --- /dev/null +++ b/src/runtime/mock.rs @@ -0,0 +1,74 @@ +//! A fake host for tests: definitions of the wasm imports the runtime layer +//! links against, backed by in-memory images so readers can run on the host. + +use core::{cell::RefCell, num::NonZeroU64}; + +use std::vec::Vec; + +use crate::Process; + +std::thread_local! { + static MEMORY: RefCell)>> = const { RefCell::new(Vec::new()) }; +} + +/// Runs a test against a process whose memory holds the given regions, each an +/// address and the bytes starting there. Reads outside every region fail. +pub fn with_process(regions: &[(u64, &[u8])], test: impl FnOnce(&Process) -> R) -> R { + MEMORY.with(|memory| { + *memory.borrow_mut() = regions + .iter() + .map(|&(address, bytes)| (address, bytes.to_vec())) + .collect(); + }); + let process = Process::attach("mock").expect("the mock always attaches"); + test(&process) +} + +#[no_mangle] +extern "C" fn process_attach(_name_ptr: *const u8, _name_len: usize) -> Option { + NonZeroU64::new(1) +} + +#[no_mangle] +extern "C" fn process_detach(_process: u64) {} + +#[no_mangle] +extern "C" fn process_read(_process: u64, address: u64, buf_ptr: *mut u8, buf_len: usize) -> bool { + MEMORY.with(|memory| { + memory.borrow().iter().any(|(start, bytes)| { + let Some(offset) = address.checked_sub(*start) else { + return false; + }; + let Ok(offset) = usize::try_from(offset) else { + return false; + }; + if !offset + .checked_add(buf_len) + .is_some_and(|end| end <= bytes.len()) + { + return false; + } + // SAFETY: The runtime layer passes a buffer valid for buf_len + // bytes, and the range is checked to lie inside the region. + unsafe { + core::ptr::copy_nonoverlapping(bytes.as_ptr().add(offset), buf_ptr, buf_len); + } + true + }) + }) +} + +#[cfg(test)] +mod tests { + use super::with_process; + + #[test] + fn reads_come_from_the_regions() { + with_process(&[(0x1000, &[1, 2, 3, 4])], |process| { + assert_eq!(process.read::(0x1000_u64).unwrap(), 0x04030201); + assert_eq!(process.read::<[u8; 2]>(0x1002_u64).unwrap(), [3, 4]); + assert!(process.read::(0x0FFF_u64).is_err()); + assert!(process.read::(0x1001_u64).is_err()); + }); + } +} diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 21164a14..742dbec2 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -5,6 +5,8 @@ mod memory_range; mod process; mod sys; +#[cfg(all(test, not(target_family = "wasm")))] +pub(crate) mod mock; pub mod settings; pub mod timer; From 41cf503d37b2772e2ea7915cd24e3bfc57a85bf6 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 07:59:17 +0200 Subject: [PATCH 2/7] add pe debug id read --- src/file_format/pe.rs | 292 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 292 insertions(+) diff --git a/src/file_format/pe.rs b/src/file_format/pe.rs index 46f63cb1..b5bda8ca 100644 --- a/src/file_format/pe.rs +++ b/src/file_format/pe.rs @@ -95,6 +95,94 @@ struct OptionalCOFFHeader { // There's more but those vary depending on whether it's PE or PE+. } +// The magic at the head of the optional header decides between the PE32 and +// PE32+ layouts. +const OPTIONAL_HEADER_MAGIC_PE32: u16 = 0x10B; +const OPTIONAL_HEADER_MAGIC_PE32_PLUS: u16 = 0x20B; + +/// An entry of the data directory array at the end of the optional header, +/// naming where one of the image's tables lives. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct DataDirectory { + virtual_address: u32, + size: u32, +} + +/// The full PE32 optional header. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct OptionalHeader32 { + magic: u16, + major_linker_version: u8, + minor_linker_version: u8, + size_of_code: u32, + size_of_initialized_data: u32, + size_of_uninitialized_data: u32, + address_of_entry_point: u32, + base_of_code: u32, + base_of_data: u32, + image_base: u32, + section_alignment: u32, + file_alignment: u32, + major_operating_system_version: u16, + minor_operating_system_version: u16, + major_image_version: u16, + minor_image_version: u16, + major_subsystem_version: u16, + minor_subsystem_version: u16, + win32_version_value: u32, + size_of_image: u32, + size_of_headers: u32, + checksum: u32, + subsystem: u16, + dll_characteristics: u16, + size_of_stack_reserve: u32, + size_of_stack_commit: u32, + size_of_heap_reserve: u32, + size_of_heap_commit: u32, + loader_flags: u32, + number_of_rva_and_sizes: u32, + data_directories: [DataDirectory; 16], +} + +/// The full PE32+ optional header, which drops `base_of_data` and widens the +/// image base and the stack and heap sizes. +#[derive(Debug, Copy, Clone, Zeroable, Pod)] +#[repr(C)] +struct OptionalHeader64 { + magic: u16, + major_linker_version: u8, + minor_linker_version: u8, + size_of_code: u32, + size_of_initialized_data: u32, + size_of_uninitialized_data: u32, + address_of_entry_point: u32, + base_of_code: u32, + image_base: u64, + section_alignment: u32, + file_alignment: u32, + major_operating_system_version: u16, + minor_operating_system_version: u16, + major_image_version: u16, + minor_image_version: u16, + major_subsystem_version: u16, + minor_subsystem_version: u16, + win32_version_value: u32, + size_of_image: u32, + size_of_headers: u32, + checksum: u32, + subsystem: u16, + dll_characteristics: u16, + size_of_stack_reserve: u64, + size_of_stack_commit: u64, + size_of_heap_reserve: u64, + size_of_heap_commit: u64, + loader_flags: u32, + number_of_rva_and_sizes: u32, + data_directories: [DataDirectory; 16], +} + #[derive(Debug, Copy, Clone, Zeroable, Pod, Default)] #[repr(C)] struct ExportedSymbolsTableDef { @@ -484,3 +572,207 @@ impl FileVersion { .map(|val| val.file_version) } } + +/// The identity of the debug information of a PE module, as recorded in the +/// module's CodeView debug directory entry. Every build of a module gets a +/// fresh identity, so it names one exact binary: symbol servers key their +/// downloads on the GUID and age pair. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct DebugId { + /// The GUID of the debug information, in the byte order it is stored in. + pub guid: [u8; 16], + /// The number of times the debug information was written out. + pub age: u32, +} + +impl DebugId { + /// Reads the debug identity from the CodeView entry of the debug directory + /// of the PE module starting at the specified memory address. Returns + /// `None` if the module has no debug directory, no CodeView entry, or + /// debug information in a format older than PDB 7.0. + pub fn read(process: &Process, module_address: impl Into
) -> Option { + #[repr(C)] + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + struct DebugDirectoryEntry { + characteristics: u32, + time_date_stamp: u32, + major_version: u16, + minor_version: u16, + debug_type: u32, + size_of_data: u32, + address_of_raw_data: u32, + pointer_to_raw_data: u32, + } + + #[repr(C)] + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + struct CodeView70 { + signature: [u8; 4], + guid: [u8; 16], + age: u32, + } + + const IMAGE_DIRECTORY_ENTRY_DEBUG: usize = 6; + const IMAGE_DEBUG_TYPE_CODEVIEW: u32 = 2; + + let address: Address = module_address.into(); + + let (coff_header, coff_header_address) = read_coff_header(process, address)?; + + let optional_header_address = coff_header_address + mem::size_of::() as u64; + + let (optional_header_size, directory) = + match process.read::(optional_header_address).ok()? { + OPTIONAL_HEADER_MAGIC_PE32 => ( + mem::size_of::(), + process + .read::(optional_header_address) + .ok()? + .data_directories[IMAGE_DIRECTORY_ENTRY_DEBUG], + ), + OPTIONAL_HEADER_MAGIC_PE32_PLUS => ( + mem::size_of::(), + process + .read::(optional_header_address) + .ok()? + .data_directories[IMAGE_DIRECTORY_ENTRY_DEBUG], + ), + _ => return None, + }; + + if (coff_header.size_of_optional_header as usize) < optional_header_size { + return None; + } + + let directory = Some(directory) + .filter(|directory| directory.virtual_address != 0 && directory.size != 0)?; + + let entries = directory.size as usize / mem::size_of::(); + + // The walk is bounded so a corrupt entry count can't turn it into a scan. + (0..entries.min(0x10)).find_map(|i| { + let entry = process + .read::( + address + + directory.virtual_address + + (i * mem::size_of::()) as u64, + ) + .ok() + .filter(|entry| { + entry.debug_type == IMAGE_DEBUG_TYPE_CODEVIEW && entry.address_of_raw_data != 0 + })?; + + process + .read::(address + entry.address_of_raw_data) + .ok() + .filter(|codeview| codeview.signature == *b"RSDS") + .map(|codeview| Self { + guid: codeview.guid, + age: codeview.age, + }) + }) + } +} + +impl fmt::Debug for DebugId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // The first three fields of the GUID are stored little-endian and + // render big-endian in the canonical form. + let [a0, a1, a2, a3, b0, b1, c0, c1, d0, d1, d2, d3, d4, d5, d6, d7] = self.guid; + write!( + f, + "{:08x}-{:04x}-{:04x}-{d0:02x}{d1:02x}-{d2:02x}{d3:02x}{d4:02x}{d5:02x}{d6:02x}{d7:02x} (age {})", + u32::from_le_bytes([a0, a1, a2, a3]), + u16::from_le_bytes([b0, b1]), + u16::from_le_bytes([c0, c1]), + self.age, + ) + } +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::DebugId; + use crate::runtime::mock::with_process; + + use std::{vec, vec::Vec}; + + const BASE: u64 = 0x7FF6_1000_0000; + + // The 2019.4 mono runtime's GUID, stored as CodeView stores it: the first + // three fields little-endian, the rest in order. + const GUID: [u8; 16] = [ + 0xC7, 0xAA, 0x10, 0x77, 0x5A, 0x31, 0x30, 0x4D, 0xA7, 0x7A, 0x08, 0x07, 0x29, 0x69, 0x66, + 0xF6, + ]; + + fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); + } + + // Builds a minimal mapped PE image by hand from the spec, so the walk is + // checked against the format rather than against itself: headers at the + // base, a two-entry debug directory with the CodeView entry second, and + // the PDB 7.0 record it points at. + fn image(wide: bool) -> Vec { + let mut image = vec![0; 0x400]; + put(&mut image, 0x00, b"MZ"); + put(&mut image, 0x3C, &0x80_u32.to_le_bytes()); + put(&mut image, 0x80, b"PE\0\0"); + let size_of_optional_header: u16 = if wide { 0xF0 } else { 0xE0 }; + put(&mut image, 0x94, &size_of_optional_header.to_le_bytes()); + let magic: u16 = if wide { 0x20B } else { 0x10B }; + put(&mut image, 0x98, &magic.to_le_bytes()); + let debug_dd_at = 0x98 + if wide { 0xA0 } else { 0x90 }; + put(&mut image, debug_dd_at, &0x200_u32.to_le_bytes()); + put(&mut image, debug_dd_at + 4, &(2 * 28_u32).to_le_bytes()); + // Entry 0 is POGO data, entry 1 the CodeView record. + put(&mut image, 0x200 + 0xC, &13_u32.to_le_bytes()); + put(&mut image, 0x21C + 0xC, &2_u32.to_le_bytes()); + put(&mut image, 0x21C + 0x10, &0x30_u32.to_le_bytes()); + put(&mut image, 0x21C + 0x14, &0x300_u32.to_le_bytes()); + put(&mut image, 0x300, b"RSDS"); + put(&mut image, 0x304, &GUID); + put(&mut image, 0x314, &1_u32.to_le_bytes()); + put(&mut image, 0x318, b"mono-2.0-bdwgc.pdb\0"); + image + } + + #[test] + fn reads_the_debug_id_from_a_mapped_image() { + for wide in [true, false] { + with_process(&[(BASE, &image(wide))], |process| { + let debug_id = DebugId::read(process, BASE).unwrap(); + assert_eq!(debug_id.guid, GUID); + assert_eq!(debug_id.age, 1); + }); + } + } + + #[test] + fn renders_the_guid_canonically() { + let debug_id = DebugId { guid: GUID, age: 1 }; + assert_eq!( + std::format!("{debug_id:?}"), + "7710aac7-315a-4d30-a77a-0807296966f6 (age 1)", + ); + } + + #[test] + fn answers_nothing_without_a_debug_directory() { + let mut image = image(true); + put(&mut image, 0x98 + 0xA0, &[0; 8]); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } + + #[test] + fn answers_nothing_for_debug_information_older_than_pdb_70() { + let mut image = image(true); + put(&mut image, 0x300, b"NB10"); + with_process(&[(BASE, &image)], |process| { + assert!(DebugId::read(process, BASE).is_none()); + }); + } +} From cdd85fe97cd35048cd2cb794a7e1a6587689ae58 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 08:05:00 +0200 Subject: [PATCH 3/7] add elf build id read --- src/file_format/elf.rs | 209 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 208 insertions(+), 1 deletion(-) diff --git a/src/file_format/elf.rs b/src/file_format/elf.rs index 179bfede..917e9256 100644 --- a/src/file_format/elf.rs +++ b/src/file_format/elf.rs @@ -1074,7 +1074,7 @@ struct SymTab64 { /// /// By using this function, the user must be aware of the following limitations: /// - Only allocatable symbols and symbols used by the dynamic linker are exported -/// (.symtab is not loaded in memory at runtime) +/// (.symtab is not loaded in memory at runtime) /// - Only 64-bit ELFs are supported (an empty iterator will be returned for 32-bit ELFs) pub fn symbols( process: &Process, @@ -1157,3 +1157,210 @@ pub fn symbols( }) .fuse() } + +/// The GNU build ID of an ELF module, read from its `NT_GNU_BUILD_ID` note. +/// The linker derives it from the built binary, so it names one exact build. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct BuildId { + bytes: [u8; 32], + len: u8, +} + +impl BuildId { + /// The bytes of the build ID. + pub fn as_bytes(&self) -> &[u8] { + &self.bytes[..self.len as usize] + } +} + +impl fmt::Debug for BuildId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for byte in self.as_bytes() { + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +/// Reads the GNU build ID from the notes of the ELF module starting at the +/// given address. Returns [`None`] if the module carries no such note, which +/// not every module does. Only little-endian ELFs are supported. +pub fn build_id(process: &Process, module_address: Address) -> Option { + #[derive(Debug, Copy, Clone, Pod, Zeroable)] + #[repr(C)] + struct NoteHeader { + n_namesz: u32, + n_descsz: u32, + n_type: u32, + } + + const NT_GNU_BUILD_ID: u32 = 3; + + let header = process.read::
(module_address).ok()?; + let info = Info::parse(bytemuck::bytes_of(&header))?; + + if info.endian != Endian::Little { + return None; + } + + let (e_phoff, e_phentsize, e_phnum) = if info.bitness.is_64() { + let header = process.read::(module_address).ok()?; + (header.e_phoff, header.e_phentsize as u64, header.e_phnum) + } else { + let header = process.read::(module_address).ok()?; + ( + header.e_phoff as u64, + header.e_phentsize as u64, + header.e_phnum, + ) + }; + + (0..e_phnum).find_map(|index| { + let at = module_address + e_phoff + e_phentsize.wrapping_mul(index as u64); + + let (p_type, p_vaddr, p_filesz) = if info.bitness.is_64() { + let program_header = process.read::(at).ok()?; + ( + program_header.p_type, + program_header.p_vaddr, + program_header.p_filesz, + ) + } else { + let program_header = process.read::(at).ok()?; + ( + program_header.p_type, + program_header.p_vaddr as u64, + program_header.p_filesz as u64, + ) + }; + + if SegmentType(p_type) != SegmentType::PT_NOTE { + return None; + } + + // A note is its header, the name, then the data, the latter two padded + // to four bytes. + let segment = module_address + p_vaddr; + let mut offset = 0; + // The walk is bounded so corrupt sizes can't turn it into a scan. + for _ in 0..0x10 { + if offset + size_of::() as u64 > p_filesz { + return None; + } + + let note = process.read::(segment + offset).ok()?; + let name = offset + size_of::() as u64; + let desc = name + (note.n_namesz as u64).next_multiple_of(4); + + if note.n_type == NT_GNU_BUILD_ID + && note.n_namesz == 4 + && (1..=32).contains(¬e.n_descsz) + && desc + note.n_descsz as u64 <= p_filesz + && process.read::<[u8; 4]>(segment + name).ok()? == *b"GNU\0" + { + let mut bytes = [0; 32]; + process + .read_into_buf(segment + desc, &mut bytes[..note.n_descsz as usize]) + .ok()?; + return Some(BuildId { + bytes, + len: note.n_descsz as u8, + }); + } + + offset = desc + (note.n_descsz as u64).next_multiple_of(4); + } + + None + }) +} + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::build_id; + use crate::runtime::mock::with_process; + + use std::{format, vec, vec::Vec}; + + const BASE: u64 = 0x7F12_3450_0000; + + // The build ID of the 2019.1 mono runtime, a sha1 the linker computed. + const BUILD_ID: [u8; 20] = [ + 0xE6, 0xAA, 0x00, 0x0A, 0x9A, 0x52, 0x01, 0x63, 0x57, 0x43, 0xC6, 0xA1, 0xB2, 0x78, 0x1E, + 0x05, 0x7D, 0x65, 0x1D, 0xAD, + ]; + + fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); + } + + // Builds a minimal mapped ELF by hand from the spec, so the walk is + // checked against the format rather than against itself: the header, a + // load and a note program header, and a note segment holding an ABI tag + // note followed by the build ID note. + fn image(wide: bool) -> Vec { + let mut image = vec![0; 0x400]; + put(&mut image, 0x00, b"\x7fELF"); + image[0x04] = if wide { 2 } else { 1 }; + image[0x05] = 1; + image[0x06] = 1; + put(&mut image, 0x10, &3_u16.to_le_bytes()); + if wide { + put(&mut image, 0x20, &0x40_u64.to_le_bytes()); + put(&mut image, 0x36, &56_u16.to_le_bytes()); + put(&mut image, 0x38, &2_u16.to_le_bytes()); + put(&mut image, 0x40, &1_u32.to_le_bytes()); + put(&mut image, 0x78, &4_u32.to_le_bytes()); + put(&mut image, 0x88, &0x200_u64.to_le_bytes()); + put(&mut image, 0x98, &0x44_u64.to_le_bytes()); + } else { + put(&mut image, 0x1C, &0x34_u32.to_le_bytes()); + put(&mut image, 0x2A, &32_u16.to_le_bytes()); + put(&mut image, 0x2C, &2_u16.to_le_bytes()); + put(&mut image, 0x34, &1_u32.to_le_bytes()); + put(&mut image, 0x54, &4_u32.to_le_bytes()); + put(&mut image, 0x5C, &0x200_u32.to_le_bytes()); + put(&mut image, 0x64, &0x44_u32.to_le_bytes()); + } + put(&mut image, 0x200, &4_u32.to_le_bytes()); + put(&mut image, 0x204, &16_u32.to_le_bytes()); + put(&mut image, 0x208, &1_u32.to_le_bytes()); + put(&mut image, 0x20C, b"GNU\0"); + put(&mut image, 0x220, &4_u32.to_le_bytes()); + put(&mut image, 0x224, &20_u32.to_le_bytes()); + put(&mut image, 0x228, &3_u32.to_le_bytes()); + put(&mut image, 0x22C, b"GNU\0"); + put(&mut image, 0x230, &BUILD_ID); + image + } + + #[test] + fn reads_the_build_id_from_a_mapped_image() { + for wide in [true, false] { + with_process(&[(BASE, &image(wide))], |process| { + let build_id = build_id(process, BASE.into()).unwrap(); + assert_eq!(build_id.as_bytes(), BUILD_ID); + }); + } + } + + #[test] + fn renders_the_id_as_hex() { + with_process(&[(BASE, &image(true))], |process| { + let build_id = build_id(process, BASE.into()).unwrap(); + assert_eq!( + format!("{build_id:?}"), + "e6aa000a9a5201635743c6a1b2781e057d651dad", + ); + }); + } + + #[test] + fn answers_nothing_without_a_note_segment() { + let mut image = image(true); + put(&mut image, 0x78, &0_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(build_id(process, BASE.into()).is_none()); + }); + } +} From 7fedd82e2238c3c6c9363a2782262bffb802ba00 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 08:06:05 +0200 Subject: [PATCH 4/7] add mach-o uuid read --- src/file_format/macho.rs | 148 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 5 deletions(-) diff --git a/src/file_format/macho.rs b/src/file_format/macho.rs index 147a24c6..c19abad2 100644 --- a/src/file_format/macho.rs +++ b/src/file_format/macho.rs @@ -1,11 +1,15 @@ //! Support for parsing Mach-O format +use core::{fmt, mem}; + #[cfg(feature = "alloc")] use core::iter::FusedIterator; #[cfg(feature = "alloc")] use alloc::collections::BTreeMap; +use bytemuck::{Pod, Zeroable}; + #[cfg(feature = "alloc")] use crate::{string::ArrayCString, Error}; use crate::{Address, PointerSize, Process}; @@ -36,11 +40,8 @@ fn scan_macho_page(process: &Process, range: (Address, u64)) -> Option
let first_page = addr + distance_to_page; for i in 0..((len - distance_to_page) / PAGE_SIZE) { let a = first_page + (i * PAGE_SIZE); - match process.read::(a) { - Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) => { - return Some(a); - } - _ => (), + if let Ok(MH_MAGIC_64 | MH_CIGAM_64 | MH_MAGIC_32 | MH_CIGAM_32) = process.read::(a) { + return Some(a); } } None @@ -71,6 +72,8 @@ fn scan_macho_pages( // Constants for the cmd field of load commands, the type // https://opensource.apple.com/source/xnu/xnu-4570.71.2/EXTERNAL_HEADERS/mach-o/loader.h.auto.html +/// the uuid +const LC_UUID: u32 = 0x1b; /// link-edit stab symbol table info #[cfg(feature = "alloc")] const LC_SYMTAB: u32 = 0x2; @@ -78,6 +81,77 @@ const LC_SYMTAB: u32 = 0x2; #[cfg(feature = "alloc")] const LC_SEGMENT_64: u32 = 0x19; +/// The UUID of a Mach-O module, from its `LC_UUID` load command. The linker +/// derives it from the built binary, so it names one exact build. +#[derive(Copy, Clone, PartialEq, Eq, Hash)] +pub struct Uuid { + /// The bytes of the UUID. + pub bytes: [u8; 16], +} + +impl fmt::Debug for Uuid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, byte) in self.bytes.iter().enumerate() { + if let 4 | 6 | 8 | 10 = i { + f.write_str("-")?; + } + write!(f, "{byte:02x}")?; + } + Ok(()) + } +} + +/// Reads the UUID from the load commands of the Mach-O module in the given +/// range. Returns [`None`] if the module carries no `LC_UUID` command. +pub fn uuid(process: &Process, range: (Address, u64)) -> Option { + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + #[repr(C)] + struct MachHeader { + magic: u32, + cputype: u32, + cpusubtype: u32, + filetype: u32, + ncmds: u32, + sizeofcmds: u32, + flags: u32, + } + + #[derive(Debug, Copy, Clone, Zeroable, Pod)] + #[repr(C)] + struct LoadCommand { + cmd: u32, + cmdsize: u32, + } + + let page = scan_macho_page(process, range)?; + let header = process.read::(page).ok()?; + + // The 64-bit header ends with one reserved field the 32-bit one lacks. + let commands = page + + match header.magic { + MH_MAGIC_64 => mem::size_of::() + mem::size_of::(), + MH_MAGIC_32 => mem::size_of::(), + _ => return None, + } as u64; + + let mut offset = 0; + // The walk is bounded so a corrupt command count can't turn it into a scan. + for _ in 0..header.ncmds.min(0x40) { + let command = process.read::(commands + offset).ok()?; + + if command.cmd == LC_UUID { + return process + .read::<[u8; 16]>(commands + offset + mem::size_of::() as u64) + .ok() + .map(|bytes| Uuid { bytes }); + } + + offset += command.cmdsize as u64; + } + + None +} + #[cfg(feature = "alloc")] struct MachOFormatOffsets { number_of_commands: u32, @@ -205,3 +279,67 @@ fn fileoff_to_vmaddr(map: &BTreeMap, fileoff: u64) -> u64 { .map(|(&k, &v)| v + fileoff - k) .unwrap_or(fileoff) } + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::uuid; + use crate::runtime::mock::with_process; + + use std::{format, vec, vec::Vec}; + + const BASE: u64 = 0x1_0000_0000; + + // The UUID of the 2019.4 mono runtime shipped with a real mac player. + const UUID: [u8; 16] = [ + 0xE7, 0x42, 0x0B, 0xC7, 0xA2, 0x6B, 0x33, 0xFA, 0xB5, 0xCD, 0x41, 0xCA, 0xD7, 0xD4, 0x61, + 0x4C, + ]; + + fn put(image: &mut [u8], at: usize, bytes: &[u8]) { + image[at..at + bytes.len()].copy_from_slice(bytes); + } + + // Builds a minimal mapped Mach-O by hand from the loader header, so the + // walk is checked against the format rather than against itself: the + // header, a segment command, and the uuid command. + fn image(wide: bool) -> Vec { + let mut image = vec![0; 0x1000]; + let magic: u32 = if wide { 0xFEEDFACF } else { 0xFEEDFACE }; + put(&mut image, 0x00, &magic.to_le_bytes()); + put(&mut image, 0x10, &2_u32.to_le_bytes()); + let commands = if wide { 0x20 } else { 0x1C }; + put(&mut image, commands, &0x19_u32.to_le_bytes()); + put(&mut image, commands + 0x4, &0x48_u32.to_le_bytes()); + put(&mut image, commands + 0x48, &0x1B_u32.to_le_bytes()); + put(&mut image, commands + 0x4C, &24_u32.to_le_bytes()); + put(&mut image, commands + 0x50, &UUID); + image + } + + #[test] + fn reads_the_uuid_from_a_mapped_image() { + for wide in [true, false] { + with_process(&[(BASE, &image(wide))], |process| { + let uuid = uuid(process, (BASE.into(), 0x1000)).unwrap(); + assert_eq!(uuid.bytes, UUID); + }); + } + } + + #[test] + fn renders_the_uuid_canonically() { + with_process(&[(BASE, &image(true))], |process| { + let uuid = uuid(process, (BASE.into(), 0x1000)).unwrap(); + assert_eq!(format!("{uuid:?}"), "e7420bc7-a26b-33fa-b5cd-41cad7d4614c"); + }); + } + + #[test] + fn answers_nothing_without_a_uuid_command() { + let mut image = image(true); + put(&mut image, 0x20 + 0x48, &0_u32.to_le_bytes()); + with_process(&[(BASE, &image)], |process| { + assert!(uuid(process, (BASE.into(), 0x1000)).is_none()); + }); + } +} From 382058000d18153130e47a387091d9c06e3ab07e Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 08:58:39 +0200 Subject: [PATCH 5/7] cut unread class image offset --- src/game_engine/unity/mono/offsets.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/game_engine/unity/mono/offsets.rs b/src/game_engine/unity/mono/offsets.rs index dbfb5e58..70881912 100644 --- a/src/game_engine/unity/mono/offsets.rs +++ b/src/game_engine/unity/mono/offsets.rs @@ -29,7 +29,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x30, - image: 0x40, name: 0x48, namespace: 0x50, vtable_size: 0x5C, @@ -57,7 +56,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x20, - image: 0x28, name: 0x2C, namespace: 0x30, vtable_size: 0x38, @@ -85,7 +83,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x30, - image: 0x40, name: 0x48, namespace: 0x50, vtable_size: 0x5C, @@ -113,7 +110,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x20, - image: 0x28, name: 0x2C, namespace: 0x30, vtable_size: 0x38, @@ -141,7 +137,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x30, - image: 0x48, name: 0x50, namespace: 0x58, vtable_size: 0x18, @@ -169,7 +164,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x24, - image: 0x30, name: 0x34, namespace: 0x38, vtable_size: 0xC, @@ -197,7 +191,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x30, - image: 0x40, name: 0x48, namespace: 0x50, vtable_size: 0x18, @@ -225,7 +218,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x24, - image: 0x2C, name: 0x30, namespace: 0x34, vtable_size: 0xC, @@ -253,7 +245,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x28, - image: 0x38, name: 0x40, namespace: 0x48, vtable_size: 0x54, @@ -281,7 +272,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x28, - image: 0x38, name: 0x40, namespace: 0x48, vtable_size: 0x54, @@ -309,7 +299,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x28, - image: 0x40, name: 0x48, namespace: 0x50, vtable_size: 0x18, @@ -337,7 +326,6 @@ impl MonoOffsets { }, class: ClassOffsets { parent: 0x28, - image: 0x38, name: 0x40, namespace: 0x48, vtable_size: 0x18, @@ -374,8 +362,6 @@ pub(super) struct HashTableOffsets { pub(super) struct ClassOffsets { pub(super) parent: u8, - #[allow(unused)] - pub(super) image: u8, // Unused for now, kept in the struct for future use pub(super) name: u8, pub(super) namespace: u8, pub(super) vtable_size: u8, // On mono V1 and V1_cattrs, this offset represents MonoVTable.data From 96814bdcbf112e30dcd97cd6fe5972ff2f7f4142 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 09:07:58 +0200 Subject: [PATCH 6/7] add image route for assembly names --- src/game_engine/unity/mono/assembly.rs | 16 +- src/game_engine/unity/mono/offsets.rs | 297 ++++++++++++++----------- 2 files changed, 184 insertions(+), 129 deletions(-) diff --git a/src/game_engine/unity/mono/assembly.rs b/src/game_engine/unity/mono/assembly.rs index fe17e628..12b6f129 100644 --- a/src/game_engine/unity/mono/assembly.rs +++ b/src/game_engine/unity/mono/assembly.rs @@ -12,11 +12,19 @@ impl Assembly { process: &Process, module: &Module, ) -> Result, Error> { + let name = match ( + module.offsets.image.assembly_name, + module.offsets.assembly.aname, + ) { + (Some(assembly_name), _) => { + self.get_image(process, module).ok_or(Error {})?.image + assembly_name + } + (_, Some(aname)) => self.assembly + aname, + _ => return Err(Error {}), + }; + process - .read_pointer( - self.assembly + module.offsets.assembly.aname, - module.pointer_size, - ) + .read_pointer(name, module.pointer_size) .and_then(|addr| process.read(addr)) } diff --git a/src/game_engine/unity/mono/offsets.rs b/src/game_engine/unity/mono/offsets.rs index 70881912..f8210a05 100644 --- a/src/game_engine/unity/mono/offsets.rs +++ b/src/game_engine/unity/mono/offsets.rs @@ -19,10 +19,13 @@ impl MonoOffsets { match (format, version, pointer_size) { (BinaryFormat::PE, Version::V3, PointerSize::Bit64) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x10, + aname: Some(0x10), image: 0x60, }, - image: ImageOffsets { class_cache: 0x4D0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x4D0, + }, hash_table: HashTableOffsets { size: 0x18, table: 0x20, @@ -46,10 +49,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V3, PointerSize::Bit32) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x8, + aname: Some(0x8), image: 0x48, }, - image: ImageOffsets { class_cache: 0x35C }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x35C, + }, hash_table: HashTableOffsets { size: 0x0C, table: 0x14, @@ -73,10 +79,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V2, PointerSize::Bit64) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x10, + aname: Some(0x10), image: 0x60, }, - image: ImageOffsets { class_cache: 0x4C0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x4C0, + }, hash_table: HashTableOffsets { size: 0x18, table: 0x20, @@ -100,10 +109,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V2, PointerSize::Bit32) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x8, + aname: Some(0x8), image: 0x44, }, - image: ImageOffsets { class_cache: 0x354 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x354, + }, hash_table: HashTableOffsets { size: 0x0C, table: 0x14, @@ -127,10 +139,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V1Cattrs, PointerSize::Bit64) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x10, + aname: Some(0x10), image: 0x58, }, - image: ImageOffsets { class_cache: 0x3D0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, hash_table: HashTableOffsets { size: 0x18, table: 0x20, @@ -154,10 +169,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V1Cattrs, PointerSize::Bit32) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x8, + aname: Some(0x8), image: 0x40, }, - image: ImageOffsets { class_cache: 0x2A0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x2A0, + }, hash_table: HashTableOffsets { size: 0xC, table: 0x14, @@ -181,10 +199,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V1, PointerSize::Bit64) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x10, + aname: Some(0x10), image: 0x58, }, - image: ImageOffsets { class_cache: 0x3D0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, hash_table: HashTableOffsets { size: 0x18, table: 0x20, @@ -208,10 +229,13 @@ impl MonoOffsets { }), (BinaryFormat::PE, Version::V1, PointerSize::Bit32) => Some(&Self { assembly: AssemblyOffsets { - aname: 0x8, + aname: Some(0x8), image: 0x40, }, - image: ImageOffsets { class_cache: 0x2A0 }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x2A0, + }, hash_table: HashTableOffsets { size: 0xC, table: 0x14, @@ -233,125 +257,148 @@ impl MonoOffsets { }, v_table: MonoVTableOffsets { vtable: 0x28 }, }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V3, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x60, - }, - image: ImageOffsets { class_cache: 0x4D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - name: 0x40, - namespace: 0x48, - vtable_size: 0x54, - fields: 0x90, - runtime_info: 0xC8, - field_count: 0xF8, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V2, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x60, - }, - image: ImageOffsets { class_cache: 0x4C0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - name: 0x40, - namespace: 0x48, - vtable_size: 0x54, - fields: 0x90, - runtime_info: 0xC8, - field_count: 0xF8, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x40 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1Cattrs, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x58, - }, - image: ImageOffsets { class_cache: 0x3D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - name: 0x48, - namespace: 0x50, - vtable_size: 0x18, - fields: 0xA8, - runtime_info: 0xF8, - field_count: 0x94, - next_class_cache: 0x100, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), - (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1, PointerSize::Bit64) => Some(&Self { - assembly: AssemblyOffsets { - aname: 0x10, - image: 0x58, - }, - image: ImageOffsets { class_cache: 0x3D0 }, - hash_table: HashTableOffsets { - size: 0x18, - table: 0x20, - }, - class: ClassOffsets { - parent: 0x28, - name: 0x40, - namespace: 0x48, - vtable_size: 0x18, - fields: 0xA0, - runtime_info: 0xF0, - field_count: 0x8C, - next_class_cache: 0xF8, - }, - field: FieldInfoOffsets { - name: 0x8, - offset: 0x18, - alignment: 0x20, - }, - v_table: MonoVTableOffsets { vtable: 0x48 }, - }), + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V3, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x60, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x4D0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + name: 0x40, + namespace: 0x48, + vtable_size: 0x54, + fields: 0x90, + runtime_info: 0xC8, + field_count: 0xF8, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V2, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x60, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x4C0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + name: 0x40, + namespace: 0x48, + vtable_size: 0x54, + fields: 0x90, + runtime_info: 0xC8, + field_count: 0xF8, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1Cattrs, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x58, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + name: 0x48, + namespace: 0x50, + vtable_size: 0x18, + fields: 0xA8, + runtime_info: 0xF8, + field_count: 0x94, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } + (BinaryFormat::ELF | BinaryFormat::MachO, Version::V1, PointerSize::Bit64) => { + Some(&Self { + assembly: AssemblyOffsets { + aname: Some(0x10), + image: 0x58, + }, + image: ImageOffsets { + assembly_name: None, + class_cache: 0x3D0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x28, + name: 0x40, + namespace: 0x48, + vtable_size: 0x18, + fields: 0xA0, + runtime_info: 0xF0, + field_count: 0x8C, + next_class_cache: 0xF8, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }) + } _ => None, } } } pub(super) struct AssemblyOffsets { - pub(super) aname: u8, + pub(super) aname: Option, // Either this or ImageOffsets::assembly_name locates the name + pub(super) image: u8, } pub(super) struct ImageOffsets { + pub(super) assembly_name: Option, // Either this or AssemblyOffsets::aname locates the name + pub(super) class_cache: u16, } From b8f7a581f449780dff3783d366d46b4fc518dd65 Mon Sep 17 00:00:00 2001 From: ero-qt Date: Fri, 28 Aug 2026 09:13:29 +0200 Subject: [PATCH 7/7] add known mono builds --- src/game_engine/unity/mono/builds.rs | 1109 ++++++++++++++++++++++++++ src/game_engine/unity/mono/mod.rs | 95 ++- 2 files changed, 1189 insertions(+), 15 deletions(-) create mode 100644 src/game_engine/unity/mono/builds.rs diff --git a/src/game_engine/unity/mono/builds.rs b/src/game_engine/unity/mono/builds.rs new file mode 100644 index 00000000..a3d7101b --- /dev/null +++ b/src/game_engine/unity/mono/builds.rs @@ -0,0 +1,1109 @@ +//! Known mono builds: exact runtime binaries, named by the identity of their +//! debug information, paired with the offsets measured from their symbols. + +use super::offsets::{ + AssemblyOffsets, ClassOffsets, FieldInfoOffsets, HashTableOffsets, ImageOffsets, MonoOffsets, + MonoVTableOffsets, +}; +use super::Version; +use crate::{file_format::pe::DebugId, PointerSize}; + +/// One exact mono runtime binary and the offsets measured from it. +pub(super) struct Build { + pub(super) guid: [u8; 16], + pub(super) pointer_size: PointerSize, + pub(super) version: Version, + pub(super) offsets: MonoOffsets, +} + +/// Looks the module's exact build up by the GUID of its debug information. +pub(super) fn find(debug_id: &DebugId) -> Option<&'static Build> { + BUILDS + .binary_search_by(|build| build.guid.cmp(&debug_id.guid)) + .ok() + .map(|index| &BUILDS[index]) +} + +/// Parses a canonical GUID into the byte order the debug directory stores it +/// in: the first three fields are little-endian. +const fn guid(canonical: &str) -> [u8; 16] { + const fn hex(byte: u8) -> u8 { + match byte { + b'0'..=b'9' => byte - b'0', + b'a'..=b'f' => byte - b'a' + 10, + _ => panic!("The GUID is not lowercase hex."), + } + } + + let canonical = canonical.as_bytes(); + assert!( + canonical.len() == 36 + && canonical[8] == b'-' + && canonical[13] == b'-' + && canonical[18] == b'-' + && canonical[23] == b'-', + "The GUID is not in its canonical form.", + ); + + let mut parsed = [0; 16]; + let mut index = 0; + let mut at = 0; + while index < 16 { + if canonical[at] == b'-' { + at += 1; + continue; + } + parsed[index] = (hex(canonical[at]) << 4) | hex(canonical[at + 1]); + index += 1; + at += 2; + } + + [ + parsed[3], parsed[2], parsed[1], parsed[0], parsed[5], parsed[4], parsed[7], parsed[6], + parsed[8], parsed[9], parsed[10], parsed[11], parsed[12], parsed[13], parsed[14], + parsed[15], + ] +} + +// The table is sorted by guid. For mono.dll builds the statics path reads +// MonoVTable.data through vtable_size and never reads v_table.vtable, so those +// builds leave it 0. +static BUILDS: &[Build] = &[ + // Unity 2017.4.40f1, mono-2.0-bdwgc.dll (net_4_6), x86. + // No x86 PDB exists for this binary, so these are the x64 layouts reread at 32 bit rules. + // Written by hand: derive-mono answers nothing without symbols. + Build { + guid: guid("54fe0c31-c851-4749-baa5-7699d1279165"), + pointer_size: PointerSize::Bit32, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x44, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x354, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x84, + field_count: 0xa4, + next_class_cache: 0xa8, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x28 }, + }, + }, + // Unity 6000.5.8, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("eb6b6239-5624-487c-a84e-d7f0a7335670"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, + // Unity 2017.4.40, mono-2.0-bdwgc.dll (net_4_6), x64. + // Layouts read from mono-2.0-bdwgc.pdb, this binary's own symbols being held nowhere. + Build { + guid: guid("2f7a3442-3c29-424d-8a46-8cc59237ed89"), + pointer_size: PointerSize::Bit64, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x4c0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }, + }, + // Unity 2021.3.11, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("1d994642-9a41-4a6a-84be-f55f9cff8f57"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 2018.4.36, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("f469c84e-5b81-4c42-8c3f-72ad629f99cb"), + pointer_size: PointerSize::Bit64, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x4c0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }, + }, + // Unity 2018.4.36, mono.dll, x64. + Build { + guid: guid("487fa150-59b5-4a18-8fed-964001db1b82"), + pointer_size: PointerSize::Bit64, + version: Version::V1Cattrs, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x58, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x3d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x50, + namespace: 0x58, + vtable_size: 0x18, + fields: 0xb0, + runtime_info: 0x100, + field_count: 0x9c, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 6000.5.8, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("4f356e63-5da8-496c-8bb8-aaf2a0b1f364"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 6000.2.12, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("018d6f65-a658-4607-93eb-2518f5018226"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 6000.7.0, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("49c1826a-d1b9-442e-8388-4509b7c91395"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 6000.3.21, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("1ac99f6b-fd3a-4dc0-93e7-782ca1b4be7d"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 2018.4.36, mono.dll, x86. + Build { + guid: guid("c3c97c70-f490-4462-a27d-b4103d2aca1f"), + pointer_size: PointerSize::Bit32, + version: Version::V1Cattrs, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x40, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x2a0, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x24, + name: 0x34, + namespace: 0x38, + vtable_size: 0xc, + fields: 0x78, + runtime_info: 0xa8, + field_count: 0x68, + next_class_cache: 0xac, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 5.6.7, mono.dll, x64. + // Layouts read from mono.pdb, this binary's own symbols being held nowhere. + Build { + guid: guid("924a8172-8d25-496f-b684-20c9f04d4f92"), + pointer_size: PointerSize::Bit64, + version: Version::V1, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x58, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x3d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x18, + fields: 0xa8, + runtime_info: 0xf8, + field_count: 0x94, + next_class_cache: 0x100, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 2020.1.18, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("984e5687-3dd9-4d72-8e88-552c6810430d"), + pointer_size: PointerSize::Bit32, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x44, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x354, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x84, + field_count: 0xa4, + next_class_cache: 0xa8, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x28 }, + }, + }, + // Unity 2020.1.18, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("0b5f7f89-7937-4300-9c3b-a1ec2c75e06e"), + pointer_size: PointerSize::Bit64, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x4c0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }, + }, + // Unity 2017.4.40, mono.dll, x64. + Build { + guid: guid("c1c35e9c-fd72-4ebf-af5e-e7c932e2865d"), + pointer_size: PointerSize::Bit64, + version: Version::V1Cattrs, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x58, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x3d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x50, + namespace: 0x58, + vtable_size: 0x18, + fields: 0xb0, + runtime_info: 0x100, + field_count: 0x9c, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 6000.3.21, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("44e461a2-1832-413d-afb1-3fe613634de3"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, + // Unity 2017.4.40, mono.dll, x86. + Build { + guid: guid("d45555b8-4783-4fba-9eeb-f830cb655d89"), + pointer_size: PointerSize::Bit32, + version: Version::V1Cattrs, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x40, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x2a0, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x24, + name: 0x34, + namespace: 0x38, + vtable_size: 0xc, + fields: 0x78, + runtime_info: 0xa8, + field_count: 0x68, + next_class_cache: 0xac, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 6000.7.0, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("8e2fbcbc-d64d-4993-a733-a489d7a90b2b"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, + // Unity 2023.1.22, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("4aac62be-dfea-4610-91fc-8a1b6c768935"), + pointer_size: PointerSize::Bit64, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x30), + class_cache: 0x4d0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x48 }, + }, + }, + // Unity 2019.4.41, mono-2.0-bdwgc.dll, x64. + Build { + guid: guid("7710aac7-315a-4d30-a77a-0807296966f6"), + pointer_size: PointerSize::Bit64, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x60, + }, + image: ImageOffsets { + assembly_name: Some(0x28), + class_cache: 0x4c0, + }, + hash_table: HashTableOffsets { + size: 0x18, + table: 0x20, + }, + class: ClassOffsets { + parent: 0x30, + name: 0x48, + namespace: 0x50, + vtable_size: 0x5c, + fields: 0x98, + runtime_info: 0xd0, + field_count: 0x100, + next_class_cache: 0x108, + }, + field: FieldInfoOffsets { + name: 0x8, + offset: 0x18, + alignment: 0x20, + }, + v_table: MonoVTableOffsets { vtable: 0x40 }, + }, + }, + // Unity 2019.4.41, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("998210ce-aee9-4d0b-a225-9c529815fc78"), + pointer_size: PointerSize::Bit32, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x44, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x354, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x84, + field_count: 0xa4, + next_class_cache: 0xa8, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x28 }, + }, + }, + // Unity 5.6.7, mono.dll, x86. + // No x86 PDB exists for this binary, so these are the x64 layouts reread at 32 bit rules. + // Written by hand: derive-mono answers nothing without symbols. + Build { + guid: guid("064ccfd8-ab0c-4a5b-b33d-7a59b8eafbab"), + pointer_size: PointerSize::Bit32, + version: Version::V1, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x40, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x2a0, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x24, + name: 0x30, + namespace: 0x34, + vtable_size: 0xc, + fields: 0x74, + runtime_info: 0xa4, + field_count: 0x64, + next_class_cache: 0xa8, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x0 }, + }, + }, + // Unity 2018.4.36, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("7059c7da-c870-4870-951d-758ba588a378"), + pointer_size: PointerSize::Bit32, + version: Version::V2, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x44, + }, + image: ImageOffsets { + assembly_name: Some(0x18), + class_cache: 0x354, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x84, + field_count: 0xa4, + next_class_cache: 0xa8, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x28 }, + }, + }, + // Unity 2021.3.11, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("51a376db-5854-4c34-925f-acb714c49e65"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, + // Unity 6000.2.12, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("9fd463e5-f21d-49da-8e5d-67d03349843a"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, + // Unity 2023.1.22, mono-2.0-bdwgc.dll, x86. + Build { + guid: guid("347d7ee9-ca67-435d-be75-237735403a3d"), + pointer_size: PointerSize::Bit32, + version: Version::V3, + offsets: MonoOffsets { + assembly: AssemblyOffsets { + aname: None, + image: 0x48, + }, + image: ImageOffsets { + assembly_name: Some(0x1c), + class_cache: 0x35c, + }, + hash_table: HashTableOffsets { + size: 0xc, + table: 0x14, + }, + class: ClassOffsets { + parent: 0x20, + name: 0x2c, + namespace: 0x30, + vtable_size: 0x38, + fields: 0x60, + runtime_info: 0x7c, + field_count: 0x9c, + next_class_cache: 0xa0, + }, + field: FieldInfoOffsets { + name: 0x4, + offset: 0xc, + alignment: 0x10, + }, + v_table: MonoVTableOffsets { vtable: 0x2c }, + }, + }, +]; + +#[cfg(all(test, not(target_family = "wasm")))] +mod tests { + use super::super::{BinaryFormat, Version}; + use super::{find, guid, MonoOffsets, BUILDS}; + use crate::file_format::pe::DebugId; + use crate::PointerSize; + + // The 2019.4 mono runtime's GUID as the debug directory stores it, the + // same anchor the pe tests read out of a mapped image. + const STORED: [u8; 16] = [ + 0xC7, 0xAA, 0x10, 0x77, 0x5A, 0x31, 0x30, 0x4D, 0xA7, 0x7A, 0x08, 0x07, 0x29, 0x69, 0x66, + 0xF6, + ]; + + #[test] + fn parses_canonical_guids_into_storage_order() { + assert_eq!(guid("7710aac7-315a-4d30-a77a-0807296966f6"), STORED); + } + + #[test] + fn table_is_sorted_and_unique() { + assert!(BUILDS.windows(2).all(|pair| pair[0].guid < pair[1].guid)); + } + + #[test] + fn finds_known_builds() { + let build = find(&DebugId { + guid: STORED, + age: 1, + }) + .unwrap(); + assert_eq!(build.pointer_size, PointerSize::Bit64); + assert!(matches!(build.version, Version::V2)); + + assert!(find(&DebugId { + guid: [0; 16], + age: 1, + }) + .is_none()); + } + + // The 2019.4 build lays out like the table its version selects, so the + // measured entry must agree with the shipped one on every member both + // carry. + #[test] + fn the_2019_4_build_matches_its_version_table() { + let build = find(&DebugId { + guid: STORED, + age: 1, + }) + .unwrap(); + let table = MonoOffsets::new(Version::V2, PointerSize::Bit64, BinaryFormat::PE).unwrap(); + + assert_eq!(build.offsets.assembly.image, table.assembly.image); + assert_eq!(build.offsets.image.class_cache, table.image.class_cache); + assert_eq!(build.offsets.hash_table.size, table.hash_table.size); + assert_eq!(build.offsets.hash_table.table, table.hash_table.table); + assert_eq!(build.offsets.class.parent, table.class.parent); + assert_eq!(build.offsets.class.name, table.class.name); + assert_eq!(build.offsets.class.namespace, table.class.namespace); + assert_eq!(build.offsets.class.vtable_size, table.class.vtable_size); + assert_eq!(build.offsets.class.fields, table.class.fields); + assert_eq!(build.offsets.class.runtime_info, table.class.runtime_info); + assert_eq!(build.offsets.class.field_count, table.class.field_count); + assert_eq!( + build.offsets.class.next_class_cache, + table.class.next_class_cache, + ); + assert_eq!(build.offsets.field.name, table.field.name); + assert_eq!(build.offsets.field.offset, table.field.offset); + assert_eq!(build.offsets.field.alignment, table.field.alignment); + assert_eq!(build.offsets.v_table.vtable, table.v_table.vtable); + } + + // The shipped table for 2021.2 and later x64 puts the vtable at 0x40; + // every measured build of that stretch puts it at 0x48. The entries keep + // what was measured. + #[test] + fn modern_x64_builds_diverge_from_their_version_table_on_the_vtable() { + let diverging = BUILDS + .iter() + .filter(|build| { + matches!(build.version, Version::V3) && build.pointer_size == PointerSize::Bit64 + }) + .count(); + assert!(diverging > 0); + assert!(BUILDS + .iter() + .filter(|build| { + matches!(build.version, Version::V3) && build.pointer_size == PointerSize::Bit64 + }) + .all(|build| build.offsets.v_table.vtable == 0x48)); + } +} diff --git a/src/game_engine/unity/mono/mod.rs b/src/game_engine/unity/mono/mod.rs index 56b44f45..715b0ade 100644 --- a/src/game_engine/unity/mono/mod.rs +++ b/src/game_engine/unity/mono/mod.rs @@ -6,12 +6,14 @@ use crate::file_format::macho; use crate::{ file_format::{elf, pe}, future::retry, + print_limited, signature::Signature, Address, Address32, Address64, PointerSize, Process, }; use core::iter::{self, FusedIterator}; mod assembly; +mod builds; use assembly::Assembly; mod image; pub use image::Image; @@ -38,12 +40,47 @@ pub struct Module { impl Module { /// Tries attaching to a Unity game that is using the standard Mono backend. - /// This function automatically detects the [Mono version](Version). If you - /// know the version in advance or it fails detecting it, use - /// [`attach`](Self::attach) instead. + /// If the mono runtime is a known build, its measured offsets are used + /// directly. Otherwise this function automatically detects the + /// [Mono version](Version). If you know the version in advance or it fails + /// detecting it, use [`attach`](Self::attach) instead. pub fn attach_auto_detect(process: &Process) -> Option { + let (module_range, format) = Self::find_runtime_module(process)?; + let pointer_size = Self::pointer_size(process, module_range, format)?; + + let debug_id = match format { + BinaryFormat::PE => pe::DebugId::read(process, module_range.0), + _ => None, + }; + + if let Some(debug_id) = &debug_id { + if let Some(build) = + builds::find(debug_id).filter(|build| build.pointer_size == pointer_size) + { + if let Some(module) = Self::attach_with( + process, + module_range, + format, + pointer_size, + build.version, + &build.offsets, + ) { + print_limited::<128>(&format_args!("known mono build: {debug_id:?}")); + return Some(module); + } + } + } + let version = Version::detect(process)?; - Self::attach(process, version) + let module = Self::attach(process, version)?; + + if let Some(debug_id) = debug_id { + if builds::find(&debug_id).is_none() { + print_limited::<128>(&format_args!("unknown mono build: {debug_id:?}")); + } + } + + Some(module) } /// Tries attaching to a Unity game that is using the standard Mono backend @@ -51,7 +88,22 @@ impl Module { /// correct for this function to work. If you don't know the version in /// advance, use [`attach_auto_detect`](Self::attach_auto_detect) instead. pub fn attach(process: &Process, version: Version) -> Option { - let (module_range, format) = [ + let (module_range, format) = Self::find_runtime_module(process)?; + let pointer_size = Self::pointer_size(process, module_range, format)?; + let offsets = MonoOffsets::new(version, pointer_size, format)?; + + Self::attach_with( + process, + module_range, + format, + pointer_size, + version, + offsets, + ) + } + + fn find_runtime_module(process: &Process) -> Option<((Address, u64), BinaryFormat)> { + [ ("mono.dll", BinaryFormat::PE), ("libmono.so", BinaryFormat::ELF), #[cfg(feature = "alloc")] @@ -62,20 +114,33 @@ impl Module { ("libmonobdwgc-2.0.dylib", BinaryFormat::MachO), ] .into_iter() - .find_map(|(name, format)| Some((process.get_module_range(name).ok()?, format)))?; - - let (mono_module, _) = module_range; + .find_map(|(name, format)| Some((process.get_module_range(name).ok()?, format))) + } - let pointer_size = match format { - BinaryFormat::PE => pe::MachineType::read(process, mono_module)?.pointer_size()?, - BinaryFormat::ELF => elf::pointer_size(process, mono_module)?, + fn pointer_size( + process: &Process, + module_range: (Address, u64), + format: BinaryFormat, + ) -> Option { + match format { + BinaryFormat::PE => pe::MachineType::read(process, module_range.0)?.pointer_size(), + BinaryFormat::ELF => elf::pointer_size(process, module_range.0), #[cfg(feature = "alloc")] - BinaryFormat::MachO => macho::pointer_size(process, module_range)?, + BinaryFormat::MachO => macho::pointer_size(process, module_range), #[allow(unreachable_patterns)] - _ => return None, - }; + _ => None, + } + } - let offsets = MonoOffsets::new(version, pointer_size, format)?; + fn attach_with( + process: &Process, + module_range: (Address, u64), + format: BinaryFormat, + pointer_size: PointerSize, + version: Version, + offsets: &'static MonoOffsets, + ) -> Option { + let (mono_module, _) = module_range; let root_domain_function_address = match format { BinaryFormat::PE => {