C++各类函数调用实现分析

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

 

可以看到,类抽象方法调用仅仅是多了通过虚函数表得到具体类方法偏移量的过程。

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值