zoukankan      html  css  js  c++  java
  • 反编译使用yield关键字的方法 转 武胜

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

    原址:http://www.cnblogs.com/JeffreyZhao/archive/2010/01/26/decompile-methods-with-yield-manually.html

    我认为这是一个真命题:“没有用.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>, ...
    {
    // Fields
    private 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>, ...
    {
    // Fields
    private 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>, ...
    {
    // Fields
    private 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》已经由图灵出版社引进,翻译完毕,只等审校和出版了。

  • 相关阅读:
    NTP on FreeBSD 12.1
    Set proxy server on FreeBSD 12.1
    win32 disk imager使用后u盘容量恢复
    How to install Google Chrome Browser on Kali Linux
    Set NTP Service and timezone on Kali Linux
    Set static IP address and DNS on FreeBSD
    github博客标题显示不了可能是标题包含 特殊符号比如 : (冒号)
    server certificate verification failed. CAfile: none CRLfile: none
    删除文件和目录(彻底的)
    如何在Curl中使用Socks5代理
  • 原文地址:https://www.cnblogs.com/zeroone/p/1656442.html
Copyright © 2011-2022 走看看