在使用IL进行的一些实验中,我尝试将程序集中的callvirt
次调用更改为call
方法。基本上发生的事情是我有一个继承链,其中包含我正在调用的成员函数。
基本上这个电话类似于:
((MyDerivedClass)myBaseObject).SomeCall();
或在IL
castclass MyDerivedClass // ** 1
call SomeCall() // ** 2
基类将SomeCall
定义为抽象方法,派生类实现它。派生类是密封的。
我知道callvirt
基本上等同于检查对象是否为空,如果它没有使用vtable调用方法,如果是,则抛出异常。在我的情况下,我知道它永远不会null
,我知道这是我想要调用的实现。我明白为什么在这种情况下你通常需要callvirt
。
那就是说,因为我知道对象永远不会为null,并且始终是派生类型的实例,我认为这不是问题:
call
,因为我们确切地知道要调用哪个成员。不需要vtable查找。在我看来,编译器在某些情况下也可以推导出一个非常合理的东西。对于那些感兴趣的人,是的,callvirt
会有速度惩罚,虽然它很小。
然而。 PEVerify告诉我这是错的。作为一个好孩子,我总是注意到PEVerify告诉我的内容。那我在这里错过了什么?为什么更改此调用会导致组装错误?
显然创建一个最小的测试用例并不是那么简单......到目前为止,我没有很多运气。
至于问题本身,我可以简单地在更大的程序中重现它:
[IL]: Error: [C:\tmp\emit\test.dll : NubiloSoft.Test::Value][offset 0x00000007] The 'this' parameter to the call must be the calling method's 'this' parameter.
IL代码值:
L_0000: ldarg.0
L_0001: ldfld class NubiloSoft.Test SomeField
L_0006: ldarg.1
L_0007: call instance bool NubiloSoft.Test::Contains(int32)
字段的类型为NubiloSoft.Test
。
至于Contains
,它在基类中是抽象的,在派生类中它被覆盖了。正如您所期望的那样。当我删除'抽象基本方法'+'覆盖'时,PEVerify再次喜欢它。
为了重现这个问题,我做了这个,到目前为止没有运气在最小的测试用例中重现它:
public abstract class FooBase
{
public abstract void MyMethod();
}
// sealed doesn't seem to do anything...
public class FooDerived : FooBase
{
public override void MyMethod()
{
Console.WriteLine("Hello world!");
}
}
public class FooGenerator
{
static void Main(string[] args)
{
Type t = CreateClass();
object o = Activator.CreateInstance(t, new[] { new FooDerived() });
var meth = t.GetMethod("Caller");
meth.Invoke(o, new object[0]);
Console.ReadLine();
}
public static Type CreateClass()
{
// Create assembly
var assemblyName = new AssemblyName("testemit");
var assemblyBuilder =
AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName,
AssemblyBuilderAccess.RunAndSave, @"c:\tmp");
// Create module
var moduleBuilder = assemblyBuilder.DefineDynamicModule("testemit", "test_emit.dll", false);
// Create type : IFoo
var typeBuilder = moduleBuilder.DefineType("TestClass", TypeAttributes.Public, typeof(object));
// Apparently we need a field to trigger the issue???
var field = typeBuilder.DefineField("MyObject", typeof(FooDerived), FieldAttributes.Public);
ConstructorBuilder constructorBuilder = typeBuilder.DefineConstructor(
MethodAttributes.Public | MethodAttributes.HideBySig |
MethodAttributes.SpecialName | MethodAttributes.RTSpecialName,
CallingConventions.HasThis, new Type[] { typeof(FooDerived) });
// Generate the constructor IL.
ILGenerator gen = constructorBuilder.GetILGenerator();
// The constructor calls the constructor of Object
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
// Store the field
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldarg_1);
gen.Emit(OpCodes.Stfld, field);
// Return
gen.Emit(OpCodes.Ret);
// Add the 'Second' method
var mb = typeBuilder.DefineMethod("Caller",
MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual |
MethodAttributes.Final | MethodAttributes.Public, CallingConventions.HasThis,
typeof(void), Type.EmptyTypes);
// Implement
gen = mb.GetILGenerator();
gen.Emit(OpCodes.Ldarg_0);
gen.Emit(OpCodes.Ldfld, field);
gen.Emit(OpCodes.Call, typeof(FooDerived).GetMethod("MyMethod"));
gen.Emit(OpCodes.Ret);
Type result = typeBuilder.CreateType();
assemblyBuilder.Save("testemit.dll");
return result;
}
}
当你运行它并调用peverify时,它会告诉你代码没有bug ...:-S
我不确定这里发生了什么......在我看来它非常相似。
答案 0 :(得分:1)
我怀疑 this blog post是相关的。特别是:
有些人认为这是通过继承侵犯隐私。在假设覆盖虚拟方法足以保证调用内部的自定义逻辑的情况下编写了大量代码。直观地说,这是有道理的,C#会让你陷入这种安全感,因为它总是将调用虚拟方法作为callvirts。
然后:
在Whidbey的晚些时候,一些人认为这非常奇怪,我们至少不希望部分信任的代码这样做。甚至可能让人们感到惊讶。我们通过引入新的验证规则解决了期望与现实之间的不匹配。
规则限制了调用者对虚拟方法进行非虚拟调用的方式,特别是只有在调用者的“this”指针上调用目标方法时才允许它。这有效地允许对象调用(或向下,尽管这很奇怪)它自己的类型层次结构。
换句话说,假设这个改变是你正在谈论的内容(听起来像它),那么规则就是防止IL违反对虚拟方法调用的正常期望。
您可能想尝试在SomeCall
中制作sealed
方法MyDerivedClass
...此时,在调用SomeCall
的意义上它不再是虚拟的在MyDerivedClass
类型的引用上总是调用相同的方法...但是,对于peverify来说,这是否是非虚拟的,这是另一回事:)