找回密码
 立即注册
首页 资源区 代码 .NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式 ...

.NET外挂系列:3. 了解 harmony 中灵活的纯手工注入方式

FatimaNuyt 2025-5-28 22:07:44
一:背景

1. 讲故事

上一篇我们讲到了 注解特性,harmony 在内部提供了 20个 HarmonyPatch 重载方法尽可能的让大家满足业务开发,那时候我也说了,特性虽然简单粗暴,但只能解决 95% 的问题,言外之意还有一些事情做不到,所以剩下的 5% 只能靠 完全手工 的方式了。
二:注解特性的局限性

虽然有20个重载方法,但还不能达到100%覆盖,不要以为我说的这种情况比较罕见,是很正常的场景,比如说:

  • 嵌套类。
  • 程序集中的某些特殊不对外公开类。
这里我就拿第二种来说把,参考代码如下:
  1. internal sealed class ServiceProviderEngineScope : IServiceScope, IDisposable, IServiceProvider, IKeyedServiceProvider, IAsyncDisposable, IServiceScopeFactory
  2. {
  3.     public ServiceProviderEngineScope(ServiceProvider provider, bool isRootScope)
  4.     {
  5.         ResolvedServices = new Dictionary<ServiceCacheKey, object>();
  6.         RootProvider = provider;
  7.         IsRootScope = isRootScope;
  8.     }
  9. }
复制代码
这段代码有几个要素:
1. internal

代码是程序集可访问,所以你不能使用任何 typeof(xxx) 形式的构造函数,否则就会报错,参考如下:
1.png

2. 有参构造函数

由于不能使用 typeof(xxx),所以只能通过 字符串模式 反射type,当你有心查找你会发现第20个重载方法虽然支持 string 格式,但不提供 Type[] argumentTypes 参数信息,代码如下:
  1. [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Delegate, AllowMultiple = true)]
  2. public class HarmonyPatch : HarmonyAttribute
  3. {
  4.     ...
  5.     public HarmonyPatch(string typeName, string methodName, MethodType methodType = MethodType.Normal);
  6.     ...
  7. }
复制代码
所以这个就是很无语的事情了,哈哈,上面所说的其实就是我最近遇到了一例 .NET托管内存暴涨 问题,观察托管堆之后,发现有 975w 的 ServiceProviderEngineScope 类,截图如下:
2.png

熟悉这个类的朋友应该明白,这是上层调用 serviceProvider.CreateScope() 方法没有释放导致的,那接下来的问题是到底谁在不断的调用 CreateScope() 呢? 直接监控 ServiceProviderEngineScope 的构造函数就可以了。
三:解决方案

1. 使用 TargetMethod 口子函数

上一篇跟大家聊过 harmony 的口子函数 TargetMethods,它可以批量返回需要被 patch 的方法,如果你明确知道只需返回一个,可以用 TargetMethod 口子来实现,有了这些思路之后,完整的实现代码如下:
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             var harmony = new Harmony("com.dotnetdebug.www");
  6.             harmony.PatchAll();
  7.             // 1. 创建服务集合
  8.             var services = new ServiceCollection();
  9.             // 2. 注册一个作用域服务
  10.             services.AddScoped<MyService>();
  11.             // 3. 构建服务提供者
  12.             var serviceProvider = services.BuildServiceProvider();
  13.             // 4. 创建作用域
  14.             var scope = serviceProvider.CreateScope();
  15.             var myService = scope.ServiceProvider.GetRequiredService<MyService>();
  16.             myService.DoSomething();
  17.             Console.ReadLine();
  18.         }
  19.     }
  20.     class MyService : IDisposable
  21.     {
  22.         public MyService()
  23.         {
  24.             Console.WriteLine("i'm MyService...");
  25.         }
  26.         public void DoSomething()
  27.         {
  28.             Console.WriteLine($"{DateTime.Now} Doing work...");
  29.         }
  30.         public void Dispose()
  31.         {
  32.             Console.WriteLine($"{DateTime.Now} Disposing MyService");
  33.         }
  34.     }
  35.     [HarmonyPatch]
  36.     public class HookServiceProviderEngineScope
  37.     {
  38.         [HarmonyTargetMethod]
  39.         static MethodBase TargetMethod()
  40.         {
  41.             var engineScopeType = Type.GetType("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection");
  42.             var constructor = engineScopeType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0];
  43.             return constructor;
  44.         }
  45.         public static void Prefix(bool isRootScope)
  46.         {
  47.             Console.WriteLine("----------------------------");
  48.             Console.WriteLine($"isRootScope:{isRootScope}");
  49.             Console.WriteLine(Environment.StackTrace);
  50.             Console.WriteLine("----------------------------");
  51.         }
  52.     }
