Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bugfix attempt for mismatched jump labels by IlProcessor #77

Closed
wants to merge 1 commit into from

Conversation

mgreter
Copy link

@mgreter mgreter commented Jun 18, 2023

This is a potential BugFix for #65, although I don't really know the implications.
Please check and give feedback and I can adjust it accordingly if needed.
I certainly only tested it with my specific use case!

Addresses GitHub Issue #65

  • Keep original leave jump labels as given by original IL.
  • Clean-up unnecessary double leave op-codes (from IlProcessor).

Hope it's okay to add the dll if others want to test if this fix also works for them: 0Harmony.dll.zip
Note: I've had to up the net version to 48 as I don't have 45 on this PC ...

Keep original leave jump labels as given by original IL.
Clean-up unnecessary double leave op-codes (from IlProcessor).

Addresses GitHub Issue BepInEx#65
@mgreter mgreter changed the title Bugfix for mismatched jump labels by IlProcessor Bugfix attempt for mismatched jump labels by IlProcessor Jun 18, 2023
@mgreter
Copy link
Author

mgreter commented Jul 23, 2023

Is there anything else I could provide to get this/a fix over the merge line?
I know that I can't assess all the potential edge-cases this fix could brake.
But we certainly see issues with transpiler patches and the current implementation.

@ManlyMarco
Copy link
Member

ManlyMarco commented Jul 23, 2023

I did a quick IL compare of this and the release version, and it looks like this version changes where the last leave call of a catch jumps after running a transpiler.

In some cases it's I think harmless (this PR on left, latest release on right):
Studio SceneInfo Load

In other cases it's not so harmless (this PR on left, latest release on right, transpiler used):
hmm

Looks like I can't merge this since it would result in a potentially quite bad regression (the above issue happened in multiple patches).

@mgreter
Copy link
Author

mgreter commented Jul 23, 2023

