
一:背景
1. 讲故事
开局一张表,故事全靠编,为了能够承上启下,先把参数列表放出来。
参数名 | 说明 |
---|---|
__instance | 访问非静态方法的实例(类似this)。 |
__result | 获取/修改返回值,要想修改用ref。 |
__resultRef | 修改返回引用(方法返回是 ref 返回 )。 |
__state | 在前缀和后缀间传递自定义数据 。 |
___fields | 读写私有字段(三下划线开头,修改需加ref)。 |
__args | 以object[]形式访问所有参数(修改数组即修改参数)。 |
方法参数同名 | 直接映射原参数。 |
__n | __n表示直接访问第n个参数,从 0 开始)。 |
__originalMethod | 获取原方法的MethodBase。 |
__runOriginal | 判断原方法是否被执行。 |
如果说上一篇聊到的参数是无害的,那这篇所聊到的参数就具有破坏性了,会让一些底层方法产生匪夷所思的输出结果。
二:补丁参数解读
1. __result
这个参数可以获取被注入方法的返回值,你可以对他进行查看和修改,为了让例子更有趣一点,我们对DateTime.Now进行注入,让它永远的丢失时分秒,是不是有点像黑客? 哈哈,参考代码如下:
internal class Program { static void Main(string[] args) { var harmony = new Harmony("com.example.patch"); harmony.PatchAll(); var time = DateTime.Now; Console.WriteLine($"当前时间:{time}"); Console.ReadLine(); } } [HarmonyPatch(typeof(DateTime), "Now", MethodType.Getter)] public class DateTimeHook { public static void Postfix(ref DateTime __result) { __result = __result.Date; } }
是不是让人很恼火,明明调的是DateTime.Now,怎么时分秒不见了。。。
2. __args
在 harmony 中有三种方式可以获取原方法的参数,分别为:
- object[] __args 获取,支持读写。
- __n 下标获取,支持读写。
- parameter 同名法,默认只读,写的话要加 ref。
为了让例子更加有趣和黑客,我们对 HttpClient 的底层方法 SendAsync 进行拦截,然后纂改url,指向一个来历不明的网址,参考代码如下:
internal class Program { static async Task Main(string[] args) { // 应用Harmony补丁 var harmony = new Harmony("com.example.httpclient"); harmony.PatchAll(); var url = "https://www.cnblogs.com"; var httpClient = new HttpClient(); Console.WriteLine($"1.request:{url}"); var response = await httpClient.GetAsync(url); var content = await response.Content.ReadAsStringAsync(); Console.WriteLine($"2.response:\n{content.Substring(0, 500)}"); Console.ReadKey(); } } [HarmonyPatch(typeof(HttpClient), "SendAsync", new Type[] { typeof(HttpRequestMessage), typeof(HttpCompletionOption), typeof(CancellationToken) })] class HttpClientPatch { static void Prefix(object[] __args) { HttpRequestMessage request = (HttpRequestMessage)__args[0]; request.RequestUri = new Uri("http://www.baidu.com"); } }
从卦中看,我明明请求的是博客园,怎么给我返回百度的内容,是不是非常诡异。。。
可能有朋友看到了,这里有一个(HttpRequestMessage)__args[0];强转的逻辑,能不能在Prefix(object[] __args)中直接接收 HttpRequestMessage 参数呢?可以的,这这就 harmony 的另外一种同名参数法,也就是参数名一定要和底层的SendAsync方法签名保持一致,截图如下:
修改后的代码如下,是不是非常的清爽。
[HarmonyPatch(typeof(HttpClient), "SendAsync", new Type[] { typeof(HttpRequestMessage), typeof(HttpCompletionOption), typeof(CancellationToken) })] class HttpClientPatch { static void Prefix(HttpRequestMessage request) { request.RequestUri = new Uri("http://www.baidu.com"); } }
可能有些人会遇到这样的情况,比如SendAsync方法的第一个参数是 internel 类型,由于是程序集可访问,所以你无法在另一个程序集的 Prefix 中声明此类型,这时候怎么办呢?可以借助 harmony 提供的__n索引法,下标是从0开始的。修改代码如下:
[HarmonyPatch(typeof(HttpClient), "SendAsync", new Type[] { typeof(HttpRequestMessage), typeof(HttpCompletionOption), typeof(CancellationToken) })] class HttpClientPatch { static void Prefix(object __0) { Type requestType = __0.GetType(); PropertyInfo requestUriProperty = requestType.GetProperty("RequestUri"); Uri newUri = new Uri("http://www.baidu.com"); requestUriProperty.SetValue(__0, newUri); } }
3. ___fields
这个参数也是一个非常简单粗暴的特性,它可以用三下划线___引出当前 this 实例上的私有字段,使用场景可以是这样的,我们知道new Thread默认是没有ThreadName的,这在高级调试中往往有所不便,所以可加这样的一段逻辑:一旦发现无名的ThreadName就给它赋一个默认的名字,参考代码如下:
internal class Program { static void Main(string[] args) { var harmony = new Harmony("com.example.patch"); harmony.PatchAll(); var thread = new Thread(() => { }); thread.Start(); Console.WriteLine($"1.查看线程名:{thread.Name?.ToString()}"); Console.ReadLine(); } } [HarmonyPatch(typeof(Thread), "Name", MethodType.Getter)] public class ThreadStartHook { public static void Prefix(Thread __instance, ref string ____name) { if (string.IsNullOrEmpty(____name)) { ____name = $"Default Threadid:{__instance.ManagedThreadId}"; } } }
三:总结
这篇文章我们聊到的一些参数多多少少都带点黑客性质,建议大家不要乱用,这里声明一下,我所说的一切都是为.NET高级调试训练营服务的,也是给学员们提供的拓展资料。