C++ 如何调用函数的

demonstrate 发表于 2006-10-26 23:20:39

给一个简单的例子,分析一下调用函数的过程
预备知识,几个常见的汇编命令
LEA,load effective address,将偏移地址装入寄存器
SAR,Shift Arithmetically right,算术右移
IMUL,带符号整数乘法
MOV 的后缀是 B W L 分别表示 byte word long
寻址方式 d( a, b, c ) 表示 a + b * c + d,其中 a 是基址,b 是 index,c 是 scale,d 是偏移
参看这个连接

我们知道,传递参数实际上是把参数放入 stack 中,如果参数很小(可放在 register 里面),
也用寄存器直接传递参数。我们看看几个简单的例子
这是一个极为简单的函数:
int
f( int a )
{
  return a * a ;
}
编译出来的汇编代码如下:
000000fe <__Z1fi>:
fe: 55 push %ebp
ff: 89 e5 mov %esp,%ebp
101: 8b 45 08 mov 0x8(%ebp),%eax
104: 0f af 45 08 imul 0x8(%ebp),%eax
108: 5d pop %ebp
109: c3 ret
可见参数在 EBP + 0x08 处(正好一个 int ?),返回值应该在 EAX 里面,如果我没有
理解错误 AT&T 的汇编代码...
好,我们看看 main 里面如何调用 f 的,
f( 3 ) ;
编译出来的结果如下
 13b:   c7 04 24 03 00 00 00    movl   0x3,(%esp)
142: e8 b7 ff ff ff call fe <__Z1fi>
结果就是通过 movl 将常数 0x03 送到 ESP 所指的位置,然后 call 之。
下面我们看看如果将调用方式换为函数指针会有什么结果:
int (*fptr)( int ) ;
fptr( 3 ) ;
编译出来的相关部分如下:
 12a:   e8 00 00 00 00          call   12f <_main+0x25>
12f: e8 00 00 00 00 call 134 <_main+0x2a>
134: c7 45 fc fe 00 00 00 movl 0xfe,0xfffffffc(%ebp)
13b: c7 04 24 03 00 00 00 movl 0x3,(%esp)
142: 8b 45 fc mov 0xfffffffc(%ebp),%eax
145: ff d0 call *%eax
我们来分析一下上面的过程,往 fptr 也就是 EBP + 0xfffffffc 对应的
地方放的应该就是函数入口地址,怎么居然是 0xfe 呢,晕!调用函数指针很容易,ms 就是把对应
地方的值送入 EAX,而参数和原来类似,call 的时候告诉它是地址就好了
我们注意到其中 12a 和 12f,在连接后连上了
  40140a:       e8 51 bc 00 00          call   40d060 <___chkstk>
40140f: e8 bc b8 00 00 call 40ccd0 <___main>
这就是说调用了两个其他的连接器需要的代码,可能做一些检查和初始化吧,诡异的是这部分居然在
编译出来的代码里面找不到.... 没看见40ccd0 和 40d060,请知情者告知!
<要求解释过程!>

下面,我们看看使用一个类的成员函数调用的过程。
class A {
public:
  static int
  g( int a )
  {
    return a << 1 ;
  }
  int
  f( int a )
  {
    return a / 2 ;
  }
} ;
我们准备使用两种函数,首先看简单的 A::f。
00000000 <__ZN1A1fEi>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 55 0c mov 0xc(%ebp),%edx
6: 89 d0 mov %edx,%eax
8: c1 f8 1f sar 0x1f,%eax
b: c1 e8 1f shr 0x1f,%eax
e: 8d 04 02 lea (%edx,%eax,1),%eax
11: d1 f8 sar %eax
13: 5d pop %ebp
14: c3 ret
15: 90 nop
16: 90 nop
17: 90 nop
不懂 sar 做什么,shr 就是 shift right 了,可能是不是要修正一下这个 signed int?
还有一个不懂的是为什么后面要用 nop 填充到 17 捏?然后是 A::g。
00000000 <__ZN1A1gEi>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 8b 45 08 mov 0x8(%ebp),%eax
6: 01 c0 add %eax,%eax
8: 5d pop %ebp
9: c3 ret
a: 90 nop
b: 90 nop
很明显,编译器把 a << 1 换成了 a + a,但是前几天不是试验结果表明 shl 要快的么?
我们如下调用该方法
A a ;
a.f( 3 ) ;
A::g( 3 ) ;
看看编译结果吧...
 134:   c7 44 24 04 03 00 00    movl   0x3,0x4(%esp)
13b: 00
13c: 8d 45 ff lea 0xffffffff(%ebp),%eax
13f: 89 04 24 mov %eax,(%esp)
142: e8 00 00 00 00 call 147 <_main+0x3d>
147: c7 04 24 03 00 00 00 movl 0x3,(%esp)
14e: e8 00 00 00 00 call 153 <_main+0x49>
这里 134 仍然是将 a.f 的参数入栈,而后却是一段不大容易理解的东西,13b 为何空下来?
LEA 将 a 的地址送入 EAX(13c),这就是我们所见到的 this 指针的作用,并入栈(13f)
然后调用需要的函数,奇怪的事情是前面调用 f 的时候没有连接也写清楚了 call 的东西,而
这里即使把 A 和调用放在一个文件中编译,也没有写出来调用的目标,而仅仅在连接后才出现
调用的名称,奇怪得很哪... 值得注意的是 static function 的调用并没有将 this 传入。

这样我们可以进一步分析一下 A::f 在调用的时候究竟是在做什么了,我们知道最后一个 SAR 必然是
进行除二运算,那么前面已对应该是将 this 指针取出来,但是该部分并未使用到 this,因此后面一个
LEA 将 EAX 里面的东西覆盖掉了。为了证明这件事情,我们另外写段程序来探讨。

下面我们讨论使用成员指针时如何调用函数。
A a ;
int (A::* Afptr)(int) ;
Afptr = &A::f ;
(a .* Afptr)( 3 ) ;
再看汇编代码
  401414:       c7 45 f0 90 0a 41 00    movl   0x410a90,0xfffffff0(%ebp)
40141b: c7 45 f4 00 00 00 00 movl 0x0,0xfffffff4(%ebp)
401422: 8b 45 f0 mov 0xfffffff0(%ebp),%eax
401425: 83 e0 01 and 0x1,%eax
401428: 84 c0 test %al,%al
40142a: 74 15 je 401441 <_main+0x57>
40142c: 89 e8 mov %ebp,%eax
40142e: 03 45 f4 add 0xfffffff4(%ebp),%eax
401431: 8d 50 ff lea 0xffffffff(%eax),%edx
401434: 8b 45 f0 mov 0xfffffff0(%ebp),%eax
401437: 03 02 add (%edx),%eax
401439: 48 dec %eax
40143a: 8b 00 mov (%eax),%eax
40143c: 89 45 ec mov %eax,0xffffffec(%ebp)
40143f: eb 06 jmp 401447 <_main+0x5d>
401441: 8b 45 f0 mov 0xfffffff0(%ebp),%eax
401444: 89 45 ec mov %eax,0xffffffec(%ebp)
401447: c7 44 24 04 03 00 00 movl 0x3,0x4(%esp)
40144e: 00
40144f: 8d 45 ff lea 0xffffffff(%ebp),%eax
401452: 03 45 f4 add 0xfffffff4(%ebp),%eax
401455: 89 04 24 mov %eax,(%esp)
401458: ff 55 ec call *0xffffffec(%ebp)
上面的代码如下含义,注意 {fckeditor}x410a90 实际上是 A::f 的地址,然后很不解的是
41b -- 43f 部分不知道在干什么,我们知道 42a 的结果是 test 为 0,因此直接跳转到 441,这时
EAX 含有 A::f 的入口地址,444 将这个地址放到 0xffffffec(%ebp),这就是 Afptr 的老窝了。
447 开始和原来一样,先将 0x3 入栈,然后是 this 指针,最后 call 之。

利用原来的一部分代码,我们研究一下 virtual 函数时如何调用的,至于对应的指针调用形式
肯定是上面的方法的结合,代码过于冗长就不列出来了。
这是我们的几个类
class A {
public:
  virtual void
  sayhello( ) const {
    cout << "Hello from A" << endl ;
  }
} ;

class A1 : public A {
public:
  void
  sayhello( ) const {
    cout << "Hello from A1" << endl ;
  }
} ;

