65.9K
CodeProject 正在变化。 阅读更多。
Home

破解 C# 2.0 迭代器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.47/5 (8投票s)

2005年4月5日

23分钟阅读

viewsIcon

75482

downloadIcon

501

一项可在 .NET 1.1 上启用 C# 2.0 迭代器的破解。

引言

本文的重点是一项为 .NET Framework 1.1 版本实现 C# 2.0 迭代器而开发的破解。这仅仅是对编译器功能着迷以及纯粹好奇它如何在 Framework 1.1 版本上运行的结果。该破解基于 C# 规范第 22 章中描述的迭代器实现。

然而,在此之前,读者应该非常清楚,该破解不具有任何实用价值。当然,不期望或不建议任何人实际将其用于任何用途,更不用说生产代码,这并非因为它无法完成任务或存在主要的性能问题,而是因为将代码,尤其是生产代码,暴露给一个修改 PE 文件但尚未经过充分测试的工具是不明智的,就像这个工具一样,更何况这个工具仅仅是一个破解!此外,C# 在下一个主要版本 2.0 的编译器中直接提供了此功能,所以为什么还要费心呢。读者已被警告!本文旨在通过展示 C# 2.0 迭代器如何与 .NET Framework 1.1 版本一起使用来娱乐读者。它并非旨在为读者提供任何可用于生产目的的东西。

首先,对于那些不熟悉该功能的读者,将对 C# 2.0 迭代器进行一个非常简短而粗略的介绍。本文假设读者非常熟悉迭代器设计模式以及它如何在 .NET 中实现。具体来说,您应该完全理解迭代器类型接口 IEnumerator 以及迭代器工厂类型接口 IEnumerable,以及 foreach 语句如何使用这两个接口来实现对聚合的通用迭代。

此介绍之后简要概述了该破解用于在目标为 .NET Framework 1.1 版本的 PE 文件中实现迭代器所采取的步骤,这些文件可以使用 .NET 1.1 平台 SDK 附带的 ildasm.exe 工具反汇编为 IL 代码。但是请注意,在使用非 Visual Studio .NET 2003 附带的 C# 和 VB 编译器生成的 PE 文件时,应格外小心。

C# 2.0 迭代器

去阅读 C# 规范第 22 章!开玩笑的,尽管强烈推荐。此外,此 C# 2.0 迭代器介绍仅提供该功能和实现的主要特性。它省略了许多其他非常重要的事实,例如,那些处理异常处理的事实。此外,读者不应惊讶地发现,此迭代器介绍只是简单地复述了 C# 规范第 22 章中的特定内容。最后,尽管如此,读者最好阅读并理解 C# 规范第 22 章,然后跳过本节。

C# 2.0 迭代器只是一个编译器功能,极大地促进了迭代器设计模式的实现。具体来说,该语言获得了通过普通函数定义迭代器类型的能力。这些迭代器函数包含迭代逻辑,并在编译时封装在嵌套的 IEnumerable/IEnumerator 类型中。在运行时,这些嵌套类型的实例用于执行相应迭代器函数表达的迭代逻辑。迭代器函数绝不会直接在其定义的类型中执行。相反,它在编译器创建并嵌套在包含迭代器函数的类型中的 IEnumerator 类型的 MoveNext 方法中运行。

迭代器函数是满足以下主要标准的任何函数,尽管也存在其他标准并在 C# 规范第 22 章中完整记录

  • 该函数的返回类型为 IEnumeratorIEnumerable。因此,该函数可以与 foreach 构造一起使用。
  • 在函数代码块中,单词(而不是关键字)yield 与关键字 return 一起使用,以向调用方生成一个值。
  • 在函数代码块中,单词(而不是关键字)yield 与关键字 break 一起使用,以终止迭代。

一个生成 1 到 5 的值的迭代器函数示例如下

System.Collections.IEnumerable yieldOneToFive()
{
    yield return 1;
    yield return 2;
    yield return 3;
    yield return 4;
    yield return 5;
}

迭代器函数的代码块可以称为 yield 块。当编译器遇到迭代器函数时,它在编译时会执行以下操作(其中包括):

  • 在包含迭代器函数的类型中创建嵌套的 IEnumerable/IEnumerator 类型。此嵌套迭代器类型通过其 IEnumerator.MoveNext 方法执行迭代器函数。
  • 用返回嵌套迭代器类型实例的代码替换迭代器函数体。迭代器函数从不以原始形式实际执行,而是始终返回一个包含原始代码的类实例。

编译器只是完成了程序员在以传统方式实现迭代器设计模式时需要做的工作。此外,迭代器函数还可以用作协程。话虽如此,C# 2.0 迭代器不仅简化了迭代器设计模式的实现,还简化了从使用协程中受益的其他设计模式的实现(Wesner Moise, .NET Undocumented - 迭代器,不仅仅用于迭代)。幸运的是,对于学习经验来说,注意迭代器函数的行为将使那些了解协程的人清楚这一点。其行为可以描述如下,尽管这肯定不是一个详尽的描述;有关详细信息,请参阅 C# 规范第 22 章

  • 每当遇到 yield return 语句时,该语句表达式的值将返回给调用方。最终发生的情况是,yield return 语句出现的点将成为嵌套迭代器的 MoveNext 方法中的点,这些点将迭代器的 Current 属性的值设置为 yield return 语句表达式的值。发生这种情况时,MoveNext 将返回 true 值,以通知调用方迭代中存在下一个元素。
  • 迭代器函数首次调用时,执行从代码块的开头开始。随后的调用中,执行从遇到的最后一个 yield return 语句之后立即开始,而不是从代码块的开头开始。所有局部变量和参数在 yield 块的所有调用中都保持状态,即它们不会超出作用域。这里,协程浮现在脑海中。像协程一样,迭代器函数暴露了一系列协作的子例程,每个子例程都在上一个子例程离开的地方继续执行,所有局部状态都得到维护,并且每个子例程都能够向调用者返回值。然而,应该清楚的是,在底层,CLR 不允许局部变量在方法调用之间保持状态,因为局部变量存储在堆栈上,并且方法在退出时其堆栈被拆除,也不能一个调用从另一个调用离开的地方开始执行。尽管如此,迭代器通过将迭代器函数转换为嵌套的 IEnumerator 类型来克服这些障碍,这些类型充当状态机,每个状态导致在嵌套迭代器的 MoveNext 方法中执行不同的代码分支。此外,这些嵌套类型具有与其封装的迭代器函数的局部变量一一对应的字段,从而给人一种局部状态在方法调用之间持续存在的错觉。然而,在程序员看来,迭代器函数的行为就像协程,无论它们如何实现。
  • 每当遇到 yield break 语句时,迭代器块终止,就像普通函数通过 return 终止一样。MoveNext 返回值 false 以指示迭代结束。

综上所述,上述示例中所示的迭代器函数将导致一个类似但不完全相同的嵌套迭代器类型

class _IEnumerable : System.Collections.IEnumerable, System.Collections.IEnumerator
{
bool System.Collections.IEnumerator.MoveNext()
{
    switch(_state)
    {
        case 0: break;
        case 1: goto state_1;
        case 2: goto state_2;
        case 3: goto state_3;
        case 4: goto state_4;
        default: return false;
    }
    _current = 1;
    _state++;
    return true;
    state_1:
        _current = 2;
        _state++;
        return true;
    state_2:
        _current = 3;
        _state++;
        return true;
    state_3:
        _current = 4;
        _state++;
        return true;
    state_4:
        _current = 5;
        _state++;
        return true;
}
//...the rest of the nested class definition
}

此外,迭代器函数本身将被重写以返回上述嵌套迭代器类型的实例。也许像这样:

System.Collections.IEnumerable yieldOneToFive()
{
    return new _IEnumerable();
}

C# 2.0 迭代器是一个极大地简化迭代器设计模式实现的特性。它消除了显式创建实现 IEnumerator 接口的类型的需要。此外,鉴于它们的行为类似于协程,迭代器函数可以用于实现与迭代无关的其他设计模式,有时解决在其中生成的值无关紧要的问题,重要的是能够让方法相互协作,即相互通信并且在这样做时不会丢失局部状态。

至此,关于 C# 2.0 迭代器的简短、几乎不存在的介绍就结束了。再次强调,强烈建议读者理解 C# 规范第 22 章。接下来将描述一个破解,它使您能够在 .NET Framework 1.1 版本中看到此功能的实际应用。

IteratorsHack

