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)

Image Description A Windows Standalone build with Mono runtime

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, __dllimports 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?

Image Description mono-2.0-bdwgc.dll exports

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.

Follow us:

Written by

Steve Boytsun