今天通过汇编角度在次理解一下虚函数.
工作环境VC6
方法DEBUG 跟踪MEMORY
测试程序代码 虽然是测试程序但希望大家还是养成良好习惯 不要污染命名空间
测试代码:
#include "iostream"
using std::cout;
using std::endl;
class a
{
int m_a;
public:
a(int x):m_a(x) {}
virtual void display()
{
cout<<"a is running !"<
};
class b : public a
{
public:
b(int y):a(y) {}
void display()
{
cout<<"b is running !"<
};
class c : public b
{
public:
c(int z):b(z) {}
void display()
{
cout<<"c is running !"<
};
int main()
{
a ca(1);
b cb(2);
c cc(3);
a * p[3]={ &ca , &cb , &cc };
p[0]->display();
p[1]->display();
p[2]->display();
return 0;
}
输出结果就不用多说了,如果你还不能想到正确结果 那先看一下基础知识在看这篇文章.
先看一下生成的汇编代码
39: int main()
40: {
004011A0 push ebp
004011A1 mov ebp,esp
004011A3 sub esp,64h
004011A6 push ebx
004011A7 push esi
004011A8 push edi
004011A9 lea edi,[ebp-64h]
004011AC mov ecx,19h
004011B1 mov eax,0CCCCCCCCh
004011B6 rep stos dword ptr [edi]
41: a ca(1);
004011B8 push 1
004011BA lea ecx,[ebp-8]
004011BD call @ILT+95(a::a) (00401064)
42: b cb(2);
004011C2 push 2
004011C4 lea ecx,[ebp-10h]
004011C7 call @ILT+10(b::b) (0040100f)
43: c cc(3);
004011CC push 3
004011CE lea ecx,[ebp-18h]
004011D1 call @ILT+105(c::c) (0040106e)
44:
45: a * p[3]={ &ca , &cb , &cc };
004011D6 lea eax,[ebp-8]
004011D9 mov dword ptr [ebp-24h],eax
004011DC lea ecx,[ebp-10h]
004011DF mov dword ptr [ebp-20h],ecx
004011E2 lea edx,[ebp-18h]
004011E5 mov dword ptr [ebp-1Ch],edx
46:
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
48: p[1]->display();
004011FB mov eax,dword ptr [ebp-20h]
004011FE mov edx,dword ptr [eax]
00401200 mov esi,esp
00401202 mov ecx,dword ptr [ebp-20h]
00401205 call dword ptr [edx]
00401207 cmp esi,esp
00401209 call __chkesp (004092c0)
49: p[2]->display();
0040120E mov eax,dword ptr [ebp-1Ch]
00401211 mov edx,dword ptr [eax]
00401213 mov esi,esp
00401215 mov ecx,dword ptr [ebp-1Ch]
00401218 call dword ptr [edx]
0040121A cmp esi,esp
0040121C call __chkesp (004092c0)
50:
51: return 0;
00401221 xor eax,eax
我们只需要看一下重要的代码就可以了
41: a ca(1);
004011B8 push 1
004011BA lea ecx,[ebp-8]
004011BD call @ILT+95(a::a) (00401064)
构造A类对象,先压栈 常量1 作为参数.
将栈中的一个地址给ECX ,此时ECX 要作为该对象的THIS 指针.现在的这块内存是全CC CC CC CC....
调用A类的构造函数.因为不是VIRTUAL FNCTION 所以是CALL一个常量地址,到一个跳表
00401064 jmp a::a (00401260)
进入到A类的构造函数.
9: a(int x):m_a(x) {}
00401260 push ebp
00401261 mov ebp,esp
00401263 sub esp,44h
00401266 push ebx
00401267 push esi
00401268 push edi
00401269 push ecx
0040126A lea edi,[ebp-44h]
0040126D mov ecx,11h
00401272 mov eax,0CCCCCCCCh
00401277 rep stos dword ptr [edi]
00401279 pop ecx
0040127A mov dword ptr [ebp-4],ecx
0040127D mov eax,dword ptr [ebp-4]
00401280 mov ecx,dword ptr [ebp+8]
00401283 mov dword ptr [eax+4],ecx
00401286 mov edx,dword ptr [ebp-4]
00401289 mov dword ptr [edx],offset a::`vftable' (0043201c)
0040128F mov eax,dword ptr [ebp-4]
00401292 pop edi
00401293 pop esi
00401294 pop ebx
00401295 mov esp,ebp
00401297 pop ebp
00401298 ret 4
我们只看最重要的几行就可以了
0040127A mov dword ptr [ebp-4],ecx
0040127D mov eax,dword ptr [ebp-4]
00401280 mov ecx,dword ptr [ebp+8]
00401283 mov dword ptr [eax+4],ecx
00401286 mov edx,dword ptr [ebp-4]
00401289 mov dword ptr [edx],offset a::`vftable' (0043201c)
0040128F mov eax,dword ptr [ebp-4]
此时ECX是该对象的THIS 指针.把THIS指针给一个变量(栈来实现) . 在给EAX 所以此时EAX也是THIS指针.
mov ecx,dword ptr [ebp+8]
实现了将刚才压入栈中的参数1 给ECX.
mov dword ptr [eax+4],ecx
该值在给以THISI 指针开始+4到+8的4个字节赋值(因为是 INT型 所以为4个字节.) 因为A类有虚函数,我们知道 编译器会给A类对象的前4个字节赋予类A的VTABLE的地址,这4个字节就是我们常说的VPTR 它指向虚表.所以VPTR后的4个字节才是存放对象的唯一一个数据成员(INT).
此时该对象的内存为CC CC CC CC 01 00 00 00 VPTR还没赋值
mov edx,dword ptr [ebp-4]
THIS指针给EDX
mov dword ptr [edx],offset a::`vftable' (0043201c)
把类A虚表地址 给VPTR 也就是THIS指针开始的4个字节
eax,dword ptr [ebp-4] 还是把THIS指针给EAX 我认为这是一条废语句
此时该对象的内存是
0012FF78 1C 20 43 00 01 00 00 00 虚表指针 数据成员
类B
42: b cb(2);
004011C2 push 2
004011C4 lea ecx,[ebp-10h]
004011C7 call @ILT+10(b::b) (0040100f)
同样压栈2 作为参数. 并找到下一个8字节空间作为类B对象使用的内存
并进入跳表0040100F jmp b::b (00401310)
进入构造函数
20: b(int y):a(y) {}
00401310 push ebp
00401311 mov ebp,esp
00401313 sub esp,44h
00401316 push ebx
00401317 push esi
00401318 push edi
00401319 push ecx
0040131A lea edi,[ebp-44h]
0040131D mov ecx,11h
00401322 mov eax,0CCCCCCCCh
00401327 rep stos dword ptr [edi]
00401329 pop ecx
0040132A mov dword ptr [ebp-4],ecx
0040132D mov eax,dword ptr [ebp+8]
00401330 push eax
00401331 mov ecx,dword ptr [ebp-4]
00401334 call @ILT+95(a::a) (00401064)
00401339 mov ecx,dword ptr [ebp-4]
0040133C mov dword ptr [ecx],offset b::`vftable' (00432034)
00401342 mov eax,dword ptr [ebp-4]
00401345 pop edi
00401346 pop esi
00401347 pop ebx
00401348 add esp,44h
0040134B cmp ebp,esp
0040134D call __chkesp (004092c0)
00401352 mov esp,ebp
00401354 pop ebp
00401355 ret 4
主要来看这段
0040132A mov dword ptr [ebp-4],ecx
0040132D mov eax,dword ptr [ebp+8]
00401330 push eax
00401331 mov ecx,dword ptr [ebp-4]
00401334 call @ILT+95(a::a) (00401064)
00401339 mov ecx,dword ptr [ebp-4]
0040133C mov dword ptr [ecx],offset b::`vftable' (00432034)
00401342 mov eax,dword ptr [ebp-4]
ECX还是该对象的THIS指针.给一个变量
把参数2给EAX,把EAX压栈 作为参数来调用基类的构造函数. 函数和上边说的一样,所以返回时该对象内存为
1C 20 43 00 02 00 00 00 此时虚表指针指向的是类A的VTABLE
ecx,dword ptr [ebp-4]
把THIS指针给ECX
dword ptr [ecx],offset b::`vftable' (00432034)
把B类的VTABLE地址给THIS指针开始的4个字节 (VPTR) 所以现在对象的内存是
34 20 43 00 02 00 00 00
类C的和前边两个基本相同.最后该对象的内存是 4C 20 43 00 03 00 00 00
最后3个对象的内存为
4C 20 43 00 03 00 00 00 34 20 43 00 02 00 00 00 烫烫L C.....4 C.....
0012FF78 1C 20 43 00 01 00 00 00
45: a * p[3]={ &ca , &cb , &cc };
004011D6 lea eax,[ebp-8]
004011D9 mov dword ptr [ebp-24h],eax
004011DC lea ecx,[ebp-10h]
004011DF mov dword ptr [ebp-20h],ecx
004011E2 lea edx,[ebp-18h]
004011E5 mov dword ptr [ebp-1Ch],edx
把3个对象的THIS指针赋值给3个4字节的指针数组.
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
48: p[1]->display();
004011FB mov eax,dword ptr [ebp-20h]
004011FE mov edx,dword ptr [eax]
00401200 mov esi,esp
00401202 mov ecx,dword ptr [ebp-20h]
00401205 call dword ptr [edx]
00401207 cmp esi,esp
00401209 call __chkesp (004092c0)
49: p[2]->display();
0040120E mov eax,dword ptr [ebp-1Ch]
00401211 mov edx,dword ptr [eax]
00401213 mov esi,esp
00401215 mov ecx,dword ptr [ebp-1Ch]
00401218 call dword ptr [edx]
0040121A cmp esi,esp
0040121C call __chkesp (004092c0)
50:
51: return 0;
00401221 xor eax,eax
这里就是虚函数调用,他们因为虚表不同,所以输出的结果 调用的函数是不同的.
先看看
47: p[0]->display();
004011E8 mov eax,dword ptr [ebp-24h]
004011EB mov edx,dword ptr [eax]
004011ED mov esi,esp
004011EF mov ecx,dword ptr [ebp-24h]
004011F2 call dword ptr [edx]
004011F4 cmp esi,esp
004011F6 call __chkesp (004092c0)
把第一个对象的THIS指针给EAX,
mov edx,dword ptr [eax]
THIS指针开始的4个字节取内容.给EDX, 此时EDX就是VPTR,也就是VTABLE的地址
mov ecx,dword ptr [ebp-24h]
把THIS指针在给ECX
call dword ptr [edx]
每个类的虚函数在虚表中的位置就靠它在类中声明的位置而定,没个虚函数的位置就做为了VTABLE中的ID,因为测试代码为每个类只定义了一个VIRTUAL FUNCTION 所以这里就CALL了EDX指向的VTABLE开始的4字节的那个为一一个VIRTUAL函数地址
跟踪一下内存
0012FF78 1C 20 43 00
跟踪[eax] EDX = 0043201C
跟踪[edx] 0043201C 5F 10 40 00
跟踪 0040105F jmp a::display (004012b0)
于是调用了A类的DISPLAY()函数
后边的两个调用一样会利用VTABLE来调用虚函数.