反編譯使用yield關鍵字的方法 轉

weixin_34391854發表於2010-01-26

反編譯使用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》已經由圖靈出版社引進,翻譯完畢,只等審校和出版了。

相關文章