.NET Emit 入门教程:第六部分:IL 指令:3:详解 ILGenerator 指令方法:参数加载指令

前言:

在上一篇中,我们介绍了 ILGenerator 辅助方法。

本篇,将详细介绍指令方法,并详细介绍指令的相关用法。

在接下来的教程,关于IL指令部分,会将指令分为以下几个分类进行讲解:

复制代码
1、参数加载指令:ld 开头的指令,单词为:load argument

2、参数存储指令:st 开头的指令,单词为:store

3、创建实例指令: new 开头的指令。

4、方法调用指令:call 开头的指令。

5、分支条件指令:br 开头的指令,单词为 break

6、类型转换指令:cast 或 conv 开头的指令,单词为:convert

7、运算操作指令:add/sub/mul/div/rem ,加减乘除取余。

8、其它指令

下面开始介绍第一部分,参数加载指令:

参数加载指令:

参数加载指令用于在方法中加载参数到操作数栈中,为后续的操作做准备。

当涉及 CIL(Common Intermediate Language)指令时,以 "ld" 开头的指令通常用于加载数据到操作数栈中。

以下是一些常见的以 "ld" 开头的参数加载指令及其简要说明:

  1. ldarg: 将指定索引位置的参数加载到操作数栈中。用于将方法的参数加载到操作数栈中,以便在方法中进行操作或传递给其他方法。

  2. ldarga: 将指定索引位置的参数的地址加载到操作数栈中。通常用于获取参数的地址,以便在方法中对参数进行引用传递。

  3. ldc_X: 将常量加载到操作数栈中。其中 X 可以是 I(整数)、I4(32 位整数)、I8(64 位整数)、R4(单精度浮点数)、R8(双精度浮点数)、I4_M1(-1 的特殊表示)、I4_0、I4_1、I4_2、I4_3、I4_4、I4_5(特殊整数常量)等。

  4. ldloc: 将指定索引位置的本地变量加载到操作数栈中。用于将方法内部的局部变量加载到操作数栈中,以进行后续的操作或传递给其他方法。

  5. ldloca: 将指定索引位置的本地变量的地址加载到操作数栈中。通常用于获取局部变量的地址,以便在方法中对局部变量进行引用传递。

  6. ldfld: 将对象的字段值加载到操作数栈中。用于加载对象的字段值,以便在方法中进行操作或传递给其他方法。

  7. 其它:。

这些指令提供了丰富的功能,使得在方法内部能够方便地处理参数、常量、本地变量和对象字段值。通过合理使用这些指令,可以实现对数据的灵活操作和处理。

参数加载指令:短格式和长格式

在 IL(Intermediate Language)中,有一些指令支持短格式和长格式的表示(以 "_S" 结尾代表 short 短指令,默认对应无 _S结尾的即代表长格式指令)。

短格式指令通常用于跳转到相对较近的位置,而长格式指令则可以用于跳转到较远的位置。

在实际应用中,由于 IL 的灵活性和可扩展性,编译器会根据需要自动选择合适的指令格式。

这样可以根据具体的跳转距离来选择最有效的指令格式,从而使生成的代码更加高效。

总的来说,短格式和长格式的区别在于其编码的范围,短格式指令编码的范围较小,适用于相对较近的跳转位置,而长格式指令则可以覆盖更大的跳转范围。

这种设计可以使得 IL 代码在执行时更加高效。

1、从方法传参中加载:ldarg 加载参数值

ldarg: 将指定索引位置的参数加载到操作数栈中,该参数为 Load Argument(加载参数)的简写。

该参数的作用:是从指定的方法传参中获得参数值,并将该值加载到操作数栈中。

OpCodes一共提供了5个相关的指令:

复制代码
Ldarg_0:0索引参数。
Ldarg_1:1索引参数
Ldarg_2:2索引参数
Ldarg_3:3索引参数
Ldarg:使用自定义索引

示例代码:

复制代码
ILGenerator il = methodBuilder.GetILGenerator();
il.Emit(OpCodes.Ldarg_1);
il.Emit(OpCodes.Ldarg,2);//这里使用自定义索引
il.Emit(OpCodes.Add);
il.Emit(OpCodes.Ret);

对应生成代码:

在本段示例代码中:

复制代码
Ldarg_0:代表 this 当前对象。

Ldarg_1:代码参数a

Ldarg_2:代码参数b

