以前没太注意过这个问题,只知道C++代码调用某个虚函数时要到虚函数表里去查找,然后执行特定的函数。这两天跟别人调程序时发现,VC和gcc对这个检索操作的实现方法是不一样的:
早期的gcc(2001年5月前?)使用的是一个比较直观的查表方法。gcc中指向成员函数的指针其实是一个结构,类似于最早期的cfront编译器:
struct{??? short __delta;??? short __index;??? union??? {??????? void * __pfn;??????? short _delta2;??? } _pfn_or_delta2;};
如果指向非虚成员函数,__pfn中就保存了内存中实际的函数指针。如果指向虚成员函数,__pfn无效,__index为该函数在虚函数表中的索引。调用虚函数时,根据虚函数表的地址和索引查到特定函数地址。
后续版本的gcc在不断改进这种虚函数表的检索方法(实际上后续的gcc对C++ ABI的改动比较多),现在的gcc 3.3.3的源码里,gcc/cp/cp-tree.h等文件包含了相关代码。不过现在的实现代码比较复杂,我还没有彻底看明白。
而VC的实现与此大不相同。VC为一棵继承树上的每个虚函数生成一个vcall代码(可以被看成一个小的中介函数),根据每个虚函数在虚函数表中的偏移不同,每个vcall调用时为eax增加的偏移量不同。这实际上是一种快速的查表机制,而且不用传虚函数表的index就可以调用到特定函数了。
`vcall':00401010? mov???????? eax,dword ptr [ecx] 00401012? jmp???????? dword ptr [eax] `vcall':00401020? mov???????? eax,dword ptr [ecx] 00401022? jmp???????? dword ptr [eax+4] `vcall':00401030? mov???????? eax,dword ptr [ecx] 00401032? jmp???????? dword ptr [eax+8] `vcall':00401040? mov???????? eax,dword ptr [ecx] 00401042? jmp???????? dword ptr [eax+0Ch]
调用时,对同名虚函数(无论该函数在属于一个类),使用的都是同一个vcall,但调用不同类的虚函数时,传入vcall的虚函数表起始地址不同(下面C2类是C1类的派生类,f1和f2是虚函数)。
& C1::f1 - offset `vcall' (401010h) & C1::f2 - offset `vcall' (401020h)& C2::f1 - offset `vcall' (401010h) & C2::f2 - offset `vcall' (401020h)