diff --git a/src/game_engine/unity/mono/class.rs b/src/game_engine/unity/mono/class.rs index 2ffe93c6..2d302eb5 100644 --- a/src/game_engine/unity/mono/class.rs +++ b/src/game_engine/unity/mono/class.rs @@ -135,7 +135,56 @@ impl Class { .await } - fn get_static_table_pointer(&self, process: &Process, module: &Module) -> Option
{ + /// 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`. 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 { + // `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
{ let runtime_info = process .read_pointer( self.class + module.offsets.class.runtime_info, @@ -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
{ + let mut vtables = self.get_vtable(process, module)?; // Mono V1 behaves differently when it comes to recover the static table match module.version {