来源 http://blog.csdn.net/sxf_824/article/details/6258403
C++函数调用一般分为三类:
1.普通函数调用。
2.类方法调用。
3.类虚函数方法调用。
这三类方法调用是如何运作的呢,其中的玄机到底是什么,今天写了一个简单程序,并通过objdump得到汇编代码进行分析。代码实例如下:
void FunctionNormal( int value )
{
int b = value;
}
class VirtualBase
{
public:
virtual void test1(int value) = 0;
virtual void test2(int value) = 0;
};
class ChildClass : public VirtualBase
{
private:
int value;
public:
void test1( int value );
void test2( int value );
};
void ChildClass::test1( int param )
{
value = param;
}
void ChildClass::test2( int param )
{
value = param;
}
class NormalClass
{
private:
int value1;
int value2;
public:
void test( int b);
};
void NormalClass::test( int b)
{
value2=b;
}
main()
{
//声明变量,并通过赋值语句便于定位代码
int flag =0;
//普通方法调用
int tmpvalue =10;
FunctionNormal(tmpvalue);
//一个分割点,不带虚函数普通类方法调用
flag=1;
NormalClass* tmp1 = new NormalClass();
tmp1->test(10);
//一个分割点,带虚函数方法调用
flag=1;
VirtualBase* tmp = new ChildClass();
tmp->test2(flag);
}
汇编关键部分的分析如下,希望大家对C++类面向对象特性和抽象特性的实现机理有所帮助:
1.第一部分,我们先看普通函数调用:
0804858c <main>:
...
//下面这行代码为我们的临时变量flag赋值
804859e: c7 45 e8 00 00 00 00 movl $0x0,0xffffffe8(%ebp)
//为我们临时变量tmpvalue赋值
80485a5: c7 45 ec 0a 00 00 00 movl $0xa,0xffffffec(%ebp)
//下面两行,将参数tmpvalue入栈,
80485ac: 8b 45 ec mov 0xffffffec(%ebp),%eax
80485af: 89 04 24 mov %eax,(%esp)
//调用FunctionNormal方法
80485b2: e8 9d ff ff ff call 8048554 <_Z14FunctionNormali>
...
接下来我们看看FunctionNormal的汇编实现,也就是80485b2处代码,参数传递是通过堆栈负责的,这部分的细节大家可以google或者百度。
简单介绍下堆栈保存数据如下:
1.参数入栈
2.返回调用地址入栈(从函数返回)
3.方法的一些临时变量。
08048554 <_Z14FunctionNormali>:
//保存ebp堆栈辅助寄存器
8048554: 55 push %ebp
8048555: 89 e5 mov %esp,%ebp
8048557: 83 ec 10 sub $0x10,%esp
//得到方法参数value的值void FunctionNormal( int value )
804855a: 8b 45 08 mov 0x8(%ebp),%eax
804855d: 89 45 fc mov %eax,0xfffffffc(%ebp)
8048560: c9 leave
8048561: c3 ret
//调用很简单,参数入栈后直接通过函数地址进行调用。
2.第二部分,我们看看普通类方法调用
...
//这又是我们的flag变量赋值
80485b7: c7 45 e8 01 00 00 00 movl $0x1,0xffffffe8(%ebp)
80485be: c7 04 24 08 00 00 00 movl $0x8,(%esp)
80485c5: e8 a2 fe ff ff call 804846c <_Znwj@plt>
80485ca: c7 00 00 00 00 00 movl $0x0,(%eax)
80485d0: c7 40 04 00 00 00 00 movl $0x0,0x4(%eax)
80485d7: 89 45 f0 mov %eax,0xfffffff0(%ebp)
80485da: c7 44 24 04 0a 00 00 movl $0xa,0x4(%esp)
80485e1: 00
//关键是下面两行,指针NormalClass* tmp1被压入堆栈
80485e2: 8b 45 f0 mov 0xfffffff0(%ebp),%eax
80485e5: 89 04 24 mov %eax,(%esp)
80485e8: e8 91 ff ff ff call 804857e <_ZN11NormalClass4testEi>
。。。
我们再看看函数的汇编代码,也就是804857e处的代码:
0804857e <_ZN11NormalClass4testEi>:
804857e: 55 push %ebp
804857f: 89 e5 mov %esp,%ebp
//关键在这里,从堆栈中取出对象指针内容
8048581: 8b 55 08 mov 0x8(%ebp),%edx
//从堆栈中取出void NormalClass::test( int b)参数b
8048584: 8b 45 0c mov 0xc(%ebp),%eax
//这就是关键,所有的成员变量的访问,都通过edx做偏移
这里可以看到,成员变量 int value2,就是通过基地址偏移4字节得到的
8048587: 89 42 04 mov %eax,0x4(%edx)
804858a: 5d pop %ebp
804858b: c3 ret
可以看到,类方法调用只不过在参数入栈时,多了一步将对象的基地址入栈,方法对成员变量的访问,就可以通过这个基地址加上偏移量了。
3.第三部分,我们看看虚函数类方法调用
//呵呵,又是对我们我们华丽的flag变量赋值
80485ed: c7 45 e8 01 00 00 00 movl $0x1,0xffffffe8(%ebp)
80485f4: c7 04 24 08 00 00 00 movl $0x8,(%esp)
80485fb: e8 6c fe ff ff call 804846c <_Znwj@plt>
8048600: 89 c3 mov %eax,%ebx
8048602: 89 1c 24 mov %ebx,(%esp)
8048605: e8 3c 00 00 00 call 8048646 <_ZN10ChildClassC1Ev>
804860a: 89 5d f4 mov %ebx,0xfffffff4(%ebp)
//关键在如下部分:首先得到临时对象tmp, VirtualBase* tmp = new ChildClass();
//至于C++的虚函数表,这里就不做过多赘述了,大家可以google
804860d: 8b 45 f4 mov 0xfffffff4(%ebp),%eax
//通过类对象的地址,得到虚函数表的地址,虚函数表存储在对象的最开始位置
8048610: 8b 00 mov (%eax),%eax
//得到第二个方法的地址,void ChildClass::test2( int param )
8048612: 83 c0 04 add $0x4,%eax
8048615: 8b 10 mov (%eax),%edx
//下面是对象基地址和参数入栈
8048617: 8b 45 e8 mov 0xffffffe8(%ebp),%eax
804861a: 89 44 24 04 mov %eax,0x4(%esp)
804861e: 8b 45 f4 mov 0xfffffff4(%ebp),%eax
8048621: 89 04 24 mov %eax,(%esp)
//调用通过虚函数表得到的函数地址
8048624: ff d2 call *%edx
可以看到,类抽象方法调用仅仅是多了通过虚函数表得到具体类方法偏移量的过程。