我认为这是一个真命题:“没有用.NET Reflector反编译并阅读过代码的程序员不是专业的.NET程序员”。.NET Reflector强大的地方就在于可以把IL代码反编译成可读性颇高的高级语言代码,并且能够支持相当多的“模式”,根据这些模式它可以在一定程度上把某些语法糖给还原,甚至可以支持简单的Lambda表达式和LINQ。只可惜,.NET Reflector还是无法做到极致,某些情况下生成的代码还是无法还原到易于理解——yield关键字便是这样一个典型的情况。不过还行,对于不复杂的逻辑,我们可以通过人肉来“整理”个大概。

简单yield方法编译结果分析

yeild的作用是简化枚举器,也就是IEnumerator<T>或IEnumerable<T>的实现。“人肉”反编译的关键在于发现编译器的规律,因此我们先来观察编译器的处理结果。值得注意的是,我们这里所谈的“分析”,都采用的是微软目前的C# 3.0编译器。从理论上来说,这些结果或是规律,都有可能无法运用在Mono和微软之前或今后的C#编译器上。首先我们准备一段使用yield的代码:

static IEnumerator<int> GetSimpleEnumerator()
{Console.WriteLine("Creating Enumerator");yield return 0;yield return 1;yield return 2;Console.WriteLine("Enumerator Created");
}

为了简化问题,我们在这里采用IEnumerator<T>。自动生成的IEnumerable<T>和IEnumerator<T>区别不大,您可以自己观察一下,有机会我会单独讨论和分析其中的区别。经过编译之后再使用.NET Reflector进行反编译,得到的结果是:

private static IEnumerator<int> GetSimpleEnumerator()
{return new <GetSimpleEnumerator>d__0(0);
}[CompilerGenerated]
private sealed class <GetSimpleEnumerator>d__0 : IEnumerator<int>, ...
{// Fieldsprivate int <>1__state;private int <>2__current;// Methods[DebuggerHidden]public <GetSimpleEnumerator>d__0(int <>1__state){this.<>1__state = <>1__state;}private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;Console.WriteLine("Creating Enumerator");this.<>2__current = 0;this.<>1__state = 1;return true;case 1:this.<>1__state = -1;this.<>2__current = 1;this.<>1__state = 2;return true;case 2:this.<>1__state = -1;this.<>2__current = 2;this.<>1__state = 3;return true;case 3:this.<>1__state = -1;Console.WriteLine("Enumerator Created");break;}return false;}...
}

