前几天在看《.Net框架程序设计》的时候,好像记得书中有提到说每个对象在创建后都会有一个字段保存了一个内存地址,这个内存地址指向对象实际类型的方法表,其中维护了类型每个方法的签名以及他们的入口地址的对应关系。每次调用方法的时候会到这个表中去查找方法入口地址。而根据我之前对于程序的了解,只有虚函数才会需要保存在这个“函数指针表”中,而非虚方法因为在编译时就已经知道了函数入口地址的相对偏移量(因为确切的知道将要调用的是哪个类的哪个方法),所以最终生成的cpu call指令中可以直接得到函数入口地址(模块加载时的基地址加上偏移量就是实际的入口地址)。而虚方法在编译时无法知道具体调用的是哪个方法,所以才会用这个“虚函数指针表”来使系统能够在运行时获得要调用的是哪个方法。
上面提到“虚方法在编译时无法知道具体调用的是哪个方法”。我们看下面的代码:
而当编译器遇到obj.ToString()时,编译器知道ToString是一个虚函数,所以他知道无法确定这里调用的到底是Ojbect类的ToString还是某个继承类重写后的ToString。当然,你也许会觉得这个很明显,因为obj被指向了一个字符串,所以“简单推理”就可以得出这里实际上是String类的ToString函数。但是,上面的代码只是最简单的情况。看下面这个稍微复杂点的:
所以,系统使用了一个叫做“虚函数指针表”的东西来使得程序在运行时可以调用到正确的方法,比如上面的代码,如果GetObject返回的是字符串,则obj.ToString实际上就会调用String.ToString,如果GetObject返回的是ArrayList,那么obj.ToString实际上就会调用ArrayList.ToString方法。
任何一个包含有虚方法的类型都对应有一个虚函数指针表。因为object类本身就定义了虚方法,而C#所有的类型都必须继承自object,因此所有的类都会有一个虚方法指针表。这个虚函数指针表中,保存了每个虚函数的签名和他的入口地址的对应关系。如果一个类型重写了基类的某个虚方法,那么就会在他的虚函数指针表中改写方法的入口地址,否则就把基类中这个虚函数的入口地址复制过来。如下面的代码:
假设object类型的虚函数指针表是这样的(内存地址是假设的,函数入口的偏移量,相对于模块加载时的基地址):
因为MyClass重写了GetHashCode和ToString,所以MyClass的虚函数指针表会是这样的:
AnotherClass又重写了MyClass的ToString,所以AnotherClass的虚函数指针表是这样的:
在创建每个对象的时候,对象都会自动包含一个指针字段,该指针指向其所属类型的虚函数指针表的地址。比如第二段代码中的GetObject方法内,如果返回的是ArrayList,则obj实际上是一个ArrayList类型的实例。因此他的这个指针中保存的是ArrayList类的虚函数指针表地址。
在遇到obj.ToString这样的虚函数调用时,可以这样得到真正的函数方法(假设obj是MyClass类型):
1、从obj的内存中取得虚函数指针表的地址。这个地址保存在对象内存中最开始的位置。
2、因为要访问的是ToString,所以系统知道要到虚函数指针表的第三条中去找ToString入口地址的偏移量。(知道到第三条去取,是因为编译器在编译的时候就知道每个类一共又多少个虚方法,而且是编译器负责填充这个虚函数指针表的,编译器当然知道ToString要从第三条中去取。)
3、根据从虚函数指针表中找到的这个入口地址,调用函数。
为了验证这个,我写了一段简单的代码,并把反编译的的结果做了一个简单的注释。不过我对汇编也不是很熟悉,水平只有“大学的一点印象+今天一天”呵呵。所以如果各位发现有错误还请指出来。
这些是vs2003对Fun函数的反编译结果。源代码在最后面一部分:
对应的C#源代码:
最后,从上面的汇编代码可以看出,非虚方法是无需借助这样的方式就可以调用的。因此可以推测这些非虚方法是不会放在“函数指针表”中去的。
那么至于文章开始提到很多资料说类的非虚方法也会放在方发表中,这个就要期待高手来验证一下了,偶的调试技巧还不够呵呵。
上面提到“虚方法在编译时无法知道具体调用的是哪个方法”。我们看下面的代码:
public void Fun()
{
object obj;
string str;
str = "abc";
str.Clone();
obj = str;
obj.ToString();
}
当编译器遇到str.Clone时,因为这个方法不是一个虚方法,所以编译器知道调用的一定是String.Clone方法。这时候我们就可以将String.Clone方法的入口地址直接生成到cpu指令里面去。{
object obj;
string str;
str = "abc";
str.Clone();
obj = str;
obj.ToString();
}
而当编译器遇到obj.ToString()时,编译器知道ToString是一个虚函数,所以他知道无法确定这里调用的到底是Ojbect类的ToString还是某个继承类重写后的ToString。当然,你也许会觉得这个很明显,因为obj被指向了一个字符串,所以“简单推理”就可以得出这里实际上是String类的ToString函数。但是,上面的代码只是最简单的情况。看下面这个稍微复杂点的:
public void Fun()
{
object obj;
obj = this.GetObject();
obj.ToString();
}
public object GetObject()
{
switch(DateTime.Today.DayOfWeek)
{
case DayOfWeek.Monday:
return "a";
case DayOfWeek.Saturday:
return new System.Collections.ArrayList();
default:
return new System.Data.DataSet();
}
}
如果是这种情况,那编译器又该如何去分析呢?很容易看出,GetObject返回的到底是什么类型,只有执行了之后才知道,而且每次执行的结果还可能不同。因此编译器是无论如何也无法确定返回的是什么类型,自然也就无法确认obj.ToString()到底调用的是哪个类型的ToString方法。{
object obj;
obj = this.GetObject();
obj.ToString();
}
public object GetObject()
{
switch(DateTime.Today.DayOfWeek)
{
case DayOfWeek.Monday:
return "a";
case DayOfWeek.Saturday:
return new System.Collections.ArrayList();
default:
return new System.Data.DataSet();
}
}
所以,系统使用了一个叫做“虚函数指针表”的东西来使得程序在运行时可以调用到正确的方法,比如上面的代码,如果GetObject返回的是字符串,则obj.ToString实际上就会调用String.ToString,如果GetObject返回的是ArrayList,那么obj.ToString实际上就会调用ArrayList.ToString方法。
任何一个包含有虚方法的类型都对应有一个虚函数指针表。因为object类本身就定义了虚方法,而C#所有的类型都必须继承自object,因此所有的类都会有一个虚方法指针表。这个虚函数指针表中,保存了每个虚函数的签名和他的入口地址的对应关系。如果一个类型重写了基类的某个虚方法,那么就会在他的虚函数指针表中改写方法的入口地址,否则就把基类中这个虚函数的入口地址复制过来。如下面的代码:
public class MyClass : Object
{
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return base.ToString();
}
}
public class AnotherClass : MyClass
{
public override string ToString()
{
return base.ToString();
}
}
{
public override int GetHashCode()
{
return base.GetHashCode();
}
public override string ToString()
{
return base.ToString();
}
}
public class AnotherClass : MyClass
{
public override string ToString()
{
return base.ToString();
}
}
假设object类型的虚函数指针表是这样的(内存地址是假设的,函数入口的偏移量,相对于模块加载时的基地址):
Equals | 0x0001 |
GetHashCode | 0x0002 |
ToString | 0x0003 |
Equals | 0x0001 |
GetHashCode | 0x0004 |
ToString | 0x0005 |
Equals | 0x0001 |
GetHashCode | 0x0004 |
ToString | 0x0006 |
在创建每个对象的时候,对象都会自动包含一个指针字段,该指针指向其所属类型的虚函数指针表的地址。比如第二段代码中的GetObject方法内,如果返回的是ArrayList,则obj实际上是一个ArrayList类型的实例。因此他的这个指针中保存的是ArrayList类的虚函数指针表地址。
在遇到obj.ToString这样的虚函数调用时,可以这样得到真正的函数方法(假设obj是MyClass类型):
1、从obj的内存中取得虚函数指针表的地址。这个地址保存在对象内存中最开始的位置。
2、因为要访问的是ToString,所以系统知道要到虚函数指针表的第三条中去找ToString入口地址的偏移量。(知道到第三条去取,是因为编译器在编译的时候就知道每个类一共又多少个虚方法,而且是编译器负责填充这个虚函数指针表的,编译器当然知道ToString要从第三条中去取。)
3、根据从虚函数指针表中找到的这个入口地址,调用函数。
为了验证这个,我写了一段简单的代码,并把反编译的的结果做了一个简单的注释。不过我对汇编也不是很熟悉,水平只有“大学的一点印象+今天一天”呵呵。所以如果各位发现有错误还请指出来。
这些是vs2003对Fun函数的反编译结果。源代码在最后面一部分:
public void Fun()
{
ClassA a = null;
00000000 push ebp
00000001 mov ebp,esp
00000003 sub esp,0Ch //
00000006 push edi
00000007 push esi
00000008 push ebx
00000009 mov dword ptr [ebp-4],ecx //
0000000c xor ebx,ebx // 变量声明时的内存分配。a 存放在 ebx 中
0000000e xor esi,esi // 变量声明时的内存分配。b 存放在 esi 中
00000010 xor ebx,ebx // ebx 清零(a = null)
ClassB b = null;
00000012 xor esi,esi // esi 清零(b = null)
a = new ClassA(1);
00000014 mov ecx,0C55138h //
00000019 call FF9F1F50 //
0000001e mov edi,eax
00000020 mov ecx,edi
00000022 mov edx,1 // 参数
00000027 call dword ptr ds:[00C55170h] // 调用构造方法
0000002d mov ebx,edi // 将构造方法返回的值赋值给a(ebx)
b = new ClassB(1);
0000002f mov ecx,0C55200h
00000034 call FF9F1F50
00000039 mov edi,eax
0000003b mov ecx,edi
0000003d mov edx,1
00000042 call dword ptr ds:[00C55238h] // 调用构造方法
00000048 mov esi,edi // 将构造方法返回的值赋值给b(esi)
a.ToString();
0000004a mov ecx,ebx // 将 a 的地址向 ecx 复制一份(ToString 函数的隐藏参数 this )
0000004c mov eax,dword ptr [ecx] // 从 ecx 指向的内存中复制一个 dword 值(a 的虚函数指针表的地址,放在对象 a 的最前面 4 字节中),放在 eax 中
0000004e call dword ptr [eax+28h] // 调用虚方法。方法的入口地址存放在这里:eax 指向的内存向后偏移 28h 。(eax 指向虚函数指针表,偏移后是 ToString 方法的入口地址)
00000051 nop
b.ToString();
00000052 mov ecx,esi // 将 b 的地址向 ecx 复制一份(ToString 函数的隐藏参数 this )
00000054 mov eax,dword ptr [ecx] // 从 ecx 指向的内存地址中复制一个 dword 值(a 的虚函数指针表的地址),放在 eax 中
00000056 call dword ptr [eax+28h] // 调用虚方法。方法的入口地址存放在这里:eax 指向的内存向后偏移 28h 。(eax 指向虚函数指针表,偏移后是 ToString 方法的入口地址)
00000059 nop
b.Copy();
0000005a mov ecx,esi // 将 b 的地址向 ecx 复制一份(Copy 函数的隐藏参数 this )
0000005c cmp dword ptr [ecx],ecx
0000005e call dword ptr ds:[00C55244h] // 通过直接制定 Copy 方法入口地址,调用 Copy 方法。
00000064 nop
b.Empty();
00000065 mov ecx,esi // 将 b 的地址向 ecx 复制一份(函数的隐藏参数 this )
00000067 cmp dword ptr [ecx],ecx
00000069 call dword ptr ds:[00C55248h] // 调用 Empty 方法。
}
0000006f nop
00000070 pop ebx
00000071 pop esi
00000072 pop edi
00000073 mov esp,ebp
00000075 pop ebp
00000076 ret
{
ClassA a = null;
00000000 push ebp
00000001 mov ebp,esp
00000003 sub esp,0Ch //
00000006 push edi
00000007 push esi
00000008 push ebx
00000009 mov dword ptr [ebp-4],ecx //
0000000c xor ebx,ebx // 变量声明时的内存分配。a 存放在 ebx 中
0000000e xor esi,esi // 变量声明时的内存分配。b 存放在 esi 中
00000010 xor ebx,ebx // ebx 清零(a = null)
ClassB b = null;
00000012 xor esi,esi // esi 清零(b = null)
a = new ClassA(1);
00000014 mov ecx,0C55138h //
00000019 call FF9F1F50 //
0000001e mov edi,eax
00000020 mov ecx,edi
00000022 mov edx,1 // 参数
00000027 call dword ptr ds:[00C55170h] // 调用构造方法
0000002d mov ebx,edi // 将构造方法返回的值赋值给a(ebx)
b = new ClassB(1);
0000002f mov ecx,0C55200h
00000034 call FF9F1F50
00000039 mov edi,eax
0000003b mov ecx,edi
0000003d mov edx,1
00000042 call dword ptr ds:[00C55238h] // 调用构造方法
00000048 mov esi,edi // 将构造方法返回的值赋值给b(esi)
a.ToString();
0000004a mov ecx,ebx // 将 a 的地址向 ecx 复制一份(ToString 函数的隐藏参数 this )
0000004c mov eax,dword ptr [ecx] // 从 ecx 指向的内存中复制一个 dword 值(a 的虚函数指针表的地址,放在对象 a 的最前面 4 字节中),放在 eax 中
0000004e call dword ptr [eax+28h] // 调用虚方法。方法的入口地址存放在这里:eax 指向的内存向后偏移 28h 。(eax 指向虚函数指针表,偏移后是 ToString 方法的入口地址)
00000051 nop
b.ToString();
00000052 mov ecx,esi // 将 b 的地址向 ecx 复制一份(ToString 函数的隐藏参数 this )
00000054 mov eax,dword ptr [ecx] // 从 ecx 指向的内存地址中复制一个 dword 值(a 的虚函数指针表的地址),放在 eax 中
00000056 call dword ptr [eax+28h] // 调用虚方法。方法的入口地址存放在这里:eax 指向的内存向后偏移 28h 。(eax 指向虚函数指针表,偏移后是 ToString 方法的入口地址)
00000059 nop
b.Copy();
0000005a mov ecx,esi // 将 b 的地址向 ecx 复制一份(Copy 函数的隐藏参数 this )
0000005c cmp dword ptr [ecx],ecx
0000005e call dword ptr ds:[00C55244h] // 通过直接制定 Copy 方法入口地址,调用 Copy 方法。
00000064 nop
b.Empty();
00000065 mov ecx,esi // 将 b 的地址向 ecx 复制一份(函数的隐藏参数 this )
00000067 cmp dword ptr [ecx],ecx
00000069 call dword ptr ds:[00C55248h] // 调用 Empty 方法。
}
0000006f nop
00000070 pop ebx
00000071 pop esi
00000072 pop edi
00000073 mov esp,ebp
00000075 pop ebp
00000076 ret
对应的C#源代码:
using System;
namespace ConsoleApp
{
public class Class2
{
public Class2()
{
}
[STAThread]
static void Main(string[] args)
{
Class2 obj = new Class2();
obj.Fun();
}
public void Fun()
{
ClassA a = null;
ClassB b = null;
a = new ClassA(1);
b = new ClassB(1);
a.ToString();
b.ToString();
b.Copy();
b.Empty();
}
}
public class ClassA
{
private int _value;
public ClassA(int value)
{
this._value = value;
}
public override string ToString()
{
return this._value.ToString();
}
}
public class ClassB
{
private int _value;
public ClassB(int value)
{
this._value = value;
}
public override string ToString()
{
return this._value.ToString();
}
public int Value
{
get
{
return this._value;
}
set
{
this._value = value;
}
}
public ClassB Copy()
{
ClassB b = new ClassB(this._value);
return b;
}
public void Empty()
{
}
}
}
namespace ConsoleApp
{
public class Class2
{
public Class2()
{
}
[STAThread]
static void Main(string[] args)
{
Class2 obj = new Class2();
obj.Fun();
}
public void Fun()
{
ClassA a = null;
ClassB b = null;
a = new ClassA(1);
b = new ClassB(1);
a.ToString();
b.ToString();
b.Copy();
b.Empty();
}
}
public class ClassA
{
private int _value;
public ClassA(int value)
{
this._value = value;
}
public override string ToString()
{
return this._value.ToString();
}
}
public class ClassB
{
private int _value;
public ClassB(int value)
{
this._value = value;
}
public override string ToString()
{
return this._value.ToString();
}
public int Value
{
get
{
return this._value;
}
set
{
this._value = value;
}
}
public ClassB Copy()
{
ClassB b = new ClassB(this._value);
return b;
}
public void Empty()
{
}
}
}
最后,从上面的汇编代码可以看出,非虚方法是无需借助这样的方式就可以调用的。因此可以推测这些非虚方法是不会放在“函数指针表”中去的。
那么至于文章开始提到很多资料说类的非虚方法也会放在方发表中,这个就要期待高手来验证一下了,偶的调试技巧还不够呵呵。