函数调用栈详解(good)

 

 

全国嵌入式人才培训基地


1. 函数调用
 
上一页
 第 19 章 汇编与C之间的关系
  下一页
 

 

--------------------------------------------------------------------------------

1. 函数调用 请点评

我们用下面的代码来研究函数调用的过程。
例 19.1. 研究函数的调用过程
int bar(int c, int d)

{

               int e = c + d;

               return e;

}

 

int foo(int a, int b)

{

               return bar(a, b);

}

 

int main(void)

{

               foo(2, 3);

               return 0;

}

 

如果在编译时加上-g选项(在第 10 章 gdb讲过-g选项),那么用objdump反汇编时可以把C代码和汇编代码穿插起来显示,这样C代码和汇编代码的对应关系看得更清楚。反汇编的结果很长,以下只列出我们关心的部分。
$ gcc main.c -g

$ objdump -dS a.out

...

08048394 <bar>:

int bar(int c, int d)

{

 8048394:              55                                push   %ebp

 8048395:              89 e5                             mov    %esp,%ebp

 8048397:              83 ec 10                          sub    $0x10,%esp

               int e = c + d;

 804839a:              8b 55 0c                          mov    0xc(%ebp),%edx

 804839d:              8b 45 08                         mov    0x8(%ebp),%eax

 80483a0:              01 d0                             add    %edx,%eax

 80483a2:              89 45 fc                           mov    %eax,-0x4(%ebp)

               return e;

 80483a5:              8b 45 fc                           mov    -0x4(%ebp),%eax

}

 80483a8:              c9                                 leave 

 80483a9:              c3                                 ret   

 

080483aa <foo>:

 

int foo(int a, int b)

{

 80483aa:              55                                push   %ebp

 80483ab:              89 e5                             mov    %esp,%ebp

 80483ad:              83 ec 08                          sub    $0x8,%esp

               return bar(a, b);

 80483b0:              8b 45 0c                          mov    0xc(%ebp),%eax

 80483b3:              89 44 24 04                      mov    %eax,0x4(%esp)

 80483b7:              8b 45 08                         mov    0x8(%ebp),%eax

 80483ba:              89 04 24                         mov    %eax,(%esp)

 80483bd:              e8 d2 ff ff ff         call   8048394 <bar>

}

 80483c2:              c9                                 leave 

 80483c3:              c3                                 ret   

 

080483c4 <main>:

 

int main(void)

{

 80483c4:              8d 4c 24 04                      lea    0x4(%esp),%ecx

 80483c8:              83 e4 f0                          and    $0xfffffff0,%esp

 80483cb:              ff 71 fc                             pushl  -0x4(%ecx)

 80483ce:              55                                push   %ebp

 80483cf:               89 e5                             mov    %esp,%ebp

 80483d1:              51                                push   %ecx

 80483d2:              83 ec 08                          sub    $0x8,%esp

               foo(2, 3);

 80483d5:              c7 44 24 04 03 00 00            movl   $0x3,0x4(%esp)

 80483dc:              00

 80483dd:              c7 04 24 02 00 00 00            movl   $0x2,(%esp)

 80483e4:              e8 c1 ff ff ff         call   80483aa <foo>

               return 0;

 80483e9:              b8 00 00 00 00                  mov    $0x0,%eax

}

 80483ee:              83 c4 08                          add    $0x8,%esp

 80483f1:               59                                pop    %ecx

 80483f2:               5d                                pop    %ebp

 80483f3:               8d 61 fc                           lea    -0x4(%ecx),%esp

 80483f6:               c3                                 ret  

...

要查看编译后的汇编代码,其实还有一种办法是gcc -S main.c,这样只生成汇编代码main.s,而不生成二进制的目标文件。
整个程序的执行过程是main调用foo,foo调用bar,我们用gdb跟踪程序的执行,直到bar函数中的int e = c + d;语句执行完毕准备返回时,这时在gdb中打印函数栈帧。
(gdb) start

...

main () at main.c:14

14                           foo(2, 3);

(gdb) s

foo (a=2, b=3) at main.c:9

9                             return bar(a, b);

(gdb) s

bar (c=2, d=3) at main.c:3

3                             int e = c + d;

(gdb) disassemble

Dump of assembler code for function bar:

0x08048394 <bar+0>:         push   %ebp

0x08048395 <bar+1>:         mov    %esp,%ebp

0x08048397 <bar+3>:         sub    $0x10,%esp

0x0804839a <bar+6>:          mov    0xc(%ebp),%edx

0x0804839d <bar+9>:         mov    0x8(%ebp),%eax

0x080483a0 <bar+12>:        add    %edx,%eax

0x080483a2 <bar+14>:        mov    %eax,-0x4(%ebp)

0x080483a5 <bar+17>:        mov    -0x4(%ebp),%eax

0x080483a8 <bar+20>:        leave 

0x080483a9 <bar+21>:        ret   

End of assembler dump.

(gdb) si

0x0804839d           3                             int e = c + d;

(gdb) si

0x080483a0           3                             int e = c + d;

(gdb) si

0x080483a2           3                             int e = c + d;

(gdb) si

4                             return e;

(gdb) si

5             }

(gdb) bt

#0  bar (c=2, d=3) at main.c:5

#1  0x080483c2 in foo (a=2, b=3) at main.c:9

#2  0x080483e9 in main () at main.c:14

(gdb) info registers

eax            0x5 5

ecx            0xbff1c440     -1074674624

edx            0x3 3

ebx            0xb7fe6ff4     -1208061964

esp            0xbff1c3f4      0xbff1c3f4

ebp            0xbff1c404    0xbff1c404

esi            0x8048410      134513680

edi            0x80482e0     134513376

eip            0x80483a8      0x80483a8 <bar+20>

eflags         0x200206       [ PF IF ID ]

cs             0x73               115

ss             0x7b               123

ds             0x7b               123

es             0x7b               123

fs             0x0   0

gs             0x33               51

(gdb) x/20 $esp

0xbff1c3f4:             0x00000000           0xbff1c6f7              0xb7efbdae            0x00000005

0xbff1c404:            0xbff1c414             0x080483c2           0x00000002           0x00000003

0xbff1c414:            0xbff1c428             0x080483e9           0x00000002           0x00000003

0xbff1c424:            0xbff1c440             0xbff1c498             0xb7ea3685           0x08048410

0xbff1c434:            0x080482e0           0xbff1c498             0xb7ea3685           0x00000001

(gdb)

这里又用到几个新的gdb命令。disassemble可以反汇编当前函数或者指定的函数,单独用disassemble命令是反汇编当前函数,如果disassemble命令后面跟函数名或地址则反汇编指定的函数。以前我们讲过step命令可以一行代码一行代码地单步调试,而这里用到的si命令可以一条指令一条指令地单步调试。info registers可以显示所有寄存器的当前值。在gdb中表示寄存器名时前面要加个$,例如p $esp可以打印esp寄存器的值,在上例中esp寄存器的值是0xbff1c3f4,所以x/20 $esp命令查看内存中从0xbff1c3f4地址开始的20个32位数。在执行程序时,操作系统为进程分配一块栈空间来保存函数栈帧,esp寄存器总是指向栈顶,在x86平台上这个栈是从高地址向低地址增长的,我们知道每次调用一个函数都要分配一个栈帧来保存参数和局部变量,现在我们详细分析这些数据在栈空间的布局,根据gdb的输出结果图示如下[29]:
图 19.1. 函数栈帧

 


图中每个小方格表示4个字节的内存单元,例如b: 3这个小方格占的内存地址是0xbff1c420~0xbff1c423,我把地址写在每个小方格的下边界线上,是为了强调该地址是内存单元的起始地址。我们从main函数的这里开始看起:
               foo(2, 3);

 80483d5:              c7 44 24 04 03 00 00            movl   $0x3,0x4(%esp)

 80483dc:              00

 80483dd:              c7 04 24 02 00 00 00            movl   $0x2,(%esp)

 80483e4:              e8 c1 ff ff ff         call   80483aa <foo>

               return 0;

 80483e9:              b8 00 00 00 00                  mov    $0x0,%eax

要调用函数foo先要把参数准备好,第二个参数保存在esp+4指向的内存位置,第一个参数保存在esp指向的内存位置,可见参数是从右向左依次压栈的。然后执行call指令,这个指令有两个作用:
1.    foo函数调用完之后要返回到call的下一条指令继续执行,所以把call的下一条指令的地址0x80483e9压栈,同时把esp的值减4,esp的值现在是0xbff1c418。
2.    修改程序计数器eip,跳转到foo函数的开头执行。
现在看foo函数的汇编代码:
int foo(int a, int b)

