程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 人肉反編譯使用yield關鍵字的方法

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

編輯:關於.NET

我認為這是一個真命題:“沒有用.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》似乎讓我對此類技術圖書的“偏見”有所動搖了——但只此一本 而已,估計我還是不會去買這樣的書。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved