OpCodes.Call 与 OpCodes.Callvirt (使用.net 中的动态方法编程备忘录7)

.NET反射性能优化:EmitCreateDelegate实战指南 反射是.NET中实现动态类型操作的基础机制,但其运行时元数据解析带来显著性能开销,常导致高频调用场景下吞吐骤降、延迟飙升。其核心原理在于绕过JIT编译期绑定,转为运行时查表类型检查,技术价值体现在ORM、序列化、DI等框架的通用性支撑上。典型应用场景包括Excel批量导入、JSON反序列化、DTO映射及模型绑定等需动态属性访问的业务模块。本文深入对比IL Emit动态生成Delegate.CreateDelegate两种主流优化路径,结合装箱拆箱处理、泛型类型推导、缓存策略选型等关键细节,提供可直接落地 阅读详情

MSDN中,对 OpCodes.Call 与 OpCodes.Callvirt 的解释,分别为:

 

OpCodes.Call

 

调用由传递的方法说明符指示的方法。

 

堆栈转换行为依次为:

1.将从 arg1 到 argN 的方法参数推送到堆栈上。

2.从堆栈中弹出从 argN 到 arg1 的方法参数;通过这些参数执行方法调用并将控制转移到由方法说明符表示的方法。完成后,被调用方方法生成返回值并将其发送给调用方。

3.将返回值推送到堆栈上。

 

call 指令调用由通过该指令传递的方法说明符指示的方法。方法说明符是元数据标记,它指示将调用的方法和将被传递到该方法的放到堆栈上的参数的数目、类型和顺序以及要使用的调用约定。tail (Tailcall) 前缀指令可以紧靠 call 指令之前,以指定转移控制之前应释放当前方法状态。如果调用将控制转移到比原始方法信任级别更高的方法,则不释放堆栈帧。而是继续无提示执行,就像尚未提供 tail 一样。元数据标记带有用来确定是对静态方法、实例方法、虚方法还是全局函数进行调用所需的足够的信息。在所有这些情况中,目标地址完全从方法说明符确定(与用于调用虚方法的 Callvirt 指令相比,后者的目标地址还取决于 Callvirt 前推送的实例引用的运行时类型。)

将参数按从左到右的顺序放到堆栈上。即,先计算第一个参数并将其放到堆栈上,然后处理第二个参数,接着处理第三个参数,直到将所有需要的参数都按降序放置在堆栈的顶部为止。有三个重要的特殊情况:

1. 对实例方法(或虚方法)的调用必须在任何用户可见的参数前推送该实例引用。实例引用不得是空引用。元数据中带有的签名不在参数列表中为 this 指针包含项;它而是使用位来指示方法是否需要传递 this 指针。

2. 使用 call(而不是 callvirt)调用虚方法是有效的;这指示将使用方法指定的类(而不是从所调用的对象动态指定的类)来解析该方法。

3. 请注意,可以通过 call 或 callvirt 指令调用委托的 Invoke 方法。

如果系统安全机制没有授予调用方对被调用方法的访问权限,则可能引发 SecurityException。在 Microsoft 中间语言 (MSIL) 指令被转换为本机代码时(而不是在运行时),可能进行安全检查。

注意:
对值类型调用 System.Object 的方法时,考虑使用 constrained 前缀和 callvirt 指令,而不是发出 call 指令。因而不需要根据值类型是否重写方法来发送不同 IL,从而避免了潜在的版本问题。对值类型调用接口方法时考虑使用 constrained 前缀,因为实现接口方法的值类型方法可以用 MethodImpl 更改。这些问题在 Constrained 操作码里有更详细的说明。
 

下面的 Emit 方法重载可以使用 call 操作码:

ILGenerator.Emit(OpCode, MethodInfo)

ILGenerator.EmitCall(OpCode, MethodInfo, Type[])

 

OpCodes.Callvirt:

 

对对象调用后期绑定方法,并且将返回值推送到计算堆栈上。

堆栈转换行为依次为:

将对象引用 obj 推送到堆栈上。

将从 arg1 到 argN 的方法参数推送到堆栈上。

从堆栈中弹出从 arg1 到 argN 的方法参数和对象引用 obj;通过这些参数执行方法调用并将控制转移到由方法元数据标记引用的 obj 中的方法。完成后,被调用方方法生成返回值并将其发送给调用方。

将返回值推送到堆栈上。