{

 80483aa:              55                                push   %ebp

 80483ab:              89 e5                             mov    %esp,%ebp

 80483ad:              83 ec 08                          sub    $0x8,%esp

push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4。esp的值现在是0xbff1c414,下一条指令把这个值传送给ebp寄存器。这两条指令合起来是把原来ebp的值保存在栈上,然后又给ebp赋了新值。在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问,例如foo函数的参数a和b分别通过ebp+8和ebp+12来访问。所以下面的指令把参数a和b再次压栈,为调用bar函数做准备,然后把返回地址压栈,调用bar函数:
               return bar(a, b);

 80483b0:              8b 45 0c                          mov    0xc(%ebp),%eax

 80483b3:              89 44 24 04                      mov    %eax,0x4(%esp)

 80483b7:              8b 45 08                         mov    0x8(%ebp),%eax

 80483ba:              89 04 24                         mov    %eax,(%esp)

 80483bd:              e8 d2 ff ff ff         call   8048394 <bar>

现在看bar函数的指令:
int bar(int c, int d)

{

 8048394:              55                                push   %ebp

 8048395:              89 e5                             mov    %esp,%ebp

 8048397:              83 ec 10                          sub    $0x10,%esp

               int e = c + d;

 804839a:              8b 55 0c                          mov    0xc(%ebp),%edx

 804839d:              8b 45 08                         mov    0x8(%ebp),%eax

 80483a0:              01 d0                             add    %edx,%eax

 80483a2:              89 45 fc                           mov    %eax,-0x4(%ebp)

这次又把foo函数的ebp压栈保存,然后给ebp赋了新值,指向bar函数栈帧的栈底,通过ebp+8和ebp+12分别可以访问参数c和d。bar函数还有一个局部变量e,可以通过ebp-4来访问。所以后面几条指令的意思是把参数c和d取出来存在寄存器中做加法,计算结果保存在eax寄存器中,再把eax寄存器存回局部变量e的内存单元。
在gdb中可以用bt命令和frame命令查看每层栈帧上的参数和局部变量,现在可以解释它的工作原理了:如果我当前在bar函数中,我可以通过ebp找到bar函数的参数和局部变量,也可以找到foo函数的ebp保存在栈上的值,有了foo函数的ebp,又可以找到它的参数和局部变量,也可以找到main函数的ebp保存在栈上的值,因此各层函数栈帧通过保存在栈上的ebp的值串起来了。
现在看bar函数的返回指令:
               return e;

 80483a5:              8b 45 fc                           mov    -0x4(%ebp),%eax

}

 80483a8:              c9                                 leave 

 80483a9:              c3                                 ret

bar函数有一个int型的返回值,这个返回值是通过eax寄存器传递的,所以首先把e的值读到eax寄存器中。然后执行leave指令,这个指令是函数开头的push %ebp和mov %esp,%ebp的逆操作:
1.    把ebp的值赋给esp,现在esp的值是0xbff1c404。
2.    现在esp所指向的栈顶保存着foo函数栈帧的ebp,把这个值恢复给ebp,同时esp增加4,esp的值变成0xbff1c408。
最后是ret指令,它是call指令的逆操作:
1.    现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4,esp的值变成0xbff1c40c。
2.    修改了程序计数器eip,因此跳转到返回地址0x80483c2继续执行。
地址0x80483c2处是foo函数的返回指令:
 80483c2:              c9                                 leave 

 80483c3:              c3                                 ret

重复同样的过程,又返回到了main函数。注意函数调用和返回过程中的这些规则:
1.    参数压栈传递,并且是从右向左依次压栈。
2.    ebp总是指向当前栈帧的栈底。
3.    返回值通过eax寄存器传递。
这些规则并不是体系结构所强加的,ebp寄存器并不是必须这么用,函数的参数和返回值也不是必须这么传,只是操作系统和编译器选择了以这样的方式实现C代码中的函数调用,这称为Calling Convention,Calling Convention是操作系统二进制接口规范(ABI,Application Binary Interface)的一部分。
习题 请点评

1、在第 2 节 “自定义函数”讲过,Old Style C风格的函数声明可以不指定参数个数和类型,这样编译器不会对函数调用做检查,那么如果调用时的参数类型不对或者参数个数不对会怎么样呢?比如把本节的例子改成这样:
int foo();

int bar();

 

int main(void)

{

               foo(2, 3, 4);

               return 0;

}

 

int foo(int a, int b)

{

               return bar(a);

}

 

int bar(int c, int d)

{

               int e = c + d;

               return e;

}

main函数调用foo时多传了一个参数,那么参数a和b分别取什么值?多的参数怎么办?foo调用bar时少传了一个参数,那么参数d的值从哪里取得?请读者利用反汇编和gdb自己分析一下。我们再看一个参数类型不符的例子:
#include <stdio.h>

 

int main(void)

{

               void foo();

               char c = 60;

               foo(c);

               return 0;

}

 

void foo(double d)

{

               printf("%f/n", d);

}

打印结果是多少?如果把声明void foo();改成void foo(double);,打印结果又是多少?
 


--------------------------------------------------------------------------------

[29] Linux内核为每个新进程指定的栈空间的起始地址都会有些不同,所以每次运行这个程序得到的地址都不一样,但通常都是0xbf??????这样一个地址。

--------------------------------------------------------------------------------


上一页
 上一级
  下一页
 
第 19 章 汇编与C之间的关系
 起始页
  2. main函数和启动例程
 


全国嵌入式人才培训基地


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/unbutun/archive/2010/12/02/6051184.aspx

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值