class A2 : public A {
public:
  void
  sayhello( ) const {
    cout << "Hello from A2" << endl ;
  }
} ;
看看对应的汇编代码片断
00000000 <__ZNK1A8sayhelloEv>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub 0x8,%esp
6: c7 44 24 04 00 00 00 movl 0x0,0x4(%esp)
d: 00
e: c7 04 24 00 00 00 00 movl 0x0,(%esp)
15: e8 00 00 00 00 call 1a <__ZNK1A8sayhelloEv+0x1a>
1a: c7 44 24 04 00 00 00 movl 0x0,0x4(%esp)
21: 00
22: 89 04 24 mov %eax,(%esp)
25: e8 00 00 00 00 call 2a <__ZNK1A8sayhelloEv+0x2a>
2a: c9 leave
2b: c3 ret

00000000 <__ZNK1A8sayhelloEv>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub 0x8,%esp
6: c7 44 24 04 00 00 00 movl 0x0,0x4(%esp)
d: 00
e: c7 04 24 00 00 00 00 movl 0x0,(%esp)
15: e8 00 00 00 00 call 1a <__ZNK1A8sayhelloEv+0x1a>
1a: c7 44 24 04 00 00 00 movl 0x0,0x4(%esp)
21: 00
22: 89 04 24 mov %eax,(%esp)
25: e8 00 00 00 00 call 2a <__ZNK1A8sayhelloEv+0x2a>
2a: c9 leave
2b: c3 ret

00000000 <__ZNK2A28sayhelloEv>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub 0x8,%esp
6: c7 44 24 04 1b 00 00 movl 0x1b,0x4(%esp)
d: 00
e: c7 04 24 00 00 00 00 movl 0x0,(%esp)
15: e8 00 00 00 00 call 1a <__ZNK2A28sayhelloEv+0x1a>
1a: c7 44 24 04 00 00 00 movl 0x0,0x4(%esp)
21: 00
22: 89 04 24 mov %eax,(%esp)
25: e8 00 00 00 00 call 2a <__ZNK2A28sayhelloEv+0x2a>
2a: c9 leave
2b: c3 ret
我们发现这里也有一部分和 this 相关的代码,想来是 sub 那句,这里也没用到 this...
看看 virtual 函数怎么被调用的吧
void
f( const A& a ) {
  a.sayhello( ) ;
}
这部分的汇编代码如下:
000000fe <__Z1fRK1A>:
fe: 55 push %ebp
ff: 89 e5 mov %esp,%ebp
101: 83 ec 08 sub 0x8,%esp
104: 8b 45 08 mov 0x8(%ebp),%eax
107: 8b 10 mov (%eax),%edx
109: 8b 45 08 mov 0x8(%ebp),%eax
10c: 89 04 24 mov %eax,(%esp)
10f: 8b 02 mov (%edx),%eax
111: ff d0 call *%eax
113: c9 leave
114: c3 ret
115: 90 nop
可以看出来,该函数首先通过引用(其实是个地址,104 的 0x8(%ebp))求出
vtable 的入口地址,由于只有一个 virtual 函数,而且没有其他的成员,
该对象的 vtable 入口地址偏移量就是 0,因此这个地址上也就存放着这个 virtual 函数的入口地址,
因此 107 实际上将对象 a 的地址(EAX)求值((%eax))获得了 vtable 的入口地址放到 EDX,
同时为了传入 this 指针,又在 109--10c 把 a 的地址放入栈中,最后 10f 将需要调用的 virtual 函数的
地址放到 EAX 并 call 之。
关键词(Tag): pointer class member function

曾经的这一天...


收藏: QQ书签 del.icio.us 订阅: Google 抓虾

最新评论


  • demonstrate
    2006-10-27 13:53:45

    13b 为何空下来?
    其实没空下来,因为 0x3 是一个 long,所以占了 4 bytes,而上面一行写不下了
    ft...

发表评论

* 昵称

已经注册过? 请登录

新用户请先注册 以便能显示头像及追踪评论回复

Email
网址
* 评论
表情
 
 

分类小组论坛
杂谈, 娱乐、八卦, 文学、艺术, 体育, 旅游、同城, 象牙塔, 情感, 时尚、生活, 星座, 科技

请注意遵守中华人民共和国法律法规, 如威胁到本站生存, 将依法向有关部门报告, 同时本站的相关记录可能成为对您不利的证据.

相关法律法规
全国人大常委会关于维护互联网安全的决定
中华人民共和国计算机信息系统安全保护条例
中华人民共和国计算机信息网络国际联网管理暂行规定
计算机信息网络国际联网安全保护管理办法
计算机信息系统国际联网保密管理规定