要在 Framework 1.1 版本中看到 C# 2.0 迭代器在实际运行,必须采取以下步骤,至少在处理本文描述的破解时如此

  1. 添加对名为 Iterators 的程序集的引用,该程序集公开两个类型:Iterators.IteratorFunctionAttributeIterators.Yield。本文随附的源代码提供了这样的程序集。
  2. 创建返回类型为 IEnumerable 的函数。尽管 C# 规范规定迭代器函数可以具有 IEnumerableIEnumerator 的返回类型,但此破解要求迭代器函数具有 IEnumerable 的返回类型,它只是 IEnumerator 的一个工厂。
  3. 使用 IteratorFunctionAttribute 装饰上述步骤 2 中定义的每个迭代器函数,此属性由上述步骤 1 中引用的程序集公开。
  4. 在上述步骤 2 中定义的每个迭代器函数中,在需要向调用者生成值时,调用静态方法 Yield.Return,该值是提供给该方法的参数。实际上,C# 2.0 的 yield return 语句由该方法模拟,该方法定义在上述步骤 1 中引用的程序集公开的 Yield 类型中。
  5. 在每个迭代器函数中,使用带有 null 表达式的 return 语句来终止迭代。换句话说,普通的 return 语句用于模拟 C# 2.0 的 yield break 语句。
  6. 编译定义了迭代器函数的程序集。
  7. 运行 IteratorsHack 程序集,为其两个命令行参数提供参数。第一个参数包含要处理的源 PE 文件(即上述步骤 6 的结果)的路径。第二个参数包含目标 PE 文件(即破解创建的 PE 文件)的路径,该文件将迭代器函数封装在嵌套的迭代器类型中,这在半完整程度上符合 C# 规范第 22 章的迭代器实现部分。
  8. 运行上述步骤 7 中破解创建的 PE 文件,并祈祷它能正常工作!

一个破解将处理的迭代器函数示例

[Iterators.IteratorFunction]
System.Collections.IEnumerable yieldOneToFive()
{
    Iterators.Yield.Return(1);
    Iterators.Yield.Return(2);
    Iterators.Yield.Return(3);
    Iterators.Yield.Return(4);
    Iterators.Yield.Return(5);
    return null;
}

在高层次上,该破解执行以下步骤:

  1. 通过运行 ildasm.exe 工具将源 PE 文件反汇编为 IL 文本文件。
  2. 将上述步骤 1 中生成的 IL 文本文件的内容加载到 StringCollection 中。此后,此 StringCollection 将被称为指令流。
  3. 检查上述步骤 2 中加载的指令流,并记录所有迭代器函数。
  4. 处理指令流的头部和定义部分,添加必要的嵌套 IEnumerable/IEnumerator 类型,这些类型封装了上述步骤 3 中捕获的迭代器函数。
  5. 创建一个包含上述步骤 4 结果的 IL 文本文件。
  6. 使用 ilasm.exe 工具将上述步骤 5 中创建的 IL 文本文件汇编成 PE 文件。
  7. 通过 peverify.exe 工具运行上述步骤 6 中创建的 PE 文件,以确保 PE 文件符合类型安全标准,并且没有会导致无效堆栈状态的代码。如果 peverify 工具确定 PE 文件存在问题,则该破解将抛出异常。

请注意,此破解绝非词法分析器,而只是一种粗糙的解析例程,它完全依赖于原生的 string 函数、一些基本的正则表达式以及 ildasm.exe 提供的 IL 格式来完成任务。此外,此破解在 IL 级别操作的原因仅仅是为了避免开发此类词法分析器,因为 IL 指令的原始性质使得与解释 C# 和 VB 等高级语言所涉及的内容相比,解析 IL 微不足道。如果没有一个像样的词法分析器,在 IL 级别将迭代器函数转换为状态机比在 C#/VB 级别更容易。

接下来简要描述该破解执行的部分(但不是全部)处理。具体来说,简要介绍了在将迭代器函数转换为相应的嵌套 IEnumerator 类型过程中对迭代器函数进行的一些 IL 修改,主要关注从迭代器函数到封装该函数的嵌套迭代器的 MoveNext 方法的转换。鉴于此讨论面向 IL,读者最好熟悉 IL 堆栈。

迭代器函数中的实例成员访问

如果迭代器函数是实例方法,则指令流会更新,以便每个参数为零的 ldarg 指令 (ldarg.0) 后面都跟着一个 ldfld 指令。为什么 ldarg.0 指令很重要?嗯,它仅当迭代器函数不是静态方法而是实例方法时才相关,在这种情况下,ldarg.0 指令将当前实例指针推送到堆栈上。迭代器函数最终将在嵌套 IEnumerator 类型的 MoveNext 方法中执行,而不是在定义迭代器函数的类中执行。这个内部类将反过来拥有一个指向外部类实例的字段。添加到指令流中的 ldfld 操作将把这个指针推送到堆栈上,以便对外部类型的成员访问不中断,因为后续指令将对这个后者指针进行操作。例如,以下是迭代器函数中定义的访问实例成员的代码

//this is IL code running within a non static Iterator function
//defined within type Namespace1.Class1 

//push instance (this) pointer onto stack 
ldarg.0 
//push field _i onto stack 
ldfld int32 Namespace1.Class1::_i

迭代器函数被封装在嵌套类型中后,上述 IL 代码将类似于

//this is IL code running within the MoveNext method 
//of type Namespace1.Class1/Enumerable1 which is nested 
//within type Namespace1.Class1 
//and represents an Iterator function 