Thanks for taking a look, although a bit hard to reproduce your "regression" on my own here, so I had to go by the screenshots. And if I'm not wrong it doesn't really look like it would regress (maybe it's even doing the proper thing now). Edit. also see that the try/catch now actually jumps to different parts in the code, which may or may not be more reasonable to expect!?

Your second screen will jump right after the "end handler (catch)". There it will load the "stfld lastLoadErrorCode" again, doesn't it? IMO it seems it was doing the exact same code thing in the catch clause too and put the result into "stloc.s V_8".

My patch on the other hand will jump to the label where it will load the value it has put before into "ldloc.s V_8" again. After that both parts seem to jump then to the same label again "br IL_033f". Did you actually try/test the resulting executable if any regression actually show when the patched code is run? I couldn't get it to produce any regressions on my tests, but as I said, I can only test a limited amount, although at some point I did just blindly add empty transpiler patches to all kind of methods, and with my patch applied it never failed AFAIR.

Anyway, thx again for looking into this issue!

Can you maybe compare it to IL code before any patch was applied? Another argument for that it is now more correct is that nothing else seems to use the jump label at "0338", but there was one to begin with. So original code probably had some jumps to that position, but where are they after patching with your latest release?

FWIW I see one possible regression with your test patches and my fix here. If you insert code at a position that has jump labels attached, you will need to make sure to move the labels too, if you want to insert new opcodes to run before the original code. I kinda can see how the "fix" I replaced in this PR could maybe been there to "address" this potential issue!?

var labels = codes[i].labels;
codes[i].labels = null;
codes.InsertRange(i, new[]{ ... });
codes[i].labels = labels;

@ManlyMarco
Copy link
Member

You are right, it appears like the original IL jumps like the fixed version rather than the release version. This is the original IL for the second example I posted:
image

I do remember some issues with labels in the past, but I think it was a different issue where a new label object was instantiated incorrectly.

I'll have to check for differences in different Unity versions of the game code and patch I posted.

@ManlyMarco
Copy link
Member

ManlyMarco commented Jul 24, 2023

I did find a regression, one of very old patches now throws. This is under Unity 5.6.2 with dynamicmethod backend:

[Error  : Unity Log] ArgumentException: Label not marked
Stack trace:
System.Reflection.Emit.ILGenerator.label_fixup ()
System.Reflection.Emit.DynamicMethod.CreateDynMethod ()
System.Reflection.MonoMethod.Invoke (System.Object obj, BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture)
Rethrow as TargetInvocationException: Exception has been thrown by the target of an invocation.
System.Reflection.MonoMethod.Invoke (object,System.Reflection.BindingFlags,System.Reflection.Binder,object[],System.Globalization.CultureInfo) <IL 0x000ea, 0x0069d>
System.Reflection.MethodBase.Invoke (object,object[]) <IL 0x00006, 0x0006e>
MonoMod.RuntimeDetour.Platforms.DetourRuntimeMonoPlatform.GetMethodHandle (System.Reflection.MethodBase) <IL 0x00019, 0x000fa>
MonoMod.RuntimeDetour.Platforms.DetourRuntimeILPlatform.GetIdentifiable (System.Reflection.MethodBase) <IL 0x00015, 0x000d0>
MonoMod.RuntimeDetour.Platforms.DetourRuntimeILPlatform.Pin (System.Reflection.MethodBase) <IL 0x00002, 0x000b0>
MonoMod.RuntimeDetour.DetourHelper.Pin<System.Reflection.MethodBase> (System.Reflection.MethodBase) <0x0007a>
MonoMod.RuntimeDetour.Detour..ctor (System.Reflection.MethodBase,System.Reflection.MethodBase,MonoMod.RuntimeDetour.DetourConfig&) <IL 0x0006b, 0x0044f>
(wrapper dynamic-method) MonoMod.RuntimeDetour.ILHook/Context.DMD<MonoMod.RuntimeDetour.ILHook+Context..Refresh> (MonoMod.RuntimeDetour.ILHook/Context) <IL 0x00183, 0x00d71>
(wrapper dynamic-method) MonoMod.Utils.DynamicMethodDefinition.Trampoline<MonoMod.RuntimeDetour.ILHook+Context..Refresh>?-417493248 (object) <IL 0x00026, 0x00306>
HarmonyLib.Internal.RuntimeFixes.StackTraceFixes.OnILChainRefresh (object) <IL 0x00006, 0x00060>
MonoMod.RuntimeDetour.ILHook.Apply () <IL 0x00059, 0x00305>
HarmonyLib.Public.Patching.ManagedMethodPatcher.DetourTo (System.Reflection.MethodBase) <IL 0x0004d, 0x00286>
Rethrow as HarmonyException: IL Compile Error (unknown location)
HarmonyLib.Public.Patching.ManagedMethodPatcher.DetourTo (System.Reflection.MethodBase replacement)
HarmonyLib.PatchFunctions.UpdateWrapper (System.Reflection.MethodBase original, HarmonyLib.PatchInfo patchInfo)
Rethrow as HarmonyException: IL Compile Error (unknown location)
HarmonyLib.PatchFunctions.UpdateWrapper (System.Reflection.MethodBase original, HarmonyLib.PatchInfo patchInfo)
HarmonyLib.PatchClassProcessor.ProcessPatchJob (HarmonyLib.Job job)
UnityEngine.GameObject:AddComponent(Type)
BepInEx.Bootstrap.Chainloader:Start()
UnityEngine.Application:.cctor()
Sirenix.Utilities.GlobalConfigAttribute:get_FullPath()
Sirenix.Utilities.GlobalConfigAttribute:get_FullPath()
Sirenix.Utilities.GlobalConfigAttribute:get_ResourcesPath()
Sirenix.Utilities.GlobalConfig`1:LoadInstanceIfAssetExists()
Sirenix.Serialization.UnitySerializationInitializer:Initialize()
Sirenix.Serialization.UnitySerializationInitializer:InitializeRuntime()

with cecil backend:

[Error  : Unity Log] IndexOutOfRangeException: Array index is out of range.
Stack trace:
Mono.Collections.Generic.Collection`1.get_Item (int) <0x000b5>
MonoMod.Utils.Extensions.Clone (Mono.Cecil.Cil.MethodBody,Mono.Cecil.MethodDefinition) <IL 0x000ee, 0x00861>
MonoMod.Utils.DMDCecilGenerator._Generate (MonoMod.Utils.DynamicMethodDefinition,object) <IL 0x002e3, 0x0196e>
MonoMod.Utils.DMDGenerator`1<MonoMod.Utils.DMDCecilGenerator>.Generate (MonoMod.Utils.DynamicMethodDefinition,object) <0x00121>
MonoMod.Utils.DynamicMethodDefinition.Generate (object) <IL 0x0007f, 0x00472>
MonoMod.Utils.DynamicMethodDefinition.Generate () <IL 0x00002, 0x0004e>
(wrapper dynamic-method) MonoMod.RuntimeDetour.ILHook/Context.DMD<MonoMod.RuntimeDetour.ILHook+Context..Refresh> (MonoMod.RuntimeDetour.ILHook/Context) <IL 0x00156, 0x00c1f>
(wrapper dynamic-method) MonoMod.Utils.DynamicMethodDefinition.Trampoline<MonoMod.RuntimeDetour.ILHook+Context..Refresh>?78112256 (object) <IL 0x00026, 0x00306>
HarmonyLib.Internal.RuntimeFixes.StackTraceFixes.OnILChainRefresh (object) <IL 0x00006, 0x00060>
MonoMod.RuntimeDetour.ILHook.Apply () <IL 0x00059, 0x00305>
HarmonyLib.Public.Patching.ManagedMethodPatcher.DetourTo (System.Reflection.MethodBase) <IL 0x0004d, 0x00286>
Rethrow as HarmonyException: IL Compile Error (unknown location)
HarmonyLib.Public.Patching.ManagedMethodPatcher.DetourTo (System.Reflection.MethodBase replacement)
HarmonyLib.PatchFunctions.UpdateWrapper (System.Reflection.MethodBase original, HarmonyLib.PatchInfo patchInfo)
Rethrow as HarmonyException: IL Compile Error (unknown location)
HarmonyLib.PatchFunctions.UpdateWrapper (System.Reflection.MethodBase original, HarmonyLib.PatchInfo patchInfo)
HarmonyLib.PatchClassProcessor.ProcessPatchJob (HarmonyLib.Job job)
UnityEngine.GameObject:AddComponent(Type)
BepInEx.Bootstrap.Chainloader:Start()
UnityEngine.Application:.cctor()
Sirenix.Utilities.GlobalConfigAttribute:get_FullPath()
Sirenix.Utilities.GlobalConfigAttribute:get_FullPath()
Sirenix.Utilities.GlobalConfigAttribute:get_ResourcesPath()
Sirenix.Utilities.GlobalConfig`1:LoadInstanceIfAssetExists()
Sirenix.Serialization.UnitySerializationInitializer:Initialize()
Sirenix.Serialization.UnitySerializationInitializer:InitializeRuntime()

The same patch runs fine on Unity 2019.4.9 with cecil backend, but original IL appears to be slightly different.

Source of the patch in question (PH == false)
IL output from console before the patch fails
original IL 562 (the one that crashes)
original IL 2019 (the one that works)

@mgreter
Copy link
Author

mgreter commented Jul 24, 2023

That will be quite hard to debug without the original function to reproduce it, but I can try :)
FWIW maybe try to move my "Remove duplicate leave Op-Codes to cleanup" above "Step 2: Emit code".
Looking at it, it might be that this removal after MarkLabel calls messes with offset locations.

