用内联汇编调虚函数, 理解VTABLE原理

用内联汇编调虚函数, 理解VTABLE原理
用内联汇编调虚函数, 理解VTABLE原理

虚函数和动态绑定是C++面向对象编程的核心内容之一。要理解C++虚函数的调用本质,就不得不说VPTR和VTABLE。所有拥有虚函数的C++类的大小都比可看到的内容多至少4个字节(如果派生树中存在多继承,就可能多于4个字节),这多出来的4个字节就是VPTR,它位于每个实例的最前方。VPTR的内容就是一个unsigned int的地址,指向一个内存区域,而这个被指向的内存区域就是VTABLE,所谓虚函数地址表。每个拥有虚函数的类都拥有一张VTABLE,里面是一个函数指针数组,每4个字节为一个单位,指向虚函数的入口地址。C++在调用虚函数时,首先要通过这个类的实例内容看到VPTR,从而找到VTABLE,然后根据要调用的虚函数,取相应偏移地址的内容,从而把调用转到这个位置。

以下是我用VC内联汇编编写的一个手工模拟此过程的示例,通过例子中的代码可以清楚的看出虚函数的调用过程。首先定义一个类,它有一个虚函数:


class  Class1
... {
 
int  i;
public :
 Class1(
int  i)  ... this -> =  i; }
 
virtual   void  Print( int  a,  int  b)  ... { printf( " i=%d a=%d b=%d " , i, a, b); }
}
;

如果取这个类的大小,可以看到结果是8而不是4。
下面声明这个类的一个实例,并取得其VTABLE中第一个元素的值:

 Class1 *  pC  =   new  Class1( 1 );
 
int  addr  =   * ( int * )( * ( int * )pC);

注意取值的这一行运用了复杂的强制类型转换。我把它拆开解释一下。首先是取得对象pC的前四个字节的内容,只要把pC转换成int*然后直接取值就行了:
 *(int*)pC

下一步是把取得的这个值当作是一个指针,也就是再进行一次强制类型转换:
 (int*)(*(int*)pC)

最后取这个指针所指内存的内容,也就是VTABLE中第一个函数的地址了:
 *(int*)(*(int*)pC)

取得这个地址以后,下面就用汇编代码来调用这个地址所指的函数:

 __asm
 
... {
  mov ecx, pC;
  push 
3 ;
  push 
2 ;
  call addr;
 }


注意调用类的非静态成员函数时需要先把对应实例的地址放到ECX寄存器中,也就是平常所说的“隐藏参数”了,然后为函数Print压两个参数进栈,根据运行的结果可以明显看出来调用类成员函数时也是从右向左压栈的,最后用call语句调用函数。不难发现调用类成员函数在参数个数确定时也是由被调用者负责弹栈,看来类成员函数也是可以声明为参数个数可变的函数了。

最后,执行程序,得到结果:
i=1 a=2 b=3 

你可能感兴趣的:(用内联汇编调虚函数, 理解VTABLE原理)