• .NET Emit 入门教程:第六部分:IL 指令:7:详解 ILGenerator 指令方法:分支条件指令


    前言:

    经过前面几篇的学习,我们了解到指令的大概分类,如:

    参数加载指令,该加载指令以 Ld 开头,将参数加载到栈中,以便于后续执行操作命令。

    参数存储指令,其指令以 St 开头,将栈中的数据,存储到指定的变量中,以方便后续使用。

    创建实例指令,其指令以 New 开头,用于在运行时动态生成并初始化对象。

    方法调用指令,该指令以 Call 开头,用于在运行时调用其它方法。

    本篇介绍分支条件指令,该指令通常以 Br、或 B、C 开头,用于在运行分支条件时跳转指令。

    分支条件指令介绍:

    分支条件指令是在.NET Emit编程中关键的控制流程工具,用于在IL代码中实现条件判断和控制转移。

    ILGenerator 类提供了一系列方法,用于生成这些分支条件指令,包括条件分支、无条件分支和Switch分支等。

    条件分支指令(如brtrue和brfalse)根据栈顶的布尔值决定是否跳转到目标标签,而无条件分支指令(如br)则总是进行跳转。

    Switch分支指令则用于在多个目标中选择一个跳转。

    通过比较指令(如ceq、cgt和clt),还可以进行数值比较并根据比较结果执行相应的跳转操作。

    这些指令的灵活运用可以实现复杂的控制逻辑,例如条件判断、循环和异常处理等。

    深入理解分支条件指令将帮助开发者更好地掌握.NET Emit编程,提高代码生成的效率和灵活性。

    常用分支条件指令:

    条件跳转指令:

    1. beq:如果两个值相等,则跳转到指定的标签。
    2. bge:如果第一个值大于或等于第二个值,则跳转到指定的标签。
    3. bgt:如果第一个值大于第二个值,则跳转到指定的标签。
    4. ble:如果第一个值小于或等于第二个值,则跳转到指定的标签。
    5. blt:如果第一个值小于第二个值,则跳转到指定的标签.
    6. bne.un:如果两个无符号整数值不相等,则跳转到指定的标签。
    7. brtrue:如果值为 true,则跳转到指定的标签。
    8. brfalse:如果值为 false,则跳转到指定的标签。
    9. brtrue.s:如果值为 true,则跳转到指定的标签(短格式)。
    10. brfalse.s:如果值为 false,则跳转到指定的标签(短格式).

    无条件跳转指令:

    1. br:无条件跳转到指定的标签。
    2. br.s:短格式的无条件跳转到指定的标签。
    3. leave:无条件跳转到 try、filter 或 finally 块的末尾。
    4. leave.s:短格式的无条件跳转到 try、filter 或 finally 块的末尾.

    比较跳转指令:

    1. bgt.un:如果第一个无符号整数值大于第二个值,则跳转到指定的标签。
    2. bge.un:如果第一个无符号整数值大于或等于第二个值,则跳转到指定的标签。
    3. blt.un:如果第一个无符号整数值小于第二个值,则跳转到指定的标签。
    4. ble.un:如果第一个无符号整数值小于或等于第二个值,则跳转到指定的标签.

    其他跳转指令:

    1. switch:根据给定的索引值跳转到不同的标签。
    2. brnull:如果值为 null,则跳转到指定的标签。
    3. brinst:如果对象是类的实例,则跳转到指定的标签。

    这些指令可以帮助控制流程,在特定条件下跳转到指定的标签位置执行相应的代码。

    从以上分类说明可以看出,该指令需要配置标签使用,对于标签的用法,

    如有遗忘,可以回去补一下文章:.NET Emit 入门教程:第六部分:IL 指令:2:详解 ILGenerator 辅助方法

    上面指令按使用方式,只分两种:

    1、条件跳转指令:根据栈顶的数据,及指令的判断条件,来跳转标签。
    
    2、Switch 分支跳转指令:根据给定的索引值,来跳转标签。

    1、条件跳转指令:

    条件分支指令是在IL代码中用于根据条件来执行跳转操作的指令。

    它们可以根据栈顶的(布尔)值来决定是否跳转到目标标签:

    示例指令:Brtrue

    示例指令:Brfalse

    该 true、false 指令,除了 bool 值,还兼容判断了空(引用)和数字(零),这个小细节要注意。 

    示例指令:Br

    示例代码:

    复制代码
    var dynamicMethod = new DynamicMethod("WriteAOrB", typeof(void), new Type[] { typeof(bool) }, typeof(AssMethodIL_Condition));
    
    ILGenerator il = dynamicMethod.GetILGenerator();
    
    var labelEnd = il.DefineLabel();
    var labelFalse = il.DefineLabel();
    
    il.Emit(OpCodes.Ldarg_0);
    
    il.Emit(OpCodes.Brfalse_S, labelFalse);
    
    il.EmitWriteLine("true.");
    il.Emit(OpCodes.Br_S, labelEnd);
    
    
    il.MarkLabel(labelFalse);
    il.EmitWriteLine("false");
    
    
    il.MarkLabel(labelEnd);
    il.Emit(OpCodes.Ret);     // 返回该值
    复制代码

    运行结果:

    说明:

    复制代码
    1、在 truefalse 指令中,通常只会使用其中一个。 
    
    2、在分支条件中,很多时候需要配合 Br 无条件指令跳出分支。
    
    3、在定义标签时,除了定义分支标签,还要定义结束标签,以便 Br 无条件指令的跳出。 

    4、其它条件指令的使用,和bool条件指令的使用是一样的,只需要理解指令的含义即可。
    复制代码

    2、Switch 分支条件指令:

    Switch 分支指令用于在多个目标中选择一个跳转,类似于在高级编程语言中的 switch 或者 case 语句。

    在IL代码中,Switch 指令可以实现根据一个整数值来决定跳转到不同的目标标签。Switch 分支指令的主要指令是 switch,其作用如下:

    • switch: 该指令从标签数组中选择一个目标标签进行跳转。在 IL 代码中,标签数组通常在 switch 指令之前被定义,并且 switch 指令的操作数是标签数组的引用。switch 指令会从操作数指定的标签数组中根据整数值索引来选择目标标签,然后执行跳转操作。

    Switch 分支指令的作用是根据一个整数值来决定跳转到不同的目标标签,这在处理具有多个选择的情况下非常有用,可以使得代码更加简洁和高效。

    注意事项:

    1、该 Switch 指令只是类似 switch 编程,但不等同。 

    2、该 Switch 指令只能根据索引进行指令跳转。

    同时,Switch 指令在IL代码编写起来,会相对复杂一点,特别是 case 一多,写起来可会要你命3000.

    为了区分 Switch 指令和我们编写代码时的指令 Switch case 区别,我们来看以下示例:

    这一次我们反过来,先写 C# 代码,再看它生成的 IL 代码。

    下面给一个示例代码1:

    复制代码
    static void TestString(string c)
    {
        switch(c)
        {
            case "a":
                Console.WriteLine("A");break;
            case "b":
                Console.WriteLine("B");break;
            default:
                Console.WriteLine("C");break;
        }
    }
    复制代码

    反编绎,看生成的 IL 代码:

    复制代码
    .method private hidebysig static 
            void TestString (
                string c
            ) cil managed 
        {
            // Method begins at RVA 0x3808
            // Code size 73 (0x49)
            .maxstack 2
            .locals init (
                [0] string,
                [1] string
            )
    
            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: stloc.1
            IL_0003: ldloc.1
            IL_0004: stloc.0
            IL_0005: ldloc.0
            IL_0006: ldstr "a"
            IL_000b: call bool [mscorlib]System.String::op_Equality(string, string)
            IL_0010: brtrue.s IL_0021
    
            IL_0012: ldloc.0
            IL_0013: ldstr "b"
            IL_0018: call bool [mscorlib]System.String::op_Equality(string, string)
            IL_001d: brtrue.s IL_002e
    
            IL_001f: br.s IL_003b
    
            IL_0021: ldstr "A"
            IL_0026: call void [mscorlib]System.Console::WriteLine(string)
            IL_002b: nop
            IL_002c: br.s IL_0048
    
            IL_002e: ldstr "B"
            IL_0033: call void [mscorlib]System.Console::WriteLine(string)
            IL_0038: nop
            IL_0039: br.s IL_0048
    
            IL_003b: ldstr "C"
            IL_0040: call void [mscorlib]System.Console::WriteLine(string)
            IL_0045: nop
            IL_0046: br.s IL_0048
    
            IL_0048: ret
        } // end of method Program::TestString
    复制代码

    从该生成的 IL 代码中,可以看出并没有 Switch 指令,而是常规调用字符串比较后,用 bool 条件指令进行跳转。

    那是不是用数字类型就会得到 Switch 指令呢?

    再用数字型来一次:

    复制代码
            static void TestInt(int c)
            {
                switch (c)
                {
                    case 1:
                        Console.WriteLine("AAA"); break;
                    case 2:
                        Console.WriteLine("BBB"); break;
                    default:
                        Console.WriteLine("CCC"); break;
                }
            }
    复制代码

    得到的 IL 代码如下:

    复制代码
    .method private hidebysig static 
            void TestInt (
                int32 c
            ) cil managed 
        {
            // Method begins at RVA 0x3860
            // Code size 57 (0x39)
            .maxstack 2
            .locals init (
                [0] int32,
                [1] int32
            )
    
            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: stloc.1
            IL_0003: ldloc.1
            IL_0004: stloc.0
            IL_0005: ldloc.0
            IL_0006: ldc.i4.1
            IL_0007: beq.s IL_0011
    
            IL_0009: br.s IL_000b
    
            IL_000b: ldloc.0
            IL_000c: ldc.i4.2
            IL_000d: beq.s IL_001e
    
            IL_000f: br.s IL_002b
    
            IL_0011: ldstr "AAA"
            IL_0016: call void [mscorlib]System.Console::WriteLine(string)
            IL_001b: nop
            IL_001c: br.s IL_0038
    
            IL_001e: ldstr "BBB"
            IL_0023: call void [mscorlib]System.Console::WriteLine(string)
            IL_0028: nop
            IL_0029: br.s IL_0038
    
            IL_002b: ldstr "CCC"
            IL_0030: call void [mscorlib]System.Console::WriteLine(string)
            IL_0035: nop
            IL_0036: br.s IL_0038
    
            IL_0038: ret
        } // end of method Program::TestInt
    复制代码

    依旧是 br 跳转指令。

    再试一下用枚举呢?

    复制代码
            static void TestEnum(DataAccessKind c)
            {
                switch (c)
                {
                    case  DataAccessKind.Read:
                        Console.WriteLine("AAA"); break;
                    case DataAccessKind.None:
                        Console.WriteLine("BBB"); break;
                    default:
                        Console.WriteLine("CCC"); break;
                }
            }
    复制代码

    得到 IL 代码如下:

    复制代码
    .method private hidebysig static 
            void TestEnum (
                valuetype [System.Data]Microsoft.SqlServer.Server.DataAccessKind c
            ) cil managed 
        {
            // Method begins at RVA 0x38a8
            // Code size 56 (0x38)
            .maxstack 2
            .locals init (
                [0] valuetype [System.Data]Microsoft.SqlServer.Server.DataAccessKind,
                [1] valuetype [System.Data]Microsoft.SqlServer.Server.DataAccessKind
            )
    
            IL_0000: nop
            IL_0001: ldarg.0
            IL_0002: stloc.1
            IL_0003: ldloc.1
            IL_0004: stloc.0
            IL_0005: ldloc.0
            IL_0006: brfalse.s IL_001d
    
            IL_0008: br.s IL_000a
    
            IL_000a: ldloc.0
            IL_000b: ldc.i4.1
            IL_000c: beq.s IL_0010
    
            IL_000e: br.s IL_002a
    
            IL_0010: ldstr "AAA"
            IL_0015: call void [mscorlib]System.Console::WriteLine(string)
            IL_001a: nop
            IL_001b: br.s IL_0037
    
            IL_001d: ldstr "BBB"
            IL_0022: call void [mscorlib]System.Console::WriteLine(string)
            IL_0027: nop
            IL_0028: br.s IL_0037
    
            IL_002a: ldstr "CCC"
            IL_002f: call void [mscorlib]System.Console::WriteLine(string)
            IL_0034: nop
            IL_0035: br.s IL_0037
    
            IL_0037: ret
        } // end of method Program::TestEnum
    复制代码

    还是没有见 Switch 指令。

    可见,编程中的Switch,和 IL 的 Switch 指令是不同的。

    所以很容易陷入一个误区,以为代码用 Switch 写分支,对应的IL就得用 Switch 指令。

    下面演示一个用 Switch 指令的示例:

    复制代码
     var dynamicMethod = new DynamicMethod("WriteAOrB", typeof(void), new Type[] { typeof(int) }, typeof(AssMethodIL_Condition));
    
     ILGenerator il = dynamicMethod.GetILGenerator();
    
     var labelEnd = il.DefineLabel();
    
     Label labelIndex_0 = il.DefineLabel();
     Label labelIndex_1 = il.DefineLabel();
     Label labelIndex_2 = il.DefineLabel();
     //BreakOp None=-1,Null=0,Empty=1,NullOrEmpty=2
     var lables = new Label[] { labelIndex_0, labelIndex_1, labelIndex_2 };//0、1、2
    
     il.Emit(OpCodes.Ldarg_0);
    
     il.Emit(OpCodes.Switch, lables);
    
     il.MarkLabel(labelIndex_0);
     il.EmitWriteLine("0.");
     il.Emit(OpCodes.Br_S, labelEnd);
    
     il.MarkLabel(labelIndex_1);
     il.EmitWriteLine("1.");
     il.Emit(OpCodes.Br_S, labelEnd);
    
     il.MarkLabel(labelIndex_2);
     il.EmitWriteLine("2.");
     il.Emit(OpCodes.Br_S, labelEnd);
    
     il.MarkLabel(labelEnd);
     il.Emit(OpCodes.Ret);     // 返回该值
    
     dynamicMethod.Invoke(null, new object[] { 1 });
    
    
     Console.Read();
    复制代码

    运行结果:

    从上面的示例可以看出,使用 Switch 指令,其实是在 IL 中编写类似于 Switch 语法的条件分支,而不是和C# 语法的 Switch 对应。

    总结:

    本篇介绍了在IL(Intermediate Language)代码中常见的两种指令类型:条件跳转指令和Switch 分支跳转指令。

    条件跳转指令则用于执行数值比较操作,根据比较结果执行相应的跳转操作或将比较结果压入栈中。

    由于其使用方式是一致,因此示例仅展示bool条件指令的使用,没有对其它指令展开示例,但其它条件指令的含义,也是需要仔细了解一下的。

    Switch 分支跳转指令用于根据一个整数值选择不同的目标标签进行跳转,类似于高级编程语言中的 switch 或者 case 语句。

    通过 Switch 指令,可以使代码更加简洁、高效,并提高可读性和可维护性。在处理具有多个选择的情况下特别有用,例如枚举类型或者状态机的处理。

    它们在条件分支指令和循环控制中起着关键作用,通过灵活运用比较指令,可以实现各种复杂的算法和逻辑。

    综上所述,Switch 分支跳转指令和条件跳转指令是IL代码中常用的两种控制流指令,它们在编写和优化IL代码时起着重要作用,能够使代码更加简洁、高效和易于理解。

  • 相关阅读:
    JS对闭包的理解
    一键AI绘画-生成自己想要生成的图片(你懂的)。
    [并发编程]------死肝ReentrantLock源码
    10款录屏软分析与选择使用,只看这篇文章就轻松搞定所有,高清4K无水印录屏,博主UP主轻松选择
    「网络编程」网络层协议_ IP协议学习_及深入理解
    AspectJ切面自定义注解实现参数分组校验——基础概念(2)
    轻松掌握线性代数-万字长文基础知识概览
    用Unity同时开发【微信小游戏】【安卓】【IOS】游戏#5.5.1 窗口管理器
    Excel 数据透视表教程大全之 01 什么是数据透视表 如何创建数据透视表
    PY32F003F18串口printf功能
  • 原文地址:https://www.cnblogs.com/cyq1162/p/18130979