@ManlyMarco
Copy link
Member

I can help you repro this if you DM me on discord.

@ManlyMarco
Copy link
Member

Bump, does anyone want to try fixing this?

@StarFluxGames
Copy link

Do we have any updates on this / if it's production ready?

@ManlyMarco
Copy link
Member

Do we have any updates on this / if it's production ready?

The issue I've hit was not fixed, I didn't hear from @mgreter after his last message here.

@ManlyMarco
Copy link
Member

@mgreter If you're still around, can you check #89?

@mgreter
Copy link
Author

mgreter commented Nov 25, 2023

@mgreter If you're still around, can you check #89?

Did a test, but unfortunately that patch seems to regress even cases that didn't before here :-/
Sorry to not have better news; but it doesn't fix my repro case, instead it regresses stuff that was working before.
Unfortunately I don't have any repro case locally to check for what you deem to regress with my posted potential fix.
FWIW it seems my own proposed bugfix, that seem to have worked here in the past, seems to regress now too :-(
Maybe I'm testing it all wrong, but original code does work to some extend, until testing the repro case, which still fails.

@mgreter
Copy link
Author

mgreter commented Nov 25, 2023

I think I at least solved the current regression also against my own previous fix.
Will need some more time (probably/maybe next weekend) to verify further.
Sorry, assumptions above may not be relevant, but keeping labels in place is:

@@ -162,10 +162,12 @@ public static class OpaqueTextures
             IEnumerable<CodeInstruction> instructions)
         {
             var codes = new List<CodeInstruction>(instructions);
-            // Execute our static function to do our thing
-            codes.Insert(codes.Count - 2, CodeInstruction.Call(
-                typeof(OpaqueTextures), "InitOpaqueConfig"));
-            // Return new IL codes
+            var lbls = codes[codes.Count - 2].labels;
+            codes[codes.Count - 2].labels = null;
+            var instr = CodeInstruction.Call(
+                typeof(OpaqueTextures), "InitOpaqueConfig");
+            codes.Insert(codes.Count - 2, instr);
+            codes[codes.Count - 3].labels = lbls;
             return codes;
         }
     }