//push instance pointer onto stack 
//that is, pointer to the nested type instance 
//Namespace1.Class1/Enumerable1 
ldarg.0 
//push field _this onto stack 
//which is a pointer to the outer type instance 
//Namespace1.Class1 
ldfld class Namespace1.Class1/Enumerable1::_this 
//push field _i onto stack 
//which is a field of the outer type 
//Namespace1.Class1 
ldfld int32 Namespace1.Class1::_i

执行此处理的实际代码是

private void processMemberAccess(StringCollection sc, 
                    IteratorMethodInfo imi, string cls)
{
    if(imi.IsStatic)
        return;
    int n = -1;
    while(++n < sc.Count)
    {
        string s = sc[n].Trim();
        int ndx = s.IndexOf(": ");
        if(ndx != -1)
            s = s.Substring(ndx + 1).Trim();
        if(s.StartsWith("ldarg.0"))
        sc.Insert(++n, "ldfld class " + cls + " " + cls + "/" + 
                  imi.ShortEnumerableName + "::" + THIS_FIELD);
    }
}

迭代器函数的局部变量和参数的读/写访问

所有读/写局部变量和参数值的指令都将进行处理。所有局部变量和参数最终将在表示迭代器函数的嵌套类型中提升为字段状态。因此,指令流会更新,以便读取局部变量和参数值的指令会转换为读取相应字段值的指令。具体来说,我们要处理的指令是

  • ldloc:此指令将由提供给指令的参数标识的局部变量的值推送到堆栈上。
  • ldloca:此指令将由提供给指令的参数标识的局部变量的地址推送到堆栈上。
  • ldarg:此指令将方法调用时提供的参数的值推送到堆栈上,该参数由提供给指令的参数标识。正如已经指出的,如果此指令在实例方法中提供了零参数,则当前实例的指针将被推送到堆栈上,而不是提供给方法第一个参数的参数。
  • ldarga:此指令将方法调用时提供的参数的地址推送到堆栈上,该参数由提供给指令的参数标识。

基本上,每当遇到这些指令之一时,都会根据提供给相关指令的参数定位相应的字段。然后,该指令会替换为 ldfldldflda 指令,具体取决于是否需要加载字段的值或地址。最后,字段读取指令前面会有一个 ldarg.0 指令,因为实例成员指令要求在调用之前将指向实例的指针放置在堆栈上。例如

//this is IL code running within a non static Iterator function 
//defined within type Namespace1.Class1 

//push onto stack the value of local variable v 
ldloc v 
//push onto stack the value of the argument supplied to parameter p 
//which is the first parameter in the method’s parameter list 
ldarg 1

迭代器函数被封装在嵌套类型中后,上述 IL 代码将类似于

//this is IL code running within the MoveNext method 
//of type Namespace1.Class1/Enumerable1 which is nested 
//within type Namespace1.Class1 
//and represents an Iterator function 

//push onto stack the value of field _v 
//which corresponds to local variable v 
//first though push the instance pointer 
ldarg.0 
ldfld int32 Namespace1.Class1/Enumerable1::_v 
//push onto stack the value of field _p 
//which corresponds to parameter p 
//first though push the instance pointer 
ldarg.0 
ldfld int32 Namespace1.Class1/Enumerable1::_p

执行此处理的实际代码是

private void processLocalRead(StringCollection sc, IteratorMethodInfo imi)
{
    int n = -1;
    while(++n < sc.Count)
    {
        string s = sc[n].Trim();
        int ndx = s.IndexOf(": ");
        string label = string.Empty;
        if(ndx != -1)
        {
            label = s.Substring(0, ndx + 1);
            s = s.Substring(ndx + 1).Trim();
        }
        FieldInfo fi = null;
        bool loadAddress = false;
        if(Regex.IsMatch(s, @"(?:^ldloc(?:\.| ))"))
            fi = getFieldInfo(s, imi, false);
        else if(s.StartsWith("ldloca"))
        {
            fi = getFieldInfo(s, imi, false);
            loadAddress = true;
        }
        else if(Regex.IsMatch(s, @"(?:^ldarg(?:\.| ))") && 
               (imi.IsStatic || s.IndexOf("ldarg.0") == -1))
            fi = getFieldInfo(s, imi, true);
        else if(s.StartsWith("ldarga"))
        {
            fi = getFieldInfo(s, imi, true);
            loadAddress = true;
        }
        if(fi != null)
        {
            sc.Insert(n++, label + " ldarg.0");
            sc[n] = sc[n].Replace(s, "ldfld" + 
              (loadAddress ? "a" : string.Empty) + " " + fi.Type + " " 
              + imi.EnumerableName + "::" + fi.Name).Replace(label, 
              string.Empty).Trim();
        }
    }
}

一旦局部变量和参数的读取指令被处理,所有写入局部变量和参数的指令也都会被处理,尽管后者的过程不像前者那样直接。有必要确保对局部变量和参数进行操作的写入指令反映在嵌套类型的相应字段中。换句话说,如果局部变量或参数的值在迭代器函数中设置,那么当迭代器函数被封装时,与此局部变量或参数对应的嵌套类型的字段也需要设置其值。具体来说,我们要处理的写入指令是

  • stloc:此指令将栈顶值弹出并将其存储在由提供给指令的参数标识的局部变量中。
  • starg:此指令将栈顶值弹出并将其存储在由提供给指令的参数标识的参数中。

与读取局部变量或参数的情况一样,每当遇到这些指令之一时,都会根据提供给相关指令的参数定位相应的字段。然而,这就是差异所在。读取指令总是被替换;然而,写入指令从不被替换,而是后面跟着额外的指令,这些指令通过 stfld 指令将局部变量或参数的值赋给嵌套类型的相应字段。举例说明

//this is IL code running within a non static Iterator function 
//defined within type Namespace1.Class1 

//store in local variable v the value that is on top of the stack 
stloc v 
//store in parameter p the value that is on top of the stack 
//parameter p is the first parameter of the method’s parameter list 
starg 1

迭代器函数被封装在嵌套类型中后,上述 IL 代码将类似于

//this is IL code running within the MoveNext method 
//of type Namespace1.Class1/Enumerable1 which is nested 
//within type Namespace1.Class1 
//and represents an Iterator function 

//store in local variable v the value that is on top of the stack 
//notice that this instruction has not been replaced 
stloc v 
//now store in field _v that value of variable v 
ldarg.0 
ldloc v 
stfld int32 Namespace1.Class1/Enumerable1::_v 
//store in parameter p the value that is on top of the stack 
//parameter p is the first parameter of the method’s parameter list 
//notice that this instruction has not been replaced 
//HOWEVER, since the MoveNext method does not have a parameter list 
//all parameters of the Iterator function will become local variables 
//of the MoveNext method, in addition to the local variables of the 
//Iterator function, more on this to come 
stloc p 
//now store in field _p the value of variable p 
//which in the Iterator function was actually parameter p 
//more on this to come 
ldarg.0 
ldloc p 
stfld int32 Namespace1.Class1/Enumerable1::_p

那么这里发生了什么?为什么写入局部变量和参数的指令不像读取局部变量或参数的指令那样简单地被替换?为什么迭代器函数的局部变量也必须在封装迭代器函数的嵌套类型的 MoveNext 方法中可用,尽管此嵌套类型将具有与这些局部变量对应的字段?为什么迭代器函数的参数必须转换为嵌套迭代器的 MoveNext 方法的局部变量,尽管此迭代器将具有与这些参数对应的字段?为什么会有这些低效率?

所有这些问题都有一个简单的答案,那就是,到目前为止所描述的无非是一个破解,因此,它选择了解决一个问题的简单方法,而这个问题否则将需要一个更复杂的解决方案。正如前面提到的,实例成员访问,无论是读取还是写入,都需要事先将实例指针推送到堆栈上。字段读取操作期望堆栈上的最顶层值是实例指针,并且它们只期望看到这个。另一方面,字段写入指令 stfld 期望堆栈上的最顶层值是字段将被设置的值,在其下面必须是实例指针。换句话说,stfld 指令从堆栈中弹出两个值,最顶层的值将被赋值给相关字段,第二个值是实例指针。

所以问题在于将 ldarg.0 指令(它将实例指针推送到堆栈上)放置在指令流中的何处。不假思索,人们可能会说将指令放置在 stfld 指令的正上方,就像处理字段读取指令时一样。嗯,显然,这将不起作用,因为这样做会导致实例指针成为堆栈的最顶层值,而实际上它应该是堆栈的倒数第二个值。那么将 ldarg.0 指令放置在 stfld 指令之前的指令上方呢?嗯,这甚至更糟糕,因为根本无法保证在 stfld 指令之前的指令本身不会期望并消耗堆栈中的值。例如,如果 add 指令在 stfld 指令之前,将 ldarg.0 指令放置在 add 指令的正上方将导致 add 指令从堆栈中弹出实例指针和实例指针下面的值并将这两个值相加,这显然不是目的。此外,stfld 指令可能是分支指令的目标,在这种情况下,紧接其前的指令很可能完全不相关。

要完全用 stfld 替换局部写入指令,需要一种算法,该算法不仅要跟踪堆栈状态,这意味着算法必须知道每个 IL 指令如何影响堆栈,而且还要跟踪所有代码路径。当然,到目前为止所描述的破解离这种复杂性还有光年之遥。因此,它只是确保迭代器函数的所有局部变量和参数作为封装迭代器函数的嵌套类型的 MoveNext 方法的局部变量存在。局部变量写入指令保持不变,参数写入指令替换为局部变量写入指令,最后,已赋值的局部变量立即赋值给其相应的字段。

执行此处理的实际代码是

private void processLocalWrite(StringCollection sc, IteratorMethodInfo imi)
{
    int n = -1;
    while(++n < sc.Count)
    {
        string s = sc[n].Trim();
        int ndx = s.IndexOf(": ");
        if(ndx != -1)
            s = s.Substring(ndx + 1).Trim();
        FieldInfo fi = null;
        bool starg = false;
        if(s.StartsWith("stloc"))
            fi = getFieldInfo(s, imi, false);
        else if(s.StartsWith("starg"))
        {
            fi = getFieldInfo(s, imi, true);
            starg = true;
        }
        if(fi == null)
            continue;
        string local = fi.LocalName;
        if(starg)
            sc[n] = sc[n].Replace(s, "stloc " + local);
        sc.Insert(++n, "ldarg.0");
        sc.Insert(++n, "ldloc " + local);
        sc.Insert(++n, "stfld " + fi.Type + " " + 
                  imi.EnumerableName + "::" + fi.Name);
    }
}

从迭代器函数生成值

现在,核心问题已解决,即指令流根据所有 yield return 语句进行更新。每当在迭代器函数中遇到模拟 C# 2.0 yield return 构造的 Yield.Return 方法时,提供的参数将通过 IEnumerator.Current 属性返回给调用者。具体来说:

  1. 调用 Yield.Return 方法的 call 指令被替换为 stloc 指令,该指令将参数的值存储到局部变量中。
  2. 此局部变量的值被赋给迭代器的 _current 字段,此字段当然保存迭代器 Current 属性的值。
  3. true 值存储在保存 MoveNext 方法结果的局部变量中。
  4. 迭代器的 _state 字段的值加一,以便下次调用 MoveNext 时,它可以查询此字段以确定在代码块中的哪个位置恢复执行。
  5. 执行无条件地转移到标记为 _EXIT_ITERATOR_BLOCK 的指令,该指令只是开始退出 MoveNext 方法的过程。
  6. 添加了一个虚拟指令,并将其标记为上述步骤 4 中达到的状态的执行目标。因此,下次调用 MoveNext 时,它将从该指令开始执行。
  7. 必要的指令已添加到 MoveNext 代码块的开头,以便考虑到上述步骤 4 中达到的状态并分支到该状态,下次调用 MoveNext 时。换句话说,状态机相应地更新。

为了说明

//this is IL code running within a non static Iterator function 
//defined within type Namespace1.Class1 

//yield the value 1 to the caller 
ldc.i4.1 
box 
call void [Iterators]Iterators.Yield::Return(object) 
//yield the value 2 to the caller 
ldc.i4.1 
box 
call void [Iterators]Iterators.Yield::Return(object)

迭代器函数被封装在嵌套类型中后,上述 IL 代码将类似于

//this is IL code running within the MoveNext method 
//of type Namespace1.Class1/Enumerable1 which is nested 
//within type Namespace1.Class1 
//and represents an Iterator function 

//branch to state 1 if necessary 
//that is, the state of the Iterator 
//after the first yield is encountered 
ldc.i4 1 
ldarg.0 
ldfld int32 Namespace1.Class1/Enumerable1::_state 
beq _STATE_1 
//otherwise, execution begins here 
//yield the value 1 to the caller 
//store the value in the local variable current 
ldc.i4.1 
box 
stloc current 
//set the _current field to the 
//value of the local variable current 
ldarg.0 
ldloc current 
stfld object Namespace1.Class1/Enumerable1::_current 
//set the local variable result to true 
//this variable holds the value returned by MoveNext 
ldc.i4.1 
stloc result 
//set the _state field to 1 
//so that next time MoveNext is invoked, 
//execution will begin at the instruction labeled STATE_1 
ldarg.0 
ldc.i4 1 
stfld int32 Namespace1.Class1/Enumerable1::_state 
//exit MoveNext 
br _EXIT_ITERATOR_BLOCK 
//execution will begin here when the _state field equals 1 
STATE_1: nop 
//yield the value 2 to the caller in the same manner 
//that the value 1 was yielded, always updating the state machine

执行此处理的实际代码(需要重构)是

