djkaty / Il2CppInspector

Powerful automated tool for reverse engineering Unity IL2CPP binaries
http://www.djkaty.com
GNU Affero General Public License v3.0
2.54k stars 415 forks source link

[Q] Clarification on "il2cpp_thread_attach" #155

Closed Kein closed 3 years ago

Kein commented 3 years ago

Many IL2CPP methods require some back-end thread structures to be set correctly or they will throw an access violation exception. Place the following at the start of your code to make sure everything is set up: il2cpp_thread_attach(il2cpp_domain_get());

Any chance I can get some clarification on this? I assume this is absolutely basic knowledge to anyone who knows/code in CPP.

I know that by default, Unity player has only one logic thread - "Main". Main thread responsible to receive callbacks from native code, process mono domain aka "scripts" compiled into net assemblies (thread workers spawned by CLR are their own thing of course) and send icalls back to native. Attempt to call any Unity internal functions/API outside of Main thread causes exception/access violation. However accessing any other user data or functions that do not use any Unity API is unconstrained from any of the threads (usual data synchronization concerns apply). DOTS/ECS system setup is a completely different beast that adds a thousandfold complexity we will skip here.

So, when it comes to usual mono Unity modding, any mods by default are being ran on the same Main thread and mod's "code" lives in the old good realm of Unity's PlayerLoop. We have pretty clear order of execution that we can easily reason about in our usercode.

Now, in the examples for C++ scaffolding project:
https://katyscode.wordpress.com/2020/11/27/il2cppinspector-tutorial-how-to-create-use-and-debug-il2cpp-dll-injection-projects/
https://katyscode.wordpress.com/2021/01/14/il2cppinspector-tutorial-working-with-code-in-il2cpp-dll-injection-projects/
the suggested method to inject/test code was CheatEngine's ability to inject dll into the process. As I understand, this spawn its own thread which then ordered to execute the library' code (DLL point of entry). So, as I gather, that code lives on its own thread and similar restrictions apply - no access to Unity API, but full unconstrained access to any user code that lives in IL2cpp domain (read variables, call usercode functions, etc).

So what exactly does il2cpp_thread_attach() do? Switches context to the Main thread? Temporary? Permanently? Or somehow "dispatches" the code instruction to the Main thread? In both cases - at which point, relative to player loop order of events (Il2CPP or not, this order hasnt changed in non-DOTS builds) this happens? I've noticed that "Sleep(9999999)" has 0 effect on Main thread so apparently it is still executed on its own thread spawned by injector.

djkaty commented 3 years ago

It sounds like you already know significantly more about this than I do. I know very little about how Unity works or game modding, I only work on the IL2CPP side of things.

That being said, il2cpp_thread_attach called il2cpp::vm::Thread::Attach for which you can find the source code in libil2cpp/vm/Thread.cpp. From what I can see it creates a System.Threading.Thread object for the current thread, sets up TLS for the thread and sets its context to the default thread context for the AppDomain. I've never looked into it until now, all I really know is that you need it to prevent the access violations.

Hope that gives you a lead anyhow :)

Kein commented 3 years ago

That being said, il2cpp_thread_attach called il2cpp::vm::Thread::Attach

So this functionality comes with il2cpp baggage? My bad, I thought this was your own helper.
Interesting.

From what I can see it creates a System.Threading.Thread object for the current thread, sets up TLS for the thread and sets its context to the default thread context for the AppDomain.

Yeah, that does not explain much. Doing quick-reading on threading mode in C++ it seems like it is not much different from managed C#, same limitations apply. So what magic l2cpp_thread_attach performs is still a mystery. My main concern is WHERE, at which point in gameloop, it performs that. May be there is a special callback that gets put into event queue.

djkaty commented 3 years ago

I would assume that each running thread needs an initialized Il2CppThread object in some table that IL2CPP refers to when certain calls are made, and the crash occurs as a result of it fetching an uninitialized pointer (from a non-existent Il2CppThread instance) and then trying to dereference it. Just a guess though.

When you say "APIs" I should note that TTBOMK all of the il2cpp_* APIs work just fine without calling il2cpp_thread_attach first, it's the pseudo-managed method calls that are troublesome, so I'm assuming you mean the Unity base class library when you say "APIs".

Il2CppInspector's APIs begin with il2cppi_, ie. there is an extra i at the end.

Beyond that, I have no idea I'm afraid, sorry.

Kein commented 3 years ago

APIs work just fine without calling il2cpp_thread_attach first, it's the pseudo-managed method calls that are troublesome, so I'm assuming you mean the Unity base class library when you say "APIs".

By API I'm talking about the calls to native side/code (the engine, the unityplayer itself) from mono (under mono environment, in this example). So basically all icalls to UnityPlayer is what I and Unity engineers call API, when they talk in the context of thread restrictions. Obviously there is plenty of extra libraries that also provide functional you can call API but they are different thing because they almost always are 100% managed code like Unity Mathematics CL or something similar.
So in classic mono environment, as long as you dont try to interop with Unity Player API from other threads you are good. I dont think this changes much under il2cpp, just gets abstracted.

Kein commented 3 years ago

Well there ya go:

The il2cpp_thread_attach function attaches the thread to the virtual machine, so that the garbage collector can track it. That function does not interact with Unity Engine code at all.

That makes sense now. Guess I've only tested it on API that was somehow ok for threaded RO access. Transform writes still throw so, yeah... I guess the only way to keep your plugin code in main thread loop is to patch during LoadLibrary lock/dllmain time some method to get callbacks back.

djkaty commented 3 years ago

That makes sense yes, where is the quote from?

And yes you're correct, the reason the injected code runs in a different thread is because LoadLibrary blocks until the loaded DLL's DllMain finishes executing. If you want to make code run in the main thread, you'll need to use function hooking (make a detour trampoline).

Kein commented 3 years ago

One of the Unity engineers.

you'll need to use function hooking (make a detour trampoline).

Yep. Case closed.