Moving the existing labels seems to fix my new regression.
Maybe original fix tried to adjust/guess this case!?

@ManlyMarco
Copy link
Member

Possibly, let me know whenever you have something to test. I can also send you the setup to reproduce my issue if you want.

@StarFluxGames
Copy link

I think I at least solved the current regression also against my own previous fix. Will need some more time (probably/maybe next weekend) to verify further. Sorry, assumptions above may not be relevant, but keeping labels in place is:

@@ -162,10 +162,12 @@ public static class OpaqueTextures
             IEnumerable<CodeInstruction> instructions)
         {
             var codes = new List<CodeInstruction>(instructions);
-            // Execute our static function to do our thing
-            codes.Insert(codes.Count - 2, CodeInstruction.Call(
-                typeof(OpaqueTextures), "InitOpaqueConfig"));
-            // Return new IL codes
+            var lbls = codes[codes.Count - 2].labels;
+            codes[codes.Count - 2].labels = null;
+            var instr = CodeInstruction.Call(
+                typeof(OpaqueTextures), "InitOpaqueConfig");
+            codes.Insert(codes.Count - 2, instr);
+            codes[codes.Count - 3].labels = lbls;
             return codes;
         }
     }

Moving the existing labels seems to fix my new regression. Maybe original fix tried to adjust/guess this case!?

Do we happen to have any updates on this?

@mgreter
Copy link
Author

mgreter commented Dec 14, 2023

Do we happen to have any updates on this?

Not from me. It does change the original behavior AFAICT, as I now need to make sure to move the label if needed. But that seems to be ok/correct. E.g. there is a semantic difference when injecting some code at a certain point, as in: should original code jump to the beginning of the new code or after it.

@kohanis
Copy link
Contributor

kohanis commented Dec 17, 2023

It's not mismatched jump by IlProcessor, it's actually correct from it point of view, we removed correct leave ourselves. Why do we need these checks in the first place? I'd say it's unexpected behaviour from library user's point of view and contradicts KISS

What I meant was why would we even remove second jump if first one is going to prevent second one from happening anyway? Let it be by completely deleting `Case 1'

// This would be a breaking change anyway, regardless of the implementation of the fix. Though I doubt if anyone has ever intentionally used it. I don't see any reliable way to determine if the jump was originally from block or not, given the transpiler's capabilities

@ManlyMarco
Copy link
Member

Did anyone else test this PR for regressions in other games? I've only found a single patch that started failing, so it's not a deal breaker if it does fix other issues. Especially since this harmony update would not be used in bep5 so would not immediately affect the patch in question.

@kohanis
Copy link
Contributor

kohanis commented Dec 17, 2023

I'd still recommend keeping it simple:

@@ -346,11 +346,7 @@ internal class ILManipulator
 			cur.blocks.ForEach(b => il.MarkBlockBefore(b));
 
 			// We need to handle exception handler opcodes specially because ILProcessor emits them automatically
-			// Case 1: leave + start or end of exception block => ILProcessor generates leave automatically
-			if ((cur.opcode == SRE.OpCodes.Leave || cur.opcode == SRE.OpCodes.Leave_S) &&
-			    (cur.blocks.Count > 0 || next?.blocks.Count > 0))
-				goto mark_block;
-			// Case 2: endfilter/endfinally and end of exception marker => ILProcessor will generate the correct end
+			// endfilter/endfinally and end of exception marker => ILProcessor will generate the correct end
 			if ((cur.opcode == SRE.OpCodes.Endfilter || cur.opcode == SRE.OpCodes.Endfinally) && cur.blocks.Count > 0)
 				goto mark_block;
 			// Other cases are either intentional leave or invalid IL => let them be processed and let JIT generate correct exception

Yes, there will be two leave instructions, but is deduplication worth the potential new regression, even for older versions of mono?

@ManlyMarco
Copy link
Member

Related #96

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants