原址: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关键字便是这样一个典型的情况。不过还行,对于不复杂的 逻辑,我们可以通过人肉来“整理”个大概。
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编译器所产生的结果)。从中我们可以得出一些结论:
因为从yield关键字的作用便是“中断”一个方法的逻辑,使它在下次执行MoveNext方法的时候继续执行。这就意味着自动生成的 MoveNext代码必须通过某一个手段来保留上次调用结束之后的“状态”,并根据这个状态决定下次调用的“入口”——这是个典型的状态机的“思路”。由 此看来,编译器如此实现,其“设计”意图也是比较直观的,相信您理解起来也不会有太大问题。
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个阶段之后,便可以让它们完全独立地分开,只不过此时各状态间便会出现许多重复的逻辑。不过,这段代码看似复杂,其实您仔细分析便 会发现,它其实也只是将代码拆成了上下两部分(如代码注释所示):
从上面的代码中我们还可以看出方法的“参数”及“局部变量”的转化规则:
至此,我们已经掌握了编译器基本的转化规律,可以将其运用到“人肉反编译”的过程中去。
事实上,.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》已经由图灵出版社引进,翻译完毕,只等审校和出版了。