callvirt 指令对对象调用后期绑定方法。也就是说,基于 obj 的运行时类型而不是在方法指针中可见的编译时类型选择方法。Callvirt 可用于调用虚方法和实例方法。tail 前缀可以紧靠 callvirt 指令之前 (Tailcall),以指定在转移控制前应释放当前堆栈帧。如果调用将控制转移到比原始方法信任级别更高的方法,将不释放堆栈帧。

方法元数据标记提供要调用的方法的名称、类和签名。与 obj 关联的类是属于实例的类。如果该类定义匹配指示的方法名称和签名的非静态方法,则调用该方法。否则按顺序检查此类的基类链中的所有类。如果未找到任何方法,则是错误的。

Callvirt 在调用方法前从计算堆栈中弹出该对象和关联的参数。如果该方法具有返回值,则在方法完成后将该返回值推送到堆栈上。在被调用方,obj 参数被作为参数 0 访问,arg1 被作为参数 1 访问,依此类推。

将参数按从左到右的顺序放到堆栈上。即,先计算第一个参数并将其放到堆栈上,然后处理第二个参数,接着处理第三个参数,直到将所有需要的参数都按降序放置在堆栈的顶部为止。必须在任何用户可见参数前推送实例引用 obj(callvirt 始终需要的)。签名(在元数据标记中携带)不需要在参数列表中为该指针包含项。

请注意,还可以使用 Call 指令调用虚方法。

如果在与 obj 关联的类中或其任何基类中未能找到具有指示名称和签名的非静态方法,则引发 MissingMethodException。此异常通常是在将 Microsoft 中间语言 (MSIL) 指令转换为本机代码时而不是在运行时进行检测。

如果 obj 为空,则引发 NullReferenceException。

如果系统安全机制没有授予调用方对被调用方法的访问权限,则引发 SecurityException。在 CIL 被转换为本机代码时(而不是在运行时),可能进行安全检查。

注意:
对值类型调用 System.Object 的方法时,考虑使用 constrained 前缀和 callvirt 指令。因而不需要根据值类型是否重写方法来发送不同 IL,从而避免了潜在的版本问题。对值类型调用接口方法时考虑使用 constrained 前缀,因为实现接口方法的值类型方法可以用 MethodImpl 更改。这些问题在 Constrained 操作码里有更详细的说明。
 

下面的 Emit 方法重载可以使用 callvirt 操作码:

ILGenerator.Emit(OpCode, MethodInfo)

ILGenerator.EmitCall(OpCode, MethodInfo, Type[])

 

看MSDN,Callvirt是后期绑定,似乎是,此调用可实现多态,即调用基类的virtual方法,如果继承类重写,实际调用时是调用继承类的方法。

 

据此,如果不是一定要调用基类的方法,好像 Callvirt 比 Call 更安全,并能代替 Call。

我是如此想的,也是如此做的。在我的本子上如此使用,测试时没问题,但在服务器上运行,则可能会报错,在其他同事机器上运行,有的机器可正常运行,有的则报错。

 

将报错的动态方法以 Reflector.exe 反编译成 C# 代码,复制到 C# 项目中,编译后,再用 Reflector.exe 查看其 IL 代码,并与动态生成的 IL 比较,发现除了有些调用,C#编译器以 Call 方式调用,而动态代码以 Callvirt 方式调用外,其他没什么差别。

 

最后不得已,将所有调用,都做了类似如下的修改:

 

MethodInfo method = objType.GetMethod(。。。);
if (objType == method.DeclaringType)
 il.Emit(OpCodes.Call, method);
else
 il.Emit(OpCodes.Callvirt, method);

这样处理后,在服务器上运行,终于不再报错。

 

虽然问题解决,但仍有些莫名其妙。

 

Cpp2IL API 深度解析:.NET 逆向工程的可编程化实践 在软件安全、游戏修改和自动化测试领域,.NET 程序集逆向工程是一项关键技术,它涉及将编译后的中间语言(IL)代码还原为可读形式。其核心原理在于解析程序集的元数据原生代码,通过静态分析和模式匹配重建出接近源码的逻辑结构。这项技术的价值在于能够深入分析应用程序的内部实现,为安全审计、算法研究和功能扩展提供基础。在 Unity 游戏分析、自动化安全扫描和自定义工具链构建等场景中,逆向工程尤为关键。本文聚焦于 Cpp2IL API,它通过提供模块化的类库接口,将逆向流程从手动操作升级为可编程服务,允许开发者以编 阅读详情

相关推荐

OPCODES学习网址