Ldarg_3:无。

需要注意的是:

复制代码
示例中定义的是实例方法,因此 Ldarg_0 代表 this 对象,而通过 DynamicMethod 创建的方法,默认则是静态方法,Ldarg_0 则会代表参数a。

2、从方法传参中加载:ldarga 加载参数引用地址

ldarga: 将指定索引位置的参数的地址加载到操作数栈中,该参数为 Load Argument Address(加载参数地址)的简写。

该参数的作用:是从指定的方法传参中获得参数的地址,并将该地址加载到操作数栈中。

OpCodes一共提供了2个相关的指令:

复制代码
Ldarga:需要指定索引值。
Ldarga_S:需要指定索引值

需要注意的是,在 C# 中除了操作 ref 或 out 会涉及到引用地址,其它操作操作引用地址(操作指针)都需要在unsafe方法下。

否则强行操作,会出现以下异常,例如:

也可能是以下异常:

3、数字类型值加载:ldc_X 加载参数值

在 Common Intermediate Language(CIL)中,以 "ldc" 开头的操作码指令主要用于将常量加载到操作数栈中。

这些指令在.NET平台的程序集中起着重要的作用,用于处理常量数据的加载和操作。

以下是几种以 "ldc" 开头的常见操作码指令及其分类和用途:

1. ldc_i4 / ldc_i4_s

  • 分类: 整数常量加载指令
  • 用途: 将 32 位整数常量加载到操作数栈中。ldc_i4 指令用于加载常量值介于 -2^31 到 2^31-1 之间的整数,而 ldc_i4_s 则用于加载介于 -128 到 127 之间的整数常量。

2. ldc_i8

  • 分类: 长整数常量加载指令
  • 用途: 将 64 位长整数常量加载到操作数栈中。适用于加载大于 Int32 范围的整数常量。

3. ldc_r4 / ldc_r8

  • 分类: 浮点数常量加载指令
  • 用途: 将单精度浮点数(float)或双精度浮点数(double)常量加载到操作数栈中。ldc.r4 用于加载单精度浮点数常量,而 ldc.r8 用于加载双精度浮点数常量。

4. ldc_i4_m1 / ldc_i4_0 / ldc_i4_1 / ldc_i4_2 / ....../idc_i4_8

  • 分类: 特殊整数常量加载指令
  • 用途: 分别用于加载特定的整数常量值,例如 -1、0、1、2、......、6、7、8 等。

通过合理使用这些以 "ldc" 开头的操作码指令,开发人员可以方便地加载各种类型的常量数据到操作数栈中,为程序的运行和计算提供必要的数据支持。

下面来一个简单的示例:

复制代码
ILGenerator il = dynamicMethod.GetILGenerator();
il.Emit(OpCodes.Ldc_I4, 9999);
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(int) }));
il.Emit(OpCodes.Ret);

运行后输出:

4、局部变量加载:ldloc 参加变量值

ldloc: 将指定索引位置的本地变量加载到操作数栈中。用于将方法内部的局部变量加载到操作数栈中,以进行后续的操作或传递给其他方法。

该参数为:load local 加载本地变量的简写。

该方法需要配合辅助变量使用,这个在上一篇辅助方法中有介绍到,这里重温一下上一篇的辅助方法,定义变量的内容:

5、局部变量加载:ldloca 参数变量值引用地址

ldloca: 将指定索引位置的本地变量的地址加载到操作数栈中。通常用于获取局部变量的地址,以便在方法中对局部变量进行引用传递。

该参数为:load local address 加载本地(变量、引用)地址的简写。

在C#中,操作引用地址,只能是 ref 或 out 两种方式,而操作指针(也是引用地址)则需要在unsafe 方法下才可以。

下面演示一个通过 ldloca 参数返回 out 参数的示例:

复制代码
Dictionary<string, string> dic = new Dictionary<string, string>();
dic.Add("a", "aaa");
var dicType = typeof(Dictionary<string, string>);
MethodInfo getValue = dicType.GetMethod("TryGetValue");


var method = new DynamicMethod("GetValue", typeof(object), new[] { typeof(Dictionary<string, string>), typeof(string) }, typeof(Dictionary<string, string>));//
var il = method.GetILGenerator();
var outText = il.DeclareLocal(typeof(string));

