一位朋友询问tppeof、GetType()、is、as的问题,在实验的时候顺手就用泛型写的例子。在看Jit后的反汇编时发现了一个问题,clr对泛型参数有些特殊处理。上网查了一下没有找到介绍泛型参数存储的文章因此动手做了一番实验,有了些浅显的理解在此记录下来望看到此文的高手能给予全面的解释。
以前用泛型的时候没想过clr是如何处理泛型参数的,今天查阅了下<<Expert .NET 2.0 IL Assembler>>里面讲解了一个叫做GenericParam Metadata Table的数据结构,不过没能解决我的疑惑。先来看段代码:
class C<T, U, V> where T: class
{
public void Test_Class(V v)
{
Debugger.Break();
T t = v as T;
}
public void Test_Method<X, Y>(X x, Y y)
{
Debugger.Break();
Type type1 = typeof(X);
Type type2 = typeof(Y);
}
}
class Test
{
static void Main()
{
C<Type, string, object> c = new C<Type, string, object>();
string s = "ok";
object o = s;
c.Test_Class(o);
c.Test_Method(s, o);
}
}
我们先执行Test_Class方法,当进入Test_Class方法后部分主要反汇编及注释如下:
T t = v as T;
0000003e mov eax,dword ptr [ebp-3Ch] //1.得到this指针 01E52B30
00000041 mov eax,dword ptr [eax] //2.得到C<T, U, V>的方法表地址 002A3918
00000043 mov eax,dword ptr [eax+20h] //3.得到存储泛型参数的地址 002A3954
00000046 mov eax,dword ptr [eax] //4.保存参数类型的地址 002a3958
00000048 mov eax,dword ptr [eax] //5.得到T参数的真实类型,既方法表地址 6C18172C
0000004a mov dword ptr [ebp-48h],eax
0000004d test dword ptr [ebp-48h],1
00000054 jne 0000005B
00000056 mov ecx,dword ptr [ebp-48h] //将T的方法表地址传给ECX准备call as方法
00000059 jmp 00000061
0000005b mov eax,dword ptr [ebp-48h]
0000005e mov ecx,dword ptr [eax-1]
00000061 mov edx,dword ptr [ebp-40h] //得到参数o
00000064 call 6C0598F3 //调用as方法
汇编的前两句很好理解就是得到this指针后根据托管对象头4字节找方法表地址,第三句是在方法表偏移20h出得到类型泛型参数信息,不过这里要说明一点在msdn上有篇文章《深入探索.NET框架内部了解CLR如何创建运行时对象》里面的有张ethodTable Layout的图解,不过那个图好像是.net1.1的和.net2.0的方法表layout已经不一样了。我通过测试确定了其中一些字节的涵义(希望有高手给我一份完整的解释)如下所示:
00 = Flags
04 = Instance Size
08 = ??
0C = ??
10 = ??
14 = Module addr
18 = Mehtod Table End addr
1C = EEClass addr (泛型类时,这里的含义不明)
20 = GenericParam Info addr
24 = Gobal Interface Map Table addr(?)
28 = ToString()
2C = Equals()
30 = GetHashCode()
34 = Finalize()
…… 类中的虚方法入口地址
…… 非泛型类的构造函数入口地址
…… interface table addr(如果继承了接口)
…… 一个四字节,记录泛型参数个数不知道是否还有其他涵义(如果是泛型类)
…… interface table
…… GenericParam Info(如果是泛型类)
接下来就让我们看看泛型参数信息的内容:
0x002A3954 002a3958 6c18172c(Type) 6c1808ec(string) 6c180508(object) 00000000 00000000
这里比较奇怪的是,头4字节的作用就是跳转到真实数据。好了让我来验证一下(object的方法少就用6c180508来验证):
!dumpmt -md 6c180508
EEClass: 6bf13ef0
Module: 6bf11000
Name: System.Object
mdToken: 02000002 (C:/Windows/assembly/GAC_32/mscorlib/2.0.0.0__b77a5c561934e089/mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 14
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
6c0d6a70 6bf54934 PreJIT System.Object.ToString()
6c0d6a90 6bf5493c PreJIT System.Object.Equals(System.Object)
6c0d6b00 6bf5496c PreJIT System.Object.GetHashCode()
6c1472f0 6bf54990 PreJIT System.Object.Finalize()
6c1336f0 6bf5492c PreJIT System.Object..ctor()
6c091d94 6bf54984 NONE System.Object.GetType()
6c091da4 6bf54998 NONE System.Object.MemberwiseClone()
6c09742c 6bf549a4 PreJIT System.Object.FieldSetter(System.String, System.String,
System.Object)
6c09743c 6bf549b0 PreJIT System.Object.FieldGetter(System.String, System.String,
System.Object ByRef)
6c09744c 6bf549bc PreJIT System.Object.GetFieldInfo(System.String, System.String)
6c091d80 6bf54944 NONE System.Object.InternalEquals(System.Object, System.Object)
6c0d6ab0 6bf54954 PreJIT System.Object.Equals(System.Object, System.Object)
6c0d6ae0 6bf54960 PreJIT System.Object.ReferenceEquals(System.Object, System.Object)
6c091d88 6bf54974 NONE System.Object.InternalGetHashCode(System.Object)
说完了泛型类的参数下面让就来说说泛型方法的参数。我们来执行Test_Method方法,当进入Test_Method方法后部分主要反汇编及注释如下:
X tmp = y as X;
00000044 mov eax,dword ptr [ebp+8] //得到参数,GenericParam Table的地址
00000047 mov eax,dword ptr [eax+0Ch] //偏移0Ch处为泛型参数信息地址
0000004a mov eax,dword ptr [eax] //得到X的方法表地址,此处X是string
上面的代码非常好理解我就不再贴内存数据了。通过上面的分析我们可以得出一个结论:泛型方法是通过参数压栈的方式与泛型参数信息关联的,而类的泛型参数信息是直接保存在MethodTable中的。