这篇教程原文分为8帖,它们的地址分别为:OPCODES手册 http://byhh.net/f/CS/1175690465/opcodes.rar 先看老罗的缤纷天地中OPCODES的解析,写得相当好。老罗的缤纷天地 http://www.luocong.com/(有OPCODES解析)x86机器码对照表http://ref.x86asm.net/index.htmlThe Svin的 OpCode教程(中文版本上传到我资源中去了)Opcode #1  http://www.asmcommunity.net

GhostTigerMan 2585

Emit常用Opcodes指令使用方法(含实例)

本人是从0开始学习Emit的,在学习过程中比较困扰我的就是有很多指令不理解、不会用,查了很多资料最终算是搞明白了,记录下来供大家参考。

xiaouncle的博客 7883

IL中callcallvirt的区别及其对Equal操作的影响

《CLR via C#》中是这样描述它们的:编译器在生成方法时会在方法定义表中写入该方法的记录项,每个记录项中有一组标志指令方法是静态方法、实例方法还是虚方法,如下图:生成IL代码时,编译器会根据这些标志,判断应如何生成IL代码(是使用call还是callvirt)。call (静态/前期绑定)该IL指令可调用静态方法、实例方法和虚方法。用call指令调用静态方法,必须指定方法的定义类型。用cal...

小布丁的读书笔记 2031

CNN-LSTM混合模型在动态多目标优化中的应用

动态多目标优化问题(DMOPs)是智能计算领域的重要研究方向,其目标函数和约束条件会随时间动态变化,导致传统进化算法难以应对。通过结合卷积神经网络(CNN)的空间特征提取能力和双向长短期记忆网络(BiLSTM)的时序预测优势,可以构建高效的动态环境预测模型。这种混合模型能有效捕捉决策变量的空间关联和环境变化的时间依赖关系,为智能交通调度、能源管理等工程实践提供技术支持。实验表明,基于CNN-LSTM的DIP-DMOEA算法在CEC2018测试集上IGD指标提升40.1%,响应时间缩短45%,特别适合处理风电

weixin_30315905的博客 527

.NET Emit 入门教程:第六部分:IL 指令:6:详解 ILGenerator 指令方法方法调用指令

.NET Emit 中,方法调用指令是一种关键的操作,它允许我们在运行时动态地调用各种方法。这些指令提供了一种灵活的方式,可以在程序执行期间创建、修改和调用方法,从而实现了动态代码生成和操作的功能。方法调用指令包括了一系列不同的操作码,每个操作码都代表了一种不同的调用方式,比如调用实例方法、静态方法或委托。通过理解和应用这些方法调用指令,我们可以实现诸如动态代理、AOP(面向切面编程)、方法重写等高级功能,从而扩展了.NET平台的能力和灵活性。

路过秋天 - 理想很美好,现实很残酷,坚持在路上,只因初心仍存 880

探秘C#中的秘密通道:五种引人注目的方法调用内部或私有方法

C# 中,可以使用不同的方法调用内部或私有方法。下面分别介绍通过反射、MethodInfo.CreateDelegate、表达式(树)、动态方法call)、动态方法calli)这五种方法

1057

c#通过Emit方式实现动态代理

之前自己在写一个IOC小轮子的时候,临时想加一个动态代理拦截功能,考虑到实用性方面,使用了Emit动态生成的方式代替RealProxy加反射的实现,网上查找过不少版本,但是都存在一些缺陷,所以决定自己实现一个。 首先了解一下动态代理的原理,在编码过程中,如果对原有代码不想做改动,且对操作前操作后加入一些迭代代码,我们会使用静态代理,也就是新建一个类,持有原实现类的引用,写一个同名方法并在其中调用...

月月鸟先森的博客 5413

方法的三种调用形式

在《可以调用Null的实例方法吗?》一文中,我谈到.NET方法的三种调用形式,现在我们就来着重聊聊这个话题。具体来说,这里所谓的三种方法调用形式对应着三种IL指令:CallCallVirtCalli。一、三个方法调用指令 二、三种方法调用形式 三、虚方法的分发(virtual dispatch) 四、性能差异一、三个方法调用指令虽然C#方法具有静态方法和实例方法之分,但是在IL层面,它们之...

weixin_41330812的博客 317

Emit学习-答疑篇-CallCallvirt的区别

Emit学习-答疑篇-CallCallvirt的区别 之前在Emit的学习过程中,多次碰到了方法的调用,发现有时候是使用Call而有时候是使用Callvirt,一直对这两者的区别不甚了解。然后就查阅了MSDN,MSDN中对这两者的解释为:   l  Call:调用由传递的方法说明符指示的方法;   l  Callvirt:对对象调用后期绑...

