在IL的一些实验中,我试图将程序集中的callvirt
调用更改为call
方法。基本上,我调用的成员函数有一个继承链。
基本上调用类似于:
((MyDerivedClass)myBaseObject).SomeCall();
或IL
castclass MyDerivedClass // ** 1
call SomeCall() // ** 2
基类将SomeCall
定义为抽象方法,派生类实现它。派生类是密封的。
我知道callvirt
基本上相当于检查对象是否为空,如果它不是使用虚值表调用方法,如果是,则抛出异常。在我的情况下,我知道它从来没有null
,我知道这是我想调用的实现。我理解为什么在这种情况下你通常需要一个callvirt
。
也就是说,因为我知道对象永远不会为空,并且总是派生类型的实例,所以我认为这不是问题:
- 当你考虑数据和类型是分开的,我实际上认为(**1)可以被删除(对象的数据将是相同的)和
- That(**2)可以是一个简单的
call
,因为我们确切地知道要调用哪个成员。不需要查找虚函数表。
在我看来,编译器在某些情况下也可以推断出这是一个相当合理的事情。对于那些感兴趣的人,是的,callvirt
有速度惩罚,尽管它非常小。
。PEVerify告诉我这是错误的。作为一个好孩子,我总是注意到PEVerify告诉我的一切。我在这里漏掉了什么?为什么更改此调用会导致不正确的程序集?
显然创建一个最小的测试用例不是那么简单…到目前为止,我的运气还不太好。
至于问题本身,我可以简单地在一个更大的程序中重现它:
[IL]: Error: [C:tmpemittest.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时,它会告诉你代码没有错误…: - s
我不知道这是怎么回事…
我怀疑这篇博文是相关的。特别是:
一些人认为这通过继承侵犯了隐私。许多代码都是在这样的假设下编写的,即重写虚方法足以保证调用内部的自定义逻辑。直观地说,这是有意义的,c#会让你产生这种安全感,因为它总是以callvirts的形式发出对虚拟方法的调用。
然后:
在Whidbey的后期,一些人认为这有点奇怪,至少我们不希望部分受信任的代码来做这件事。这甚至是可能的,这常常让人们感到惊讶。我们通过引入新的验证规则解决了期望和现实之间的不匹配。
该规则限制了调用方对虚方法进行非虚调用的方式,特别是只允许在调用方的this指针上调用目标方法。这有效地允许对象向上(或向下)调用它自己的类型层次结构,尽管这很奇怪。
换句话说,假设这个更改就是您所说的(听起来像),该规则的存在是为了防止IL违反如何调用虚拟方法的正常期望。
你可能想尝试使SomeCall
方法sealed
在MyDerivedClass
…在这一点上,它不再是虚拟的,因为在MyDerivedClass
类型的引用上调用SomeCall
将总是调用相同的方法…对于peverify来说,这是否足够非虚拟是另一回事:)