以上便是编译器生成的逻辑,它将yield关键字这个语法糖转化为普通的.NET结构(再次强调,这只是微软目前的C# 3.0编译器所产生的结果)。从中我们可以得出一些结论:

  • 原本GetSimpleEnumerator方法中包含yield的逻辑不复存在,取而代之的是一个由编译器自动生成的IEnumerator类的实例。
  • 原本GetSimpleEnumerator方法中包含yield的逻辑,被编译器自动转化为对应IEnumerator类中的MoveNext方法的逻辑。
  • 编译器将包含yield逻辑转化为一个状态机,并使用自动生成的state字段保存当前状态。
  • 每次调用MoveNext方法时,都通过switch语句判断state的值,直接进入特定的逻辑片断,并指定下一个状态。

因为从yield关键字的作用便是“中断”一个方法的逻辑,使它在下次执行MoveNext方法的时候继续执行。这就意味着自动生成的MoveNext代码必须通过某一个手段来保留上次调用结束之后的“状态”,并根据这个状态决定下次调用的“入口”——这是个典型的状态机的“思路”。由此看来,编译器如此实现,其“设计”意图也是比较直观的,相信您理解起来也不会有太大问题。

较为复杂的yield方法

上一个例子非常简单,因为GetSimpleEnumerator的逻辑非常简单(只有“顺序”,而没有“循环”和“选择”)。此外,这个方法也没有使用局部变量及参数,于是我们这里不妨再准备一个相对复杂的方法:

private static IEnumerator<int> GetComplexEnumerator(int[] array)
{<GetComplexEnumerator>d__2 d__ = new <GetComplexEnumerator>d__2(0);d__.array = array;return d__;
}[CompilerGenerated]
private sealed class <GetComplexEnumerator>d__2 : IEnumerator<int>, ...
{// Fieldsprivate int <>1__state;private int <>2__current;public int <i>5__4;public int <i>5__6;public int <sumEven>5__3;public int <sumOdd>5__5;public int[] array;// Methods[DebuggerHidden]public <GetComplexEnumerator>d__2(int <>1__state){this.<>1__state = <>1__state;}private bool MoveNext(){// 第一部分switch (this.<>1__state){case 0:this.<>1__state = -1;Console.WriteLine("Creating Enumerator");this.<sumEven>5__3 = 0;this.<i>5__4 = 0;goto Label_0094;case 1:this.<>1__state = -1;goto Label_0086;case 2:goto Label_00F4;default:goto Label_0123;}// 第二部分Label_0086:this.<i>5__4++;Label_0094:if (this.<i>5__4 < this.array.Length){if ((this.array[this.<i>5__4] % 2) == 0){this.<sumEven>5__3 += this.array[this.<i>5__4];this.<>2__current = this.<sumEven>5__3;this.<>1__state = 1;return true;}goto Label_0086;}this.<sumOdd>5__5 = 0;this.<i>5__6 = 0;while (this.<i>5__6 < this.array.Length){if ((this.array[this.<i>5__6] % 2) == 0){goto Label_00FB;}this.<sumOdd>5__5 += this.array[this.<i>5__6];this.<>2__current = this.<sumOdd>5__5;this.<>1__state = 2;return true;Label_00F4:this.<>1__state = -1;Label_00FB:this.<i>5__6++;}Console.WriteLine("Enumerator Created.");Label_0123:return false;}...
}

这下MoveNext的逻辑便一下子复杂了很多。我认为,这是由于编译器期望生成体积小的代码,于是它使用了goto来进行自由的跳转。其实从理论上说,把这个方法分为N个阶段之后,便可以让它们完全独立地分开,只不过此时各状态间便会出现许多重复的逻辑。不过,这段代码看似复杂,其实您仔细分析便会发现,它其实也只是将代码拆成了上下两部分(如代码注释所示):

  • 第一部分:状态机的控制逻辑,即根据当前状态进行跳转。
  • 第二部分:主体逻辑,只不过使用goto代替了普通语句中由for/if组成的逻辑,这么做的目的是为了插入Label,可以让第一部分的代码直接跳转到合适的地方——换句话说,由第一部分跳转到的Label便是yield return出现的地方。

从上面的代码中我们还可以看出方法的“参数”及“局部变量”的转化规则:

  • 参数被转化为IEnumerator类的公开字段,命名方式不变,原本的array参数直接变成array字段。
  • 局部变量被转化为IEnumerator类的公开字段,并运用一定的命名规则改名(主要是为了避免和自动生成的current及state字段产生冲突)。对于局部变量localVar,将被转化为<localVar>X__Y的形式。
  • 其他需要自动生成的字段为<>1__state及<>2__current,它们只是进行辅助逻辑,不再赘述。

至此,我们已经掌握了编译器基本的转化规律,可以将其运用到“人肉反编译”的过程中去。

试验:人肉反编译OrderedEnumerable

事实上,.NET框架中的System.Linq.OrderedEnumerable类便是一个包含yield方法的逻辑,使用.NET Reflector得到的相关代码如下:

internal abstract class OrderedEnumerable<TElement> : IOrderedEnumerable<TElement>, ...
{internal IEnumerable<TElement> source;internal abstract EnumerableSorter<TElement> GetEnumerableSorter(EnumerableSorter<TElement> next);public IEnumerator<TElement> GetEnumerator(){<GetEnumerator>d__0<TElement> d__ = new <GetEnumerator>d__0<TElement>(0);d__.<>4__this = (OrderedEnumerable<TElement>) this;return d__;}[CompilerGenerated]private sealed class <GetEnumerator>d__0 : IEnumerator<TElement>, ...{// Fieldsprivate int <>1__state;private TElement <>2__current;public OrderedEnumerable<TElement> <>4__this;public Buffer<TElement> <buffer>5__1;public int <i>5__4;public int[] <map>5__3;public EnumerableSorter<TElement> <sorter>5__2;[DebuggerHidden]public <GetEnumerator>d__0(int <>1__state){this.<>1__state = <>1__state;}private bool MoveNext(){switch (this.<>1__state){case 0:this.<>1__state = -1;this.<buffer>5__1 = new Buffer<TElement>(this.<>4__this.source);if (this.<buffer>5__1.count <= 0){goto Label_00EA;}this.<sorter>5__2 = this.<>4__this.GetEnumerableSorter(null);this.<map>5__3 = this.<sorter>5__2.Sort(this.<buffer>5__1.items, this.<buffer>5__1.count);this.<sorter>5__2 = null;this.<i>5__4 = 0;break;case 1:this.<>1__state = -1;this.<i>5__4++;break;default:goto Label_00EA;}if (this.<i>5__4 < this.<buffer>5__1.count){this.<>2__current = this.<buffer>5__1.items[this.<map>5__3[this.<i>5__4]];this.<>1__state = 1;return true;}Label_00EA:return false;}...}
}

很自然,我们需要“人肉反编译”的便是OrderedEnumerable类的GetEnumerator方法。首先,为了便于理解代码,我们首先还原各名称。既然我们已经知道了局部变量及current/state的命名规则,因此这个工作其实并不困难:

private bool MoveNext()
{switch (__state){case 0:__state = -1;var buffer = new Buffer<TElement>(this.source);if (buffer.count <= 0){goto Label_00EA;}var sorter = this.GetEnumerableSorter(null);var map = sorter.Sort(buffer.items, buffer.count);sorter = null;var i = 0;break;case 1:__state = -1;i++;break;default:goto Label_00EA;
    }if (i < buffer.count){__current = buffer.items[map[i]];__state = 1;return true;}Label_00EA:return false;
}

值得注意的是,在上面的方法中,this是由原来的<>4__this字段还原而来,它表示的是OrderedEnumerable类型(而不是自动生成的IEnumerator类)的实例。此外,其中的局部变量您需要将其理解为“自动在多次MoveNext调用中保持状态的变量”——这和C语言中的静态局部变量有些接近。自然,__state和__current变量都是自动生成用于保存状态的变量,我们姑且保留它们。

接下来,我们将要还原state等于0时的逻辑。因为我们知道,它其实是yield方法中“第一个yield return”之前的逻辑:

private IEnumerator<TElement> GetEnumerator()
{var buffer = new Buffer<TElement>(this.source);if (buffer.count <= 0) yield break;var sorter = this.GetEnumerableSorter(null);var map = sorter.Sort(buffer.items, buffer.count);// 省略sorter = null(为什么?:P)var i = 0;if (i < buffer.count){yield return buffer.items[map[i]];}...
}

我们发现,在buffer.count小于等于0的时候MoveNext直接返回false了,于是在GetEnumerator方法中我们便使用yield break直接退出。在上面的代码中我们已经还原至第一个yield return,那么当调用下一个MoveNext时(即state为1)逻辑又该如何进行呢?我们再“机械”地还原一下:

private IEnumerator<TElement> GetEnumerator()
{...i++;if (i < buffer.count){yield return buffer.items[map[i]];}else{yield break;}...
}

接着,我们会发现代码会不断重复上面这段逻辑,因此我们可以使用一个“死循环”将其包装起来。至此,GetEnumerator便还原成功了:

private IEnumerator<TElement> GetEnumerator()
{var buffer = new Buffer<TElement>(this.source);if (buffer.count <= 0) yield break;var sorter = this.GetEnumerableSorter(null);var map = sorter.Sort(buffer.items, buffer.count);var i = 0;if (i < buffer.count){yield return buffer.items[map[i]];}while (true){i++;if (i < buffer.count){yield return buffer.items[map[i]];}else{yield break;}}
}

不过,又有多少人会写这样的代码呢?的确,这段代码是我们“机械翻译”的结果。不过经过观察,事实上这段代码可以被修改成如下写法:

private IEnumerator<TElement> GetEnumerator()
{var buffer = new Buffer<TElement>(this.source);if (buffer.count <= 0) yield break;var sorter = this.GetEnumerableSorter(null);var map = sorter.Sort(buffer.items, buffer.count);for (var i = 0; i < buffer.count; i++){yield return buffer.items[map[i]];}
}

至此就完美了。最后这步转换我们利用了人脑的优越性,这样“看出”一种优雅的模式也并非难事——不过这也并非只能靠“感觉”,因为我在上面谈到,编译器会尽可能生成紧凑的代码,这意味着它和“源代码”相比不会有太多的重复。但经由我们“机械还原”之后,会发现这样一段代码其实是重复出现的:

if (i < buffer.count)
{yield return buffer.items[map[i]];
}

于是我们便可以朝着“合并代码片断”的方向去思考,得到最终的结果还是有规律可循的。

总结

如果您关注我最近的文章,并且在看到OrderedEnumerable这个类型之后应该会有所察觉:这篇文章只是我在“分析Array和LINQ排序实现”过程中的一个插曲。没错,这是LINQ排序实现的一小部分。OrderedEnumerable利用了yield关键字,这样我们使用.NET反编译之后代码的可读性很差。为此,我便特地研究了一下对yield进行“人肉反编译”的做法。不过在一开始,我原本其实是想仔细分析一下yield相关的“编译规律”,但是我发现在《C# in Depth》一书中已经对这个话题有了非常详尽的描述,只得作罢。之后我又看了这本书网站上公开的样张,感觉非常不错。

事实上,自从ASP.NET 2.0开始,我似乎就没有看过任何一本ASP.NET 2.0/3.0或是C# 2.0/3.0/4.0的书了,因为我认为这些书中的所有内容都可以从MSDN文档,互联网(如博客)以及自己使用、分析的过程中了解到。不过现在,《C# in Depth》似乎让我对此类技术图书的“偏见”有所动摇了——但只此一本而已,估计我还是不会去买这样的书。:)

对了,昨天我向“有关部门”了解到,《C# in Depth》已经由图灵出版社引进,翻译完毕,只等审校和出版了。

转载于:https://my.oschina.net/abcijkxyz/blog/721546

人肉反编译使用yield关键字的方法相关推荐

  1. 反编译使用yield关键字的方法

    我认为这是一个真命题:"没有用.NET Reflector反编译并阅读过代码的程序员不是专业的.NET程序员"..NET Reflector强大的地方就在于可以把IL代码反编译成可 ...

  2. 如何在前端编码时实现人肉双向编译

    如何在前端编码时实现人肉双向编译 React+flux是目前最火的前端解决方案之一,但flux槽点颇多,例如store比较混乱,使用比较繁琐等,于是出现了很多第三方的基于flux优化的架构. 有人统计 ...

  3. Android反编译工具jadx详细使用方法

    反编译也不是什么新鲜的东西了.但是有时候为了保护我们自己项目的代码,我们还是要通过反编译工具来检测一下我们的代码是否是安全的. 本文我们来使用一个开源的工具jadx来实现对apk文件的反编译. 1.下 ...

  4. Android反编译工具jadx详细使用方法以及混淆和加固的对比

    反编译也不是什么新鲜的东西了.但是为了保护我们自己项目的代码,我们还是要通过反编译工具来检测一下我们的代码是否安全. 本文我们来使用一个开源的工具jadx来实现对apk文件的反编译. 1.下载安装ja ...

  5. Android系统反编译FrameWork层虚拟定位方法

    首次发帖,如有不规范的地方还望多多包涵 起因是因为需要做模拟定位的功能,最终是过金融类app的定位 ,root.框架分分钟被砍死 迫于无奈,直接从操作系统入手,反正hook也不过是hook那几个系统里 ...

  6. Java jar 如何防止被反编译?代码写的太烂,害怕被人发现

    欢迎关注方志朋的博客,回复"666"获面试宝典 java作为解释型的语言,其高度抽象的特性意味其很容易被反编译,容易被反编译,自然有防止反编译措施存在.今天就拜读了一篇相关的文章, ...

  7. java反编译工具_JDA Java反编译工具的下载和使用手册

    JDA(javadecompile analysis)是一款以dex为核心的java反编译工具,同时支持apk.dex.jar文件的反编译,支持动态重命名.该软件主要是用来反编译分析代码而不是反编译出 ...

  8. 面试官:如何防止你的 jar 包被反编译?

    点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 每天 14:00 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | Java ...

  9. hex反编译成c语言,IDA Hex-Rays反编译器使用的一些小技巧

    这是什么? 在我的IDA系列中,我会介绍一些我在交互式反汇编程序,IDA Pro中发现的有趣又有用的东西. 我写这篇文章出于两个原因: 大部分有价值的信息都很分散,难以找到.有时候,你不得不靠自己去找 ...

最新文章

  1. 从方法返回Java 8的可选项时的注意事项
  2. 天使玩偶/SJY摆棋子
  3. 如何暴露内网主机到外网
  4. 新零售不简单,当初马云自己都没解释清楚!
  5. Spring Data 开发环境搭建(二)
  6. 在苹果Mac中如何将html网页转成PDF文件?
  7. 全国航空机场分布矢量数据/旅游景点poi/全国港口码头分布/地铁站分布/火车站分布/2020年POI矢量数据
  8. wcf 双向 java_我的WCF之旅 (11): 再谈WCF的双向通讯-基于Http的双向通讯 V.S. 基于TCP的双向通讯...
  9. Oracle sql 分组求四分位 上四分位 下四分位 中位数
  10. iOS 系统视频播放器简单介绍
  11. LeetCode--第25题K个一组翻转链表
  12. 编译原理: Subset Construction 子集构造法(幂集构造)(NFA转DFA)
  13. 婚庆行业发展报告,2021怎么精准引流?
  14. 史上最被低估的神级学科,看完忍不住感慨“它”也太重要了!
  15. 使用 VMware Server 在 Oracle Enterprise Linux 上安装 Oracle RAC 10g
  16. 利用色光三原色调整图片颜色
  17. java 网站服务器ip地址,java服务器ip地址
  18. 标点符号换行 css
  19. java实现裂变数据的营销分析_一张图说尽裂变营销究竟该怎么玩!
  20. 亮剑“互联网+政务服务”,航天信息助力政府“最多跑一次”改革

热门文章

  1. 基于ZigBee的智能家居系统设计
  2. advancedeast认识
  3. 如何优雅地蹭邻居家的wifi
  4. C#操作AD及Exchange Server总结(二)
  5. 福禄克Fluke TiX501 热像仪技术规格
  6. 阿里天池-全球数据智能大赛
  7. 航天中认软测/测试实习生
  8. 【Python 爬虫】(二)使用 Requests 爬取豆瓣短评
  9. MYSQL学习笔记(自用)第一章
  10. [云数据中心] 《云数据中心网络架构与技术》读书笔记 第七章 构建多数据中心网络(1/3)