C语言 — 函数调用过程。

本文从代码的角度来理解一下函数调用过程。

看例子:
#include <stdio.h>

     int add();
    
     int main()
     {
          int sum = 0;
         
          sum = add(1, 2, 3);
          printf("%d\n", sum);

          return 0;
     }

     int add(int a, int b)
     {
          int c = a + b;
          return c;
     }
我们通过gcc -g fun.c -o fun编译,然后通过objdump工具反汇编查看本程序对应的汇编代码。
本程序对应的部分汇编代码如下:
080483e4 <main>:
     #include <stdio.h>
     int add();
     int main()
     {
     80483e4:     55                        push   %ebp
     80483e5:     89 e5                     mov    %esp,%ebp
     80483e7:     83 e4 f0                  and    $0xfffffff0,%esp
     80483ea:     83 ec 20                  sub    $0x20,%esp
          int sum = 0;
     80483ed:     c7 44 24 1c 00 00 00      movl   $0x0,0x1c(%esp)
     80483f4:     00
          int a = 1, b = 2;    
     80483f5:     c7 44 24 18 01 00 00      movl   $0x1,0x18(%esp)
     80483fc:     00
     80483fd:     c7 44 24 14 02 00 00      movl   $0x2,0x14(%esp)
     8048404:     00
          sum = add(a, b);    
     8048405:     8b 44 24 14               mov    0x14(%esp),%eax
     8048409:     89 44 24 04               mov    %eax,0x4(%esp)
     804840d:     8b 44 24 18               mov    0x18(%esp),%eax
     8048411:     89 04 24                  mov    %eax,(%esp)
     8048414:     e8 20 00 00 00            call   8048439 <add>
     8048419:     89 44 24 1c               mov    %eax,0x1c(%esp)

          printf("%d\n", sum);
     804841d:     b8 20 85 04 08            mov    $0x8048520,%eax
     8048422:     8b 54 24 1c               mov    0x1c(%esp),%edx
     8048426:     89 54 24 04               mov    %edx,0x4(%esp)
     804842a:     89 04 24                  mov    %eax,(%esp)
     804842d:     e8 ea fe ff ff            call   804831c <printf@plt>

          return 0;
     8048432:     b8 00 00 00 00            mov    $0x0,%eax
     }
     8048437:     c9                        leave 
     8048438:     c3                        ret   

     08048439 <add>:

     int add(int a, int b)
     {
     8048439:     55                        push   %ebp
     804843a:     89 e5                     mov    %esp,%ebp
     804843c:     83 ec 10                  sub    $0x10,%esp
          int c = 0;
     804843f:     c7 45 fc 00 00 00 00      movl   $0x0,-0x4(%ebp)

          c = a + b;
     8048446:     8b 45 0c                  mov    0xc(%ebp),%eax
     8048449:     8b 55 08                  mov    0x8(%ebp),%edx
     804844c:     8d 04 02                  lea    (%edx,%eax,1),%eax
     804844f:     89 45 fc                  mov    %eax,-0x4(%ebp)
          return c;
     8048452:     8b 45 fc                  mov    -0x4(%ebp),%eax
     }
     8048455:     c9                        leave 
     8048456:     c3                        ret

在程序执行时,操作系统为进程分配一块栈空间来保存函数栈帧,esp寄存器总是指向栈顶,在x86平台上这个栈是从高地址向低
地址增长的,我们知道每次调用一个函数都要分配一个栈帧来保存参数和局部变量,现在我们详细分析这些数据在栈空间的布局。
1. 我们从main函数开始分析,步骤如下;
     (1)ebp压栈,然后将esp的值赋值给ebp,将esp的低四位置0,esp指向ebp-0x20的地址。这里已经为main开辟好了栈帧;
     (2)main函数将自己的局部变量sum存放到esp+0x1c的地址,a存放在esp+0x18并初始化为1,b存放在esp+0x14并初始化为2;
     (3)2条movl指令为调用add函数准备好参数。esp+4的地址存放2,在esp指向的地址存放1.从这里可以看出函数参数是从右向左压栈的;
               sum = add(a, b);    
          8048405:     8b 44 24 14               mov    0x14(%esp),%eax
          8048409:     89 44 24 04               mov    %eax,0x4(%esp)
          804840d:     8b 44 24 18               mov    0x18(%esp),%eax
          8048411:     89 04 24                  mov    %eax,(%esp)
          8048414:     e8 20 00 00 00            call   8048439 <add>
          8048419:     89 44 24 1c               mov    %eax,0x1c(%esp)
     (4)通过call指令调用0x8048421地址处的add函数。
     call指令的作用:
          a)因为add函数执行完后要返回到call指令的下一条指令继续执行,所以把call的下一条指令的地址0x8048419压栈,也就是返回地址.同时esp的值-4。
          b)修改程序计数器eip,跳转到add函数开头执行。

2. 从main函数call   8048421 <add>后,开始执行add函数,步骤如下:
     (1)push %ebp指令把ebp寄存器的值压栈,同时把esp的值减4,然后将esp的值赋值给ebp,此时,ebp保存了main函数的esp,esp指向ebp-0x10的地址。这里已经为add开辟好了栈帧;
     (2)将add函数自己的局部变量c存放在ebp-4的地址,并初始化为0;
     (3)取得ebp+0xc和ebp+0x8的值,也即b和a分别放在eax和edx中,将计算的结果int c = a + b;在存放到sum中。
               int c = a + b;
          804842f:     8b 45 0c                  mov    0xc(%ebp),%eax
          8048432:     8b 55 08                  mov    0x8(%ebp),%edx
          804844f:     89 45 fc                  mov    %eax,-0x4(%ebp)
     (4)将add函数的返回值存放到eax寄存器中,返回值通过eax寄存器传递。return c;
          8048452:     8b 45 fc                  mov    -0x4(%ebp),%eax
     (5)执行leave指令。这个指令是函数开头的push %ebp和mov %esp,%ebp 的逆操作:
          a. 把ebp的值赋给esp;
          b. 现在esp所指向的栈顶保存着add函数栈帧的ebp,把这个值恢复给ebp,同时esp增加4。
     (6)执行指令ret。它是call指令的逆操作:
          a. 现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp+4;
          b. 修改了程序计数器eip ,因此跳转到返回地址0x8048419继续执行。
         
最后调用printf,main函数返回。

整个函数调用过程的示例图如下:



     总结:在每个函数的栈帧中,ebp指向栈底,而esp指向栈顶,在函数执行过程中esp 随着压栈和出栈操作随时变化,而ebp是不动的,
函数的参数和局部变量都是通过ebp的值加上一个偏移量来访问。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值