Things you probably shouldn't do: Bending Mono to your will
In the last post I described a way
to manipulate IL2CPP CLR in a somewhat unorthodox way by hooking into its API. While occasionally
useful, it does limit you to only IL2CPP builds, making it impractical. In this post, I will
describe how to achieve a similar trick with Mono. I'm assuming you know what CLR is, basics of
__dllexport
and how C# code is compiled, so if you're fuzzy on these concepts check
out previous blog post.
Unity and Mono
By default, all Unity applications run inside Mono CLR. This allows Unity to quickly compile code
and support multiple platforms without complicating things too much. This is true for both builds
and Unity Editor. It's a commonly known fact, however there are a few interesting nuances that
were needed to make it work.
When a regular (non-Unity) C# executable is launched, it usually immediately loads a pre-installed
CLR which is then able to JIT-compile and call application's entry point.
Your Unity projects, however, do not define an entry point (that is to say none of your .cs
files have a
Main
method). Furthermore, all your code is NOT compiled into an executable - it is
compiled into a DLL (Assembly-CSharp.dll
).
So where does the executable in your builds
come from and how does your DLL factor into it?
You have, without a doubt, seen contents of a Unity Mono build. I will be using a Windows Standalone
build in this example, but builds for all platforms consist of these pieces (some of them may be a
little harder to find)
If you read the previous blog post about IL2CPP you may be noticing something familiar -
UnityPlayer.dll
. The secret sauce that makes Unity such a powerful engine, it is an
unmanaged assembly that does most of the heavy lifting in your projects. Just as IL2CPP builds have
a Main
-ish method that hands execution off to UnityPlayer.dll
, so do
executables in Mono builds. These executables and dlls aren't unique to your project and can be
found in %EditorFolder%/Data/PlaybackEngines/windowsstandalonesupport/Variations
folder
along with a few other things that are shared between all projects built with the same Unity
version.
Then, of course, there's %ProjectName%_Data
folder. It contains all the resources
Unity needs to run your game: meshes, textures, sounds and, most importantly, managed assemblies
(found in %ProjectName%_Data/Managed
folder). This is where your
Assembly-CSharp
dll resides, but you will also find a lot of other dlls - from various
ones that enable Unity functionality to some fundamental ones like mscorlib
. Unlike
regular C# applications, Unity builds include every managed assembly that is referenced in your
project. This is one of two tricks that allows Unity builds to run without requiring users to
install .Net or Mono CLR.
The other trick is CLR itself. Including all managed assemblies does little if there's no CLR to
process them. Unity cannot assume that there is one, so it does the next best thing and ships your
builds with one. That is the purpose of MonoBleedingEdge
folder, the last piece of the
puzzle. Inside there you will find a file called mono-2.0-bdwgc.dll
- a Mono CLR.
It is worth noting that this is not the same CLR you'd get by running a regular C# application
with a mono
command. Just like IL2CPP CLR, it is a branch of Mono adapted for a more specific purpose.
Unlike IL2CPP CLR though, it resembles the original much more closely with only a few additions here
and there.
For those curious, "bdwgc" in the filename stands for "Boehm–Demers–Weiser Garbage
Collector" - an algorithm used by both Mono and IL2CPP builds. There is a detailed Unity
blog post that addresses why this specific GC was chosen and how it impacts your
applications, but that is not really relevant to the topic at hand.
What can I do with this information?
Now that we got theory out of the way let's see how we can use this knowledge in practice. As
was the case with IL2CPP, mono-2.0-bdwgc.dll
must be exporting some sort of API that is
used by UnityPlayer.dll
. In IL2CPP builds finding these methods was fairly simple - you
just search the entire source code for __dllexport
. But how do we find API of an
un-managed dll?
As discussed in the previous blog post, __dllimport
s work by linking to target methods
at runtime. That means that applications must have some form of a list of methods that need to be
linked and list of methods that can be linked to.
Enter Dependency Walker - a well-established tool
that looks through executable files to find said lists. Drag an executable (managed or un-managed) in
there and it will show a tree of dependencies with all of their imports and exports. Be warned
though that processing a file can take a bit and Dependency Walker will max out your CPU to the
point that you might not be able to do anything else (honestly, I can't help but admire in a
strange way its ability to squeeze every last drop of processing power for itself). So what do we
get when we drag mono-2.0-bdwgc.dll
in there?
After about 30 seconds of unresponsiveness the likes of which you may not have seen since Windows 98 you will get something that looks like the picture above. The highlighted area is where you will find the exported methods. For our purposes you will only need function names, so to simplify navigation you can select all exports (Ctrl+A), copy them (Ctrl+C) and paste them in a text editor of your choosing. As you will quickly notice Mono API is quite substantial. Dll from Unity 2020.3 build exports a whopping 1156 methods! As with IL2CPP I will leave these methods for you to discover, but here are some that caught my eye.
- mono_compile_method
- mono_gc_walk_heap
- mono_method_print_code
- mono_stack_walk
- mono_threads_request_thread_dump
- mono_unity_domain_mempool_chunk_foreach
- mono_unity_gc_disable / mono_unity_gc_enable
- mono_unity_gc_heap_foreach
- mono_unity_stop_gc_world / mono_unity_start_gc_world
Getting method signatures right can be a bit tricky. A good place to start is Mono's GitHub repo, but if it's
not there you might have to do some reverse engineering or brute-force the problem. Once you have
the signature you can DllImport
the method into C# code with this little chunk of code
[DllImport("mono-2.0-bdwgc", CallingConvention = CallingConvention.Cdecl, EntryPoint = "mono_object_get_size")]
public static extern uint GetObjectSize(object obj);
Beware that if you wish to use this trick on Android you need to replace "mono-2.0-bdwgc" with "libmonobdwgc-2.0". As was noted in the previous blog post messing with CLR API can be dangerous and lead to some undesirable behavior. Extreme caution and meticulous testing is advised. API can also change between releases, so be sure to pay extra attention when porting your project to a newer Unity version. It is worth noting that Unity updated their CLR in 2021.2 release with prior releases using Mono 5.11 and latest ones using Mono 6.13.