一个函数到底是怎么调用的,返回值是如何返回的,这里面学问真的很多,让我们来小小的分析一下。。。。
我们看一个很简单的C代码,通过gdb调试来分析函数调用过程。
平台和工具:ubuntu12.04+gcc 4.6.3+GNU gdb 7.4-2012.04
cs.c:
int add(int a,int b)
{
return a+b;
}
int main()
{
int a=1;
int b=2;
int c=add(a,b);
}
编译:gcc -g cs.c
调试:gdb a.out
我们看一下:main函数的汇编代码:
(gdb) disas main
Dump of assembler code for function main:
01 0x080483c1 <+0>: push %ebp
02 0x080483c2 <+1>: mov %esp,%ebp
03 0x080483c4 <+3>: sub $0x18,%esp
04 0x080483c7 <+6>: movl $0x1,-0xc(%ebp)
05 0x080483ce <+13>: movl $0x2,-0x8(%ebp)
06 0x080483d5 <+20>: mov -0x8(%ebp),%eax
07 0x080483d8 <+23>: mov %eax,0x4(%esp)
08 0x080483dc <+27>: mov -0xc(%ebp),%eax
09 0x080483df <+30>: mov %eax,(%esp)
10 0x080483e2 <+33>: call 0x80483b4 <add>
11 0x080483e7 <+38>: mov %eax,-0x4(%ebp)
12 0x080483ea <+41>: leave
13 0x080483eb <+42>: ret
End of assembler dump.
为了方便我在每行前面加了序号(本来是没有的)
堆栈情况如下:
图1
下面的数字代表main函数反编译的行号
1.帧指针ebp入栈
2.帧指针ebp指向当前栈指针esp
3.分配24(ox18)字节空间,所以esp下移24字节,如图1
4,5.局部变量a,b入栈,这里可以看到变量分配是按地址增大方向,所以虽然a先定义,但a的地址小,如图1
这里,你可能会问那C在哪呢?按地址增大方向,c应该在b的上面,说的很对,图1中
ebp-4是不是什么都没有,那就是c的所在地址,我们将在11行得到证明
6,7.将add的参数b入栈
8,9.将add的参数a入栈,如图1
10.调用add子程序,我们反编译add函数,
(gdb) disas add
Dump of assembler code for function add:
0x080483b4 <+0>: push %ebp
0x080483b5 <+1>: mov %esp,%ebp
0x080483b7 <+3>: mov 0xc(%ebp),%eax
0x080483ba <+6>: mov 0x8(%ebp),%edx
0x080483bd <+9>: add %edx,%eax
0x080483bf <+11>: pop %ebp
0x080483c0 <+12>: ret
End of assembler dump.
我们看到调用的是add函数的地址: 0x080483b4
call做了两个操作,一,将返回地址入栈,那返回地址是什么呢?
返回地址,顾名思义就是调用子程序后返回的地址,那就应该是call下一条指令
的地址,即 0x080483e7 二,跳转到子程序的起始处
调用add后,堆栈如下图所以。
图2
我们将验证图2的返回地址就是0x080483e7
给两函数添加断点
gdb) break main
Breakpoint 1 at 0x80483c7: file cs.c, line 8.
(gdb) break add
Breakpoint 2 at 0x80483b7: file cs.c, line 3.
(gdb) r
Starting program: /home/yihaibo/c/a.out
Breakpoint 1, main () at cs.c:8
8 int a=1;
(gdb) n
9 int b=2;
(gdb) n
10 int c=add(a,b);
(gdb) disas main
Dump of assembler code for function main:
0x080483c1 <+0>: push %ebp
0x080483c2 <+1>: mov %esp,%ebp
0x080483c4 <+3>: sub $0x18,%esp
0x080483c7 <+6>: movl $0x1,-0xc(%ebp)
0x080483ce <+13>: movl $0x2,-0x8(%ebp)
=> 0x080483d5 <+20>: mov -0x8(%ebp),%eax
0x080483d8 <+23>: mov %eax,0x4(%esp)
0x080483dc <+27>: mov -0xc(%ebp),%eax
0x080483df <+30>: mov %eax,(%esp)
0x080483e2 <+33>: call 0x80483b4 <add>
0x080483e7 <+38>: mov %eax,-0x4(%ebp)
0x080483ea <+41>: leave
0x080483eb <+42>: ret
End of assembler dump.
调试过程中,可通过disas main,参看执行到main函数哪条指令(箭头所指地址)
(gdb) stepi
0x080483d8 10 int c=add(a,b);
(gdb) disas main
Dump of assembler code for function main:
0x080483c1 <+0>: push %ebp
0x080483c2 <+1>: mov %esp,%ebp
0x080483c4 <+3>: sub $0x18,%esp
0x080483c7 <+6>: movl $0x1,-0xc(%ebp)
0x080483ce <+13>: movl $0x2,-0x8(%ebp)
0x080483d5 <+20>: mov -0x8(%ebp),%eax
=> 0x080483d8 <+23>: mov %eax,0x4(%esp)
0x080483dc <+27>: mov -0xc(%ebp),%eax
0x080483df <+30>: mov %eax,(%esp)
0x080483e2 <+33>: call 0x80483b4 <add>
0x080483e7 <+38>: mov %eax,-0x4(%ebp)
0x080483ea <+41>: leave
0x080483eb <+42>: ret
End of assembler dump.
可通过命令:stepi,执行汇编代码一条指令,和上面对比,发现的确执行一条指令。
(gdb) disas main
Dump of assembler code for function main:
0x080483c1 <+0>: push %ebp
0x080483c2 <+1>: mov %esp,%ebp
0x080483c4 <+3>: sub $0x18,%esp
0x080483c7 <+6>: movl $0x1,-0xc(%ebp)
0x080483ce <+13>: movl $0x2,-0x8(%ebp)
0x080483d5 <+20>: mov -0x8(%ebp),%eax
0x080483d8 <+23>: mov %eax,0x4(%esp)
0x080483dc <+27>: mov -0xc(%ebp),%eax
0x080483df <+30>: mov %eax,(%esp)
=> 0x080483e2 <+33>: call 0x80483b4 <add>
0x080483e7 <+38>: mov %eax,-0x4(%ebp)
0x080483ea <+41>: leave
0x080483eb <+42>: ret
End of assembler dump.
(gdb) stepi
add (a=1, b=2) at cs.c:2
2 {
(gdb) print /x *(int*)($ebp-28)
$2 = 0x80483e7
通过多次stepi,当执行call指令时,我们可以打印出返回地址的值。
很容易计算出,返回地址所在栈地址是($ebp-28),所以通过print /x *(int*)($ebp-28)命令打印出($ebp-28)中存储的值(x 16进制),我们发现的确是0x080483e7,的确是call的下一条指令所在的地址。
这时程序将跳转到add子程序,
(gdb) disas add
Dump of assembler code for function add:
=> 0x080483b4 <+0>: push %ebp
0x080483b5 <+1>: mov %esp,%ebp
0x080483b7 <+3>: mov 0xc(%ebp),%eax
0x080483ba <+6>: mov 0x8(%ebp),%edx
0x080483bd <+9>: add %edx,%eax
0x080483bf <+11>: pop %ebp
0x080483c0 <+12>: ret
End of assembler dump.
跳转之后的堆栈如图2所示。
我们看到:
mov 0xc(%ebp),%eax 取得形参b的值2
mov 0x8(%ebp),%edx 取得形参a的值1
然后将两值相加,此时%eax中的值就是add的返回值。
pop %ebp 弹出%ebp
ret 弹出返回地址,并跳转到返回地址,此时堆栈有恢复到图1。
指令执行如下:
(gdb) disas main
Dump of assembler code for function main:
0x080483c1 <+0>: push %ebp
0x080483c2 <+1>: mov %esp,%ebp
0x080483c4 <+3>: sub $0x18,%esp
0x080483c7 <+6>: movl $0x1,-0xc(%ebp)
0x080483ce <+13>: movl $0x2,-0x8(%ebp)
0x080483d5 <+20>: mov -0x8(%ebp),%eax
0x080483d8 <+23>: mov %eax,0x4(%esp)
0x080483dc <+27>: mov -0xc(%ebp),%eax
0x080483df <+30>: mov %eax,(%esp)
0x080483e2 <+33>: call 0x80483b4 <add>
=> 0x080483e7 <+38>: mov %eax,-0x4(%ebp)
0x080483ea <+41>: leave
0x080483eb <+42>: ret
End of assembler dump.
此时将add返回值eax赋给-0x4(%ebp),即上面所说c的地址
(gdb) print &c
$6 = (int *) 0xbffff0e4
(gdb) print $ebp-4
$7 = (void *) 0xbffff0e4
(gdb) print /x *(int*)($ebp-4)
$4 = 0x3
(gdb) print c
$5 = 3
通过上面我们可以看出c的地址的确是$ebp-4,在本次实验中eax是函数的返回值
这个过程分析就over了.....
分享到:
相关推荐
浅析二级C语言之函数调用的格式.pdf
本文较详细的介绍了keilc51可再入函数和模拟堆栈的一些概念和实现原理,通过一个简单的程序来剖析keilc51在大存储模式下可重入函数的调用过程,希望能为keilc51和在51系列单片机上移植嵌入式实时操作系统的初学者...
下面小编就为大家带来一篇浅析成员函数和常成员函数的调用。小编觉得挺不错的,现在分享给大家,也给大家做个参考,一起跟随小编过来看看吧
我们说一个函数的调用模式是作为一个函数来调用,是要与其它三种调用模式做区分 函数其他的三种调用: 方法调用模式,构造器调用模式,apply/call调用模式。 方法的调用模式: var obj={ fun1: function(){ //...
假如函数在调用它之前定义... 您可能感兴趣的文章:深入理解C++中public、protected及private用法C/C++函数调用的几种方式总结浅析C语言中堆和栈的区别关于C语言中参数的传值问题浅谈C语言函数调用参数压栈的相关问题
本文详细介绍了location.href跨窗口调用函数,具体的使用方法及实例,有需要的朋友可以参考下
网上资料整理。简要解析了MFC中几类关键问题的机理、过程和方法: MFC浅析(1) 文档视图结构中,缺省的命令处理 MFC浅析(2) 文档视图结构中文...MFC浅析(7) CWnd类虚函数的调用时机、缺省实现 MFC浅析(8) CArchive 原理
关于函数参数的定义,调用以及函数参数的内容,在下面的文章中已经做了初步的介绍,有需要的可以访问进行了解: Python 函数 函数注解 在编写函数,当下肯定清楚函数如何使用的。若是函数较为复杂,过段时间,编写者...
1、一般形式的创建函数,在执行代码之前会先读取函数声明,所以可以把函数声明写在函数调用的下面: sayHi(); function sayHi(){ alert("Hi!"); } 2、使用函数表达式创建函数,调用前必须先赋值: ...
call_user_func函数类似于一种特别的调用函数的方法,使用方法如下: <?php function nowamagic($a,$b) { echo $a; echo $b; } call_user_func('nowamagic', "",""); call_user_func('nowamagic', "",""); //...
如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,...
以函数声明的方法定义的函数,函数可以在函数声明之前调用,而函数表达式的函数只能在声明之后调用. 3).以函数声明的方法定义的函数并不是真正的声明,它们仅仅可以出现在全局中,或者嵌套在其他的函数中,但是它们不能...
参数传递是Java语言中函数调用的重要步骤,清楚地了解参数传递的过程是编写出高质量程序所必须的.大多数程序设计语言具有传值调用和传引用调用的两种方法.通过对典型程序的研究与分析可以看出JAVA语言的参数传递总是...
QT信号与槽机制浅析Qt的信号和槽机制是Qt的一大...说法,简单点说就是如何在一个类的一个函数中触发另一个类的另一个函数调用,而且还要把相关的参数传递过去.好像这和回调函数也有点关系,但是消息机制可比回调函数有用
本文实例分析了js函数与php函数的区别。...注意,不能写成t(),加括号是表示调用这个函数。 alert(m()); [removed] </body> </html> 所以,声明函数也可以这样: 代码如下:t =
如果对于jquery的$()包装器函数还不是很清楚,请先参阅我的上一篇日志:浅析jQuery的链式调用
关于函数参数的定义,调用以及函数参数的内容,在下面的文章中已经做了初步的介绍,有需要的可以访问进行了解: Python 函数 函数注解 在编写函数,当下肯定清楚函数如何使用的。若是函数较为复杂,过段时间,编写者...
如果一个变量指向了一个函数,那我们直接调用abs(x)与执行f(x)返回的结果是完全相同。 2、函数名也是变量 我们也可以把函数名看成是一个变量,例如abs()函数。执行语句abs=-5,之后调用abs(-5)就会报错,因为此时...