weixin_30852451的博客 196

如何快速掌握.NET运行时方法拦截:终极Harmony完整指南

.NET开发中,你是否曾面临需要在不修改原始代码的情况下扩展或修改现有功能的需求?Harmony运行时修补库正是解决这一挑战的完美工具。作为一款强大的.NET和Mono运行时方法拦截库,Harmony让开发者能够动态修改、替换和装饰方法,为游戏Mod开发、应用程序扩展和框架增强提供了无限可能。 ## 项目背景核心价值定位 🎯 Harmony诞生于一个简单而强大的理念:在不触及原始代码的情

gitblog_01130的博客 1127

MSIL实用指南-方法的调用

方法调用指令主要有CallCallvirt。 调用static或sealed修饰的方法,用Call指令。 调用virtual或abstract修饰的方法,用Callvirt指令。 代码实例: ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new Type[] { typeof(in...

weixin_30477293的博客 104

IL,Emit之OpCodes说明(备查)

原文地址:点击打开链接名称说明Add将两个值相加并将结果推送到计算堆栈上。Add_Ovf将两个整数相加,执行溢出检查,并且将结果推送到计算堆栈上。Add_Ovf_Un将两个无符号整数值相加,执行溢出检查,并且将结果推送到计算堆栈上。And计算两个值的按位“”并将结果推送到计算堆栈上。Arglist返回指向当前方法的参数列表的非托管指针。Beq如果两个值相等,则将控制转移到目标指令。Beq_S如果

勿忘初心 2874

如何使用C#.net编译器?

 看到一篇文章,讲解如何利用C#编写.net编译器,觉得很有意思,先贴上来,以后慢慢研究。 Create a Language Compiler for the .NET FrameworkJoel PobarThis article discusses: Language definition The phases of a compiler Th

tanliyoung的专栏 1693

.NET Emit 入门教程:第六部分:IL 指令:5:详解 ILGenerator 指令方法:创建实例指令

.NET Emit 中,使用 ILGenerator 创建实例是一项重要的操作,它允许我们动态生成对象实例和数组实例的代码。通过创建实例指令,我们可以在运行时动态生成并初始化对象,为程序提供更大的灵活性和可扩展性。创建实例指令主要包括 Newobj 指令和 Newarr 指令。Newobj 指令用于创建新的对象实例,而 Newarr 指令则用于创建新的数组实例。这些指令的灵活运用可以帮助我们在运行时动态地生成各种类型的实例,满足不同场景下的需求。

路过秋天 - 理想很美好,现实很残酷,坚持在路上,只因初心仍存 1191

轻松读懂IL

学习一下IL指令,把有用的东西分享给大家。

xiaouncle的博客 4021

.NET Core中Emit的使用

反射允许我们在运行时获取对象的相关信息,创建对象的实例,执行方法。Emit是作为反射的一个比较高级的功能。使用Emit,可以从零开始动态的创建程序集及类。提供程序的灵活性。本文主要介绍.NET Core中Emit的使用

风清扬的专栏 659

解析.net中继承的实质

     在csdn论坛、博客园里都有很多帖子讨论c#中继承语法的问题,大家乐此不疲的解释virtual,override,new,final,接口,类中的继承。各种各样的例子让新手头晕脑胀,这其中还一些地方以讹传讹。比如这篇文章里面竟然说“编译器会顺着继承链往下找,一直找到合适的那个方法体”,在回复里还有人说“这个特征特现了C#编译器对里氏代换原则的支持。也就是:凡是基类适用的地方子类一定适用。

光辉的晨星 4149

[你必须知道的.NET]第十四回:认识IL代码---从开始到现在

本文将介绍以下内容: ·IL代码分析方法 ·IL命令解析 ·.NET学习方法论 1.引言 自从『你必须知道.NET』系列开篇以来,受到大家很多的关注和支持,给予了anytao巨大的鼓励和动力。俱往昔,我发现很多的园友都把目光和焦点注意在如何理解IL代码这个问题上。对我来说,这真是个莫大的好消息,因为很明显我们的思路慢慢的从应用向底...

sinolover的专栏 691
上一篇: 使用.net 中的动态方法编程备忘录6(OpCodes.Ldloc 与OpCodes.Ldloca)
下一篇: 在动态方法中如何尽量减少定义的 LocalBuilder 数量 (使用.net 中的动态方法编程备忘录8)
IsMe
博客等级 码龄24年 4粉丝 36原创
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值