il.Emit(OpCodes.Ldarg_0); // Load the dic object onto the stack
il.Emit(OpCodes.Ldarg_1);//设置字段名。
il.Emit(OpCodes.Ldloca_S, outText);// 使用地址变量来接收: out 值
il.Emit(OpCodes.Callvirt, getValue);//bool a=dic.tryGetValue(...,out value)
il.Emit(OpCodes.Pop);//不需要执行的bool返回值

il.Emit(OpCodes.Ldloc_0);//加载 out 变量的值。
il.Emit(OpCodes.Ret); // Return the value

var func = (Func<Dictionary<string, string>, string, object>)method.CreateDelegate(typeof(Func<Dictionary<string, string>, string, object>));

object result = func(dic, "a");

Console.WriteLine(result);

运行结果:

说明:

复制代码
对于 out 参数,加载的是地址参数,用 loca 地址指令,返回的是值,用 ldloc 值指令。

6、对像成员变量值加载:ldfld 参加载对象成员变量值

ldfld: 将对象的字段值加载到操作数栈中。用于加载对象的字段值,以便在方法中进行操作或传递给其他方法。

该参数为:load filed 加载字段的简写。

下面给出一个示例,读取实例类员变量的值:

复制代码
        private static void DMethod2()
        {
            MyEntity myEntity = new MyEntity() { ID = 111, Name = "hello" };
            FieldInfo idInfo = typeof(MyEntity).GetField("ID");


            var method = new DynamicMethod("GetterFunc", typeof(object), new[] { typeof(object) }, typeof(MyEntity));
            var il = method.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0); // Load the input object onto the stack
            il.Emit(OpCodes.Ldfld, idInfo); // 加载 Id 成员变量的值到堆栈
            if (idInfo.FieldType.IsValueType)
            {
                il.Emit(OpCodes.Box, idInfo.FieldType); // Box the value type
            }

            il.Emit(OpCodes.Ret); // Return the value

            var func = (Func<object, object>)method.CreateDelegate(typeof(Func<object, object>));

            object result = func(myEntity);

            Console.WriteLine(result);

        }
        class MyEntity
        {
            public int ID;
            public string Name;

        }

运行结果:

注意事项:

1、在定义 DynamicMethod 方法时,最好手动指定 Owner 归属,即该动态方法归属哪个类型,否则在运行时可能会报以下错误:

2、在操作实例成员变量(取值或赋值)时,通常需要加载两个参数,第一个是对象值,第二个是对象的成员变量值。

7、其它常用型值加载:ldstr、ldnull

ldstr:将常量字符串值加载到操作数栈中,示例如下:

ldnull:将null值加载到操作数栈中,示例如下:

ldtoken:通常适用于将运行时状态类型加载到操作数栈中,如将 Type 类型值压到栈中。

生成的示例代码:

总结:

本篇教程深入探讨了 ILGenerator 中的参数加载指令,通过详细解释Ldarg、Ldarga、Ldloc和Ldloca 等各种指令的使用,

读者能够清晰地认识到Ld指令用于加载参数或本地变量到堆栈,而St指令用于将值从堆栈存储到参数或本地变量中。

这些指令为动态方法的生成提供了基础,帮助开发者更好地掌握IL代码的生成和调试。

下一篇,将继续介绍存储指令部分。

相关推荐
一个帅气昵称啊3 小时前
.Net通过EFCore和仓储模式实现统一数据权限管控并且相关权限配置动态生成
.net·efcore·仓储模式
helloworddm5 小时前
CalculateGrainDirectoryPartition
服务器·c#·.net
步步为营DotNet6 小时前
深度剖析.NET中HttpClient的请求重试机制:可靠性提升与实践优化
开发语言·php·.net
ChaITSimpleLove6 小时前
使用 .net10 构建 AI 友好的 RSS 订阅机器人
人工智能·.net·mcp·ai bot·rss bot
专注VB编程开发20年7 小时前
vb.net宿主程序通过统一接口直接调用,命名空间要一致
服务器·前端·.net
ChaITSimpleLove21 小时前
基于 .NET Garnet 1.0.91 实现高性能分布式锁(使用 Lua 脚本)
分布式·.net·lua
用户4488466710601 天前
.NET进阶——深入理解线程(2)Thread入门到精通
c#·.net
一个帅气昵称啊1 天前
.Net——AI智能体开发基于 Microsoft Agent Framework 实现第三方聊天历史存储
人工智能·microsoft·.net