复制代码
3.png

有些朋友可能要说了,这地方为什么会有两个调用栈,熟悉底层的朋友应该知道分别由 services.BuildServiceProvider 和 serviceProvider.CreateScope 贡献的。
写到这里的时候,出门抽了个烟,突然灵光一现,既然20个单重载方法不够用,我完全可以使用 HarmonyPatch 注解特性组合呀。。。相当于平级补充,说干就干,参考代码如下:
  1.     [HarmonyPatch("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection", null, MethodType.Constructor)]
  2.     [HarmonyPatch(new Type[2] { typeof(ServiceProvider), typeof(bool) })]
  3.     public class HookServiceProviderEngineScope
  4.     {
  5.         public static void Prefix(bool isRootScope)
  6.         {
  7.             Console.WriteLine("----------------------------");
  8.             Console.WriteLine($"isRootScope:{isRootScope}");
  9.             Console.WriteLine(Environment.StackTrace);
  10.             Console.WriteLine("----------------------------");
  11.         }
  12.     }
复制代码
有了胜利喜悦之后,我想可有神鬼不测之术来解决 嵌套类 的问题,纠结了之后用 HarmonyPatch 特性理论上搞不定。
2. 完全动态hook

整体上来说前面的 TargetMethod 模式属于混合编程(特性+手工),如果让代码更纯粹一点话,就要把所有的 Attribute 摘掉,这就需要包装器类 HarmonyMethod ,修改后的代码如下:
  1.     internal class Program
  2.     {
  3.         static void Main(string[] args)
  4.         {
  5.             var harmony = new Harmony("com.dotnetdebug.www");
  6.             var engineScopeType = Type.GetType("Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope, Microsoft.Extensions.DependencyInjection");
  7.             var originalMethod = engineScopeType.GetConstructors(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)[0];
  8.             var prefixMethod = typeof(HookServiceProviderEngineScope).GetMethod("Prefix");
  9.             harmony.Patch(originalMethod, new HarmonyMethod(prefixMethod));
  10.             // 1. 创建服务集合
  11.             var services = new ServiceCollection();
  12.             // 2. 注册一个作用域服务
  13.             services.AddScoped<MyService>();
  14.             // 3. 构建服务提供者
  15.             var serviceProvider = services.BuildServiceProvider();
  16.             // 4. 创建作用域
  17.             var scope = serviceProvider.CreateScope();
  18.             var myService = scope.ServiceProvider.GetRequiredService<MyService>();
  19.             myService.DoSomething();
  20.             Console.ReadLine();
  21.         }
  22.     }
  23.     class MyService : IDisposable
  24.     {
  25.         public MyService()
  26.         {
  27.             Console.WriteLine("i'm MyService...");
  28.         }
  29.         public void DoSomething()
  30.         {
  31.             Console.WriteLine($"{DateTime.Now} Doing work...");
  32.         }
  33.         public void Dispose()
  34.         {
  35.             Console.WriteLine($"{DateTime.Now} Disposing MyService");
  36.         }
  37.     }
  38.     public class HookServiceProviderEngineScope
  39.     {
  40.         public static void Prefix(bool isRootScope)
  41.         {
  42.             Console.WriteLine("----------------------------");
  43.             Console.WriteLine($"isRootScope:{isRootScope}");
  44.             Console.WriteLine(Environment.StackTrace);
  45.             Console.WriteLine("----------------------------");
  46.         }
  47.     }
复制代码
4.png

这里稍微提一下 HarmonyMethod 类,它的内部有很多的参数可以配置,比如 优先级,日志 功能,这些都是 Attribute 所做不了的,参考如下:
  1. public class HarmonyMethod
  2. {
  3.     public MethodInfo method;
  4.     public string category;
  5.     public Type declaringType;
  6.     public string methodName;
  7.     public MethodType? methodType;
  8.     public Type[] argumentTypes;
  9.     public int priority = -1;
  10.     public string[] before;
  11.     public string[] after;
  12.     public HarmonyReversePatchType? reversePatchType;
  13.     public bool? debug;
  14.     public bool nonVirtualDelegate;
  15. }
复制代码
四:总结

在 特性 搞不定的时候,手工HarmonyMethod编程是一个很好的补充,这几篇我们只关注了 Prefix,毕竟从高级调试的角度看,我们更关注问题代码的 调用栈 ,从而寻找引发故障的元凶。
5.jpg


来源:新程序网络收集,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册