private int processMethodYields(StringCollection sc, int index, 
          ref int tryBlockIndex, IteratorMethodInfo imi,
          ref int state, int instrIndex)
{
    bool tryBlock = sc[index == 0 ? 0 : index - 1].Trim().StartsWith(".try");
    bool finallyBlock = 
         sc[index == 0 ? 0 : index - 1].Trim().StartsWith("finally");
    bool validYldBlock = index == 0 || tryBlock;
    string tryBlockLabel = string.Empty;
    int tryInstrIndex = 0;
    if(tryBlock)
    {
        tryBlockLabel = TRY_BLOCK_LABEL + (tryBlockIndex++).ToString();
        sc.Insert(++index, tryBlockLabel + ": nop");
        tryInstrIndex = index + 1;
    }
    else if(finallyBlock)
    {
        int i = index;
        while(!sc[++i].EndsWith(" endfinally"));
        string endFinallyLabel = sc[i].Substring(0, sc[i].IndexOf(":")).Trim();
        sc.Insert(++index, "ldc.i4.1");
        sc.Insert(++index, "ldloc " + imi.MoveNextResultLocal);
        sc.Insert(++index, "beq " + endFinallyLabel);
    }
    while(true)
    {
        string s = sc[++index].Trim();
        if(s == "{")
            index = processMethodYields(sc, index, 
                    ref tryBlockIndex, imi, ref state, instrIndex);
        else if(s.StartsWith("}"))
            return index;
        else if(validYldBlock && Regex.IsMatch(s, 
             @"(?:call +void +\[Iterators\]Iterators\.Yield::Return\(object\))"))
        {
            sc[index] = s.Substring(0, s.IndexOf("call ")) + 
                                    "stloc " + imi.CurrentLocal;
            sc.Insert(++index, "ldarg.0");
            sc.Insert(++index, "ldloc " + imi.CurrentLocal);
            sc.Insert(++index, "stfld object " + 
                      imi.EnumerableName + "::" + CURRENT_FIELD);
            sc.Insert(++index, "ldc.i4.1");
            sc.Insert(++index, "stloc " + imi.MoveNextResultLocal);
            sc.Insert(++index, "ldarg.0");
            sc.Insert(++index, "ldc.i4 " + (++state).ToString());
            sc.Insert(++index, "stfld int32 " + 
                      imi.EnumerableName + "::" + STATE_FIELD);
            sc.Insert(++index, (tryBlock ? "leave" : "br") + 
                                " " + EXIT_ITERATOR_BLOCK_LABEL);
            string stateLabel = STATE_LABEL + state.ToString();
            sc.Insert(++index, stateLabel + ": nop");
            sc.Insert(instrIndex++, "ldc.i4 " + (state).ToString());
            index++;
            tryInstrIndex++;
            sc.Insert(instrIndex++, "ldarg.0");
            index++;
            tryInstrIndex++;
            sc.Insert(instrIndex++, "ldfld int32 " + 
                      imi.EnumerableName + "::" + STATE_FIELD);
            index++;
            tryInstrIndex++;
            sc.Insert(instrIndex++, 
                      "beq " + (tryBlock ? tryBlockLabel : stateLabel));
            index++;
            tryInstrIndex++;
            if(!tryBlock)
                continue;
            sc.Insert(tryInstrIndex++, "ldc.i4 " + (state).ToString());
            index++;
            sc.Insert(tryInstrIndex++, "ldarg.0");
            index++;
            sc.Insert(tryInstrIndex++, "ldfld int32 " + 
                      imi.EnumerableName + "::" + STATE_FIELD);
            index++;
            sc.Insert(tryInstrIndex++, "beq " + stateLabel);
            index++;
        }
    }
}

终止迭代器函数的执行

指令流已更新,所有 ret (return) 指令都替换为一系列执行以下操作的指令:

  1. 弹出堆栈顶部的数值。由于迭代器函数返回 IEnumerable 类型的值,因此在任何遇到 ret 指令的点,都保证堆栈上只有一个值,并且该值要么是 null,要么是 IEnumerable 类型。但是,由于迭代器函数最终会被转换为封装迭代器函数的嵌套迭代器的 MoveNext 方法,而且 MoveNext 方法返回 Boolean 类型的值,因此堆栈上的任何值都会被弹出,因为从返回 Boolean 的函数返回 IEnumerable 类型的值是不合法的。
  2. false 值存储在保存 MoveNext 方法结果的局部变量中。回想一下,除了 yield return 语句之外的任何方式退出迭代器函数都表示迭代器函数不再有值可生成,即已达到迭代的末尾,因此 MoveNext 方法将返回 false 以通知客户端此情况。
  3. 值 -1 存储在迭代器的 _state 字段中,以便后续对 MoveNext 方法的任何调用除了立即退出外不会执行任何操作。
  4. 无条件地分支到标记为 _EXIT_ITERATOR_BLOCK 的指令,该指令只是开始退出 MoveNext 方法的过程。

