demonstrate 的 blog » 日志 » C++ 如何调用函数的
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 里面),
也用寄存器直接传递参数。我们看看几个简单的例子
这是一个极为简单的函数:
理解错误 AT&T 的汇编代码...
好,我们看看 main 里面如何调用 f 的,
下面我们看看如果将调用方式换为函数指针会有什么结果:
地方放的应该就是函数入口地址,怎么居然是 0xfe 呢,晕!调用函数指针很容易,ms 就是把对应
地方的值送入 EAX,而参数和原来类似,call 的时候告诉它是地址就好了
我们注意到其中 12a 和 12f,在连接后连上了
编译出来的代码里面找不到.... 没看见40ccd0 和 40d060,请知情者告知!
<要求解释过程!>
下面,我们看看使用一个类的成员函数调用的过程。
还有一个不懂的是为什么后面要用 nop 填充到 17 捏?然后是 A::g。
我们如下调用该方法
LEA 将 a 的地址送入 EAX(13c),这就是我们所见到的 this 指针的作用,并入栈(13f)
然后调用需要的函数,奇怪的事情是前面调用 f 的时候没有连接也写清楚了 call 的东西,而
这里即使把 A 和调用放在一个文件中编译,也没有写出来调用的目标,而仅仅在连接后才出现
调用的名称,奇怪得很哪... 值得注意的是 static function 的调用并没有将 this 传入。
这样我们可以进一步分析一下 A::f 在调用的时候究竟是在做什么了,我们知道最后一个 SAR 必然是
进行除二运算,那么前面已对应该是将 this 指针取出来,但是该部分并未使用到 this,因此后面一个
LEA 将 EAX 里面的东西覆盖掉了。为了证明这件事情,我们另外写段程序来探讨。
下面我们讨论使用成员指针时如何调用函数。
41b -- 43f 部分不知道在干什么,我们知道 42a 的结果是 test 为 0,因此直接跳转到 441,这时
EAX 含有 A::f 的入口地址,444 将这个地址放到 0xffffffec(%ebp),这就是 Afptr 的老窝了。
447 开始和原来一样,先将 0x3 入栈,然后是 this 指针,最后 call 之。
利用原来的一部分代码,我们研究一下 virtual 函数时如何调用的,至于对应的指针调用形式
肯定是上面的方法的结合,代码过于冗长就不列出来了。
这是我们的几个类
看看 virtual 函数怎么被调用的吧
vtable 的入口地址,由于只有一个 virtual 函数,而且没有其他的成员,
该对象的 vtable 入口地址偏移量就是 0,因此这个地址上也就存放着这个 virtual 函数的入口地址,
因此 107 实际上将对象 a 的地址(EAX)求值((%eax))获得了 vtable 的入口地址放到 EDX,
同时为了传入 this 指针,又在 109--10c 把 a 的地址放入栈中,最后 10f 将需要调用的 virtual 函数的
地址放到 EAX 并 call 之。
预备知识,几个常见的汇编命令
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>:可见参数在 EBP + 0x08 处(正好一个 int ?),返回值应该在 EAX 里面,如果我没有
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
理解错误 AT&T 的汇编代码...
好,我们看看 main 里面如何调用 f 的,
f( 3 ) ;编译出来的结果如下
13b: c7 04 24 03 00 00 00 movl 0x3,(%esp)结果就是通过 movl 将常数 0x03 送到 ESP 所指的位置,然后 call 之。
142: e8 b7 ff ff ff call fe <__Z1fi>
下面我们看看如果将调用方式换为函数指针会有什么结果:
int (*fptr)( int ) ;编译出来的相关部分如下:
fptr( 3 ) ;
12a: e8 00 00 00 00 call 12f <_main+0x25>我们来分析一下上面的过程,往 fptr 也就是 EBP + 0xfffffffc 对应的
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
地方放的应该就是函数入口地址,怎么居然是 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 {我们准备使用两种函数,首先看简单的 A::f。
public:
static int
g( int a )
{
return a << 1 ;
}
int
f( int a )
{
return a / 2 ;
}
} ;
00000000 <__ZN1A1fEi>:不懂 sar 做什么,shr 就是 shift right 了,可能是不是要修正一下这个 signed int?
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
还有一个不懂的是为什么后面要用 nop 填充到 17 捏?然后是 A::g。
00000000 <__ZN1A1gEi>:很明显,编译器把 a << 1 换成了 a + a,但是前几天不是试验结果表明 shl 要快的么?
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 a ;看看编译结果吧...
a.f( 3 ) ;
A::g( 3 ) ;
134: c7 44 24 04 03 00 00 movl 0x3,0x4(%esp)这里 134 仍然是将 a.f 的参数入栈,而后却是一段不大容易理解的东西,13b 为何空下来?
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>
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)上面的代码如下含义,注意 {fckeditor}x410a90 实际上是 A::f 的地址,然后很不解的是
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)
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>:我们发现这里也有一部分和 this 相关的代码,想来是 sub 那句,这里也没用到 this...
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
看看 virtual 函数怎么被调用的吧
void这部分的汇编代码如下:
f( const A& a ) {
a.sayhello( ) ;
}
000000fe <__Z1fRK1A>:可以看出来,该函数首先通过引用(其实是个地址,104 的 0x8(%ebp))求出
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
vtable 的入口地址,由于只有一个 virtual 函数,而且没有其他的成员,
该对象的 vtable 入口地址偏移量就是 0,因此这个地址上也就存放着这个 virtual 函数的入口地址,
因此 107 实际上将对象 a 的地址(EAX)求值((%eax))获得了 vtable 的入口地址放到 EDX,
同时为了传入 this 指针,又在 109--10c 把 a 的地址放入栈中,最后 10f 将需要调用的 virtual 函数的
地址放到 EAX 并 call 之。
曾经的这一天...
- » 2005年: 怪癖接力--from tangtang
- » 2005年: 赞 wlm !
- » 2005年: 师父,徒弟对不起你!
相关日志:
收藏:
QQ书签
del.icio.us
订阅:
Google
抓虾
