Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 56 additions & 3 deletions src/game_engine/unity/mono/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,56 @@ impl Class {
.await
}

fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option<Address> {
/// Reads the class of an object found in memory.
///
/// Every managed object begins with a pointer to its class's vtable, and a
/// vtable begins with a pointer to its class, so an object can be resolved
/// back to a [`Class`] whose fields are then available by name.
///
/// This is the only way to reach a generic instantiation such as
/// `HashSet<string>`. Each instantiation is a distinct class with its own
/// field offsets, and none of them can be looked up in an
/// [`Image`](super::Image) by name -- but any instance points at its own.
///
/// # Inflated generics may have no field names
///
/// Mono fills a class's field table in lazily, and for an inflated generic
/// nothing necessarily has. [`get_field_offset`](Self::get_field_offset)
/// can therefore return [`None`] for every field of a class resolved this
/// way, against an object that is plainly an instance of it -- and the
/// same lookup may succeed against another process running the same build,
/// so it is runtime state rather than anything a version check could
/// predict. Code that must not fail on a collection needs a fallback to
/// the known layout, guarded by a check that the object agrees with it.
pub fn of_object(process: &Process, module: &Module, object: Address) -> Option<Self> {
// `MonoVTable::klass` is the first member in every version Mono has
// shipped, so unlike the other offsets in this module it needs no
// version table. It is the same invariant an object's own header
// relies on.
let vtable = process
.read_pointer(object, module.pointer_size)
.ok()
.filter(|addr| !addr.is_null())?;
let class = process
.read_pointer(vtable, module.pointer_size)
.ok()
.filter(|addr| !addr.is_null())?;
Some(Self { class })
}

/// Returns the address of this class's `MonoVTable` in the first domain.
///
/// Every managed object begins with a pointer to the vtable of its class,
/// so this doubles as an identity handle for the class: an object at
/// `addr` is an instance of this exact class if and only if the pointer at
/// `addr` equals this value.
///
/// This is useful for games where the object of interest cannot be reached
/// by walking static fields. Games built around constructor-injection
/// dependency injection often have no static roots at all, which makes
/// [`UnityPointer`](super::UnityPointer) inapplicable, and the only way to
/// find a service is to scan the heap for the instance of its class.
pub fn get_vtable(&self, process: &Process, module: &Module) -> Option<Address> {
let runtime_info = process
.read_pointer(
self.class + module.offsets.class.runtime_info,
Expand All @@ -144,10 +193,14 @@ impl Class {
.ok()
.filter(|addr| !addr.is_null())?;

let mut vtables = process
process
.read_pointer(runtime_info + module.size_of_ptr(), module.pointer_size)
.ok()
.filter(|addr| !addr.is_null())?;
.filter(|addr| !addr.is_null())
}

fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option<Address> {
let mut vtables = self.get_vtable(process, module)?;

// Mono V1 behaves differently when it comes to recover the static table
match module.version {
Expand Down