为了说明

//this is IL code running within a non static Iterator function 
//defined within type Namespace1.Class1 

//exit the method 
//we know that either null 
//or a value of type IEnumerable 
//is the only value on the stack 
ret

迭代器函数被封装在嵌套类型中后,上述 IL 代码将类似于

//this is IL code running within the MoveNext method 
//of type Namespace1.Class1/Enumerable1 which is nested 
//within type Namespace1.Class1 
//and represents an Iterator function 

//pop off whatever value is on the stack, 
//either null or a value of type IEnumerable 
pop 
//store the value false in the local variable 
//that holds the result of the MoveNext method 
ldc.i4.0 
stloc result 
//store the value -1 in the _state field 
ldarg.0 
ldc.i4 -1 
stfld int32 Namespace1.Class1/Enumerable1::_state 
//unconditionally branch to the instruction 
//labeled _EXIT_ITERATOR_BLOCK 
br _EXIT_ITERATOR_BLOCK

执行此处理的实际代码是

private void processMethodReturns(StringCollection sc, IteratorMethodInfo imi)
{
    int n = -1;
    string result = imi.MoveNextResultLocal;
    while(++n < sc.Count)
    {
        string s = sc[n].Trim();
        int ndx = s.IndexOf(": ");
        string label = string.Empty;
        if(ndx != -1)
        {
            label = s.Substring(0, ndx + 1);
            s = s.Substring(ndx + 1).Trim();
        }
        if(s != "ret")
            continue;
        sc.Insert(n++, (label == string.Empty ? label : label + " ") + "pop");
        sc.Insert(n++, "ldc.i4.0");
        sc.Insert(n++, "stloc " + result);
        sc.Insert(n++, "ldarg.0");
        sc.Insert(n++, "ldc.i4 -1");
        sc.Insert(n++, "stfld int32 " + imi.EnumerableName + "::" + STATE_FIELD);
        sc[n] = "br " + EXIT_ITERATOR_BLOCK_LABEL;
    }
}

至此,我们结束了对该破解执行的一些 IL 破解的讨论。

演示

本文的源代码提供了相同演示的两个版本,一个用 C# 编写,另一个用 VB 编写。该演示是一个简单的 Windows 应用程序,通过使用所选目录的内容填充 TreeView 控件来演示递归迭代器的使用。请注意您选择的目录的深度;没有取消按钮!此外,不言而喻,该演示是微不足道的,并且只有在它是经过 IteratorsHack 进程运行的 PE 文件时才能工作。为方便起见,每个版本的演示都在相关演示项目的根文件夹中直接放置了这样的 PE 文件。

C# 版本的递归迭代器函数是

[IteratorFunction]
private IEnumerable getDirectories(DirectoryInfo dir)
{
    foreach(DirectoryInfo dir1 in dir.GetDirectories())
    {
        Yield.Return(dir1);
        foreach(DirectoryInfo dir2 in getDirectories(dir1))
        {
            Yield.Return(dir2);
        }
    }
    return null;
}

VB 版本的递归迭代器函数是

<IteratorFunction()> _
Private Function getDirectories(ByVal dir As DirectoryInfo) As IEnumerable
    For Each dir1 As DirectoryInfo In dir.GetDirectories()
        Yield.Return(dir1)
        For Each dir2 As DirectoryInfo In getDirectories(dir1)
            Yield.Return(dir2)
        Next
    Next
End Function

最终结论

C# 2.0 迭代器是一个强大的编程功能,它简化了迭代器设计模式的实现,尽管迭代器还有许多其他用途,这些用途与迭代本身无关。IteratorsHack 是一个次优的破解,它使得在 Framework 1.1 版本中看到迭代器功能成为可能。这之所以可能,仅仅是因为迭代器是 C# 2.0 编译器的一个功能,而不是 .NET 2.0 Framework (CLR/BCL) 的功能,不像泛型那样。不幸的是,VB 的下一个版本,它针对 .NET Framework 2.0 版本,将不具备迭代器编译器功能,只有 C# 会有。确实,VB 程序员将不得不等待,尽管 VB 比 C# 任何时候都强大 Int32.MaxValue 倍,这是一个普遍、毋庸置疑且显而易见的事实!我真诚地道歉,但我只是忍不住要发表这个廉价的评论。

感谢您花时间阅读这篇文章,希望您喜欢它,就像我喜欢写它一样。如果存在任何不准确之处(因为我不是任何领域的专家,这是完全有可能的),请告知。各位兄弟们,再见,下次再见,当然,如果上帝愿意的话!

参考文献

© . All rights reserved.