Skip to content

Nbv000/il2cpp-instrumentation-study

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

il2cpp-instrumentation-study

A runtime instrumentation bridge for IL2CPP-hosted processes -- typically Unity applications compiled for Linux, whose managed side is delivered as generated C++ inside GameAssembly.so. The bridge is a preloadable shared library that exposes a stable C ABI for enumerating managed assemblies, resolving classes and methods, invoking managed code, and reading or writing fields on live managed objects.

This is a research write-up, not a general-purpose library. It exists to document a specific set of techniques (signature-based method resolution, lazy class init recovery, main-thread dispatch under the constraint of a LD_PRELOADed native library) and to show what a working solution to each looks like.

What is IL2CPP and why does this need to exist

IL2CPP is Unity's ahead-of-time compilation pipeline. It converts the managed C# side of a Unity project into generated C++, then compiles the result to native code and packages it as GameAssembly.so alongside a small runtime that exposes a stable il2cpp_* C API. That API is what this bridge uses to walk metadata and invoke managed methods.

The public IL2CPP runtime is not distributed as a link-time library. There is no libil2cpp.so you can build against. Every consumer either generates headers from the target build (which ties them to a specific target) or resolves the runtime dynamically through dlsym. This bridge takes the second path, so it works against any IL2CPP build whose il2cpp_* exports are present.

What the bridge exposes

38 C-linkage functions, grouped roughly:

  • Bootstrap and discovery: BridgeInit, BridgePing, BridgeGetTargetModuleBase
  • Metadata walking: BridgeFindImage, BridgeFindClass, BridgeFindClassByName, BridgeIterateImageClasses, BridgeEnumClassMethods, BridgeIterateClassFields
  • Method resolution: BridgeFindMethod, BridgeFindMethodBySignature, BridgeFindMethodBySignatureNamed, BridgeFindStaticMethodReturning
  • Invocation: BridgeInvokeDirect, BridgeInvokeStatic, BridgeInvokeInstance, BridgeInvokeStaticOnClass, BridgeGetStaticProperty
  • Field access: BridgeReadStaticFieldValue, BridgeWriteStaticFieldValue, BridgeReadInstanceFieldValue, BridgeWriteInstanceFieldValue
  • Values, strings, arrays: BridgeStringNew, BridgeBoxValue, BridgeObjectUnbox, BridgeFindObjectsOfType, BridgeArrayLength, BridgeArrayGetObject
  • Main-thread dispatch: BridgeSubmitMainThread, BridgeRunOnMainThread, BridgeDrainMainThreadQueue, BridgeIsMainThread, BridgeNoteMainThread
  • Lazy class init: BridgeEnsureClassInit
  • Logging: RuntimeBridgeLog, RuntimeBridgeVLog, BridgeLogException

Every public function calls BridgeInit() first, and safely degrades to a zero / null / false result if bootstrap has not completed or failed.

What is interesting to read

If you have 15 minutes: docs/DESIGN.md. It walks through the four problems that took the most work to solve, and how the code addresses each:

  1. Bootstrap timing. Target module is loaded before its own runtime is initialized. Naive dlsym races and reads null domain pointers.
  2. Signature-based method resolution. Obfuscated targets rename classes and methods on every rebuild. Resolution by name breaks on every patch; resolution by shape (return type, parameter types, arity) does not.
  3. Lazy class init. il2cpp_class_get_methods silently returns an empty iterator until the class is initialized. il2cpp_runtime_class_init runs managed .cctor code and must be dispatched to the main thread.
  4. Main-thread dispatch with race-safe timeout cleanup. Blocking calls through BridgeRunOnMainThread allocate their sync primitives on the caller's stack. If the caller times out before the drain thread picks up the job, we have to remove the job under the queue mutex before the sync primitives get destroyed.

Building

$ make
g++ -std=c++17 -O2 -Wall -Wextra -fPIC -fvisibility=hidden -Iinclude \
    src/runtime_bridge.cpp -o runtime_bridge.so -shared -ldl -lpthread

You get runtime_bridge.so, ~48KB, exporting 38 Bridge* and Runtime* symbols under default visibility.

Using it

The bridge is designed to be LD_PRELOADed into an IL2CPP process alongside a domain-specific consumer -- a second preloaded library that knows the shape of the managed API in the target and drives the bridge to do useful things.

$ LD_PRELOAD=./runtime_bridge.so:./my_domain_layer.so ./target_binary

Inside the domain layer, resolve bridge exports via dlsym(RTLD_DEFAULT, ...) because the bridge is loaded with RTLD_GLOBAL:

auto find_class = (BridgeFindClassFn)dlsym(RTLD_DEFAULT, "BridgeFindClass");
Il2CppClass* env = find_class("mscorlib.dll", "System", "Environment");

See examples/inspect_target.cpp for a self-contained walkthrough that resolves the bridge, waits for it to come up, and enumerates the methods of System.Environment.

Environment variables

  • BRIDGE_LOG -- path to the bridge log file (default /tmp/runtime_bridge.log).
  • BRIDGE_TARGET_MODULE -- override the default target module name (GameAssembly.so) if the host uses a different filename.

Scope and non-goals

  • Linux only. /proc/self/maps, dlopen(..., RTLD_NOLOAD), and LD_PRELOAD are all POSIX-y, but the module-discovery logic is Linux-shaped. A macOS port would need DYLD_INSERT_LIBRARIES and a different maps reader.
  • No hot-reload. The bridge does not attempt to survive a runtime reload of the target module.
  • No generated proxies. External consumers pass class names, method names, and argument arrays raw. A future iteration could generate typed stubs from a metadata dump, but that would defeat the point of signature-based resolution.

License

MIT. See LICENSE.

About

Runtime instrumentation bridge for IL2CPP-hosted processes (Unity Linux builds). Signature-based method resolution, lazy class init recovery, LD_PRELOAD bootstrap.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors