现在的位置: 首页 > 编程语言 > 正文

函数调用

2018年02月24日 编程语言 ⁄ 共 2336字 ⁄ 字号 评论关闭

函数调用

前言

  本来打算在写完 数组、结构体、指针 等之后再写函数调用的,因为函数调用会牵扯到这些东西,但是我觉得那样做的话总会露出点意犹未尽的马脚,所以还是先简单地分析一下函数调用吧,之后再不断的完善函数调用这个大家伙。

C源程序(double.c)

#include <stdio.h>

int Double(int b)
{
    int c;

    c = b + b;
    ++b;    // 会影响到 a 吗?

    return c;
}

int main()
{
    int a = 1;
    int d = Double(a);

    printf("a:%d d:%d\n", a, d);
    return 0;
}

反汇编

gcc -S double.c

  gcc -S double.c默认就是把汇编源代码输出到 double.s 中,之前一直用 -o 选项是为了避免过多的解释O(∩_∩)O~, double.s 中的内容简化后:

Double:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    8(%ebp), %eax
    addl    %eax, %eax
    movl    %eax, -4(%ebp)
    addl    $1, 8(%ebp)
    movl    -4(%ebp), %eax
    leave
    ret

main:
    pushl   %ebp
    movl    %esp, %ebp
    andl    $-16, %esp
    subl    $32, %esp
    movl    $1, 28(%esp)
    movl    28(%esp), %eax
    movl    %eax, (%esp)
    call    Double
    movl    %eax, 24(%esp)
    movl    $.LC0, %eax
    movl    24(%esp), %edx
    movl    %edx, 8(%esp)
    movl    28(%esp), %edx
    movl    %edx, 4(%esp)
    movl    %eax, (%esp)
    call    printf
    movl    $0, %eax
    leave
    ret 

分析

  其中包含了对 Double 函数的一次完整调用。

  现在从第18行开始分析:


    movl    $1, 28(%esp)    # a=1
这是给 局部变量 a 赋值
假设栈指针寄存器 esp 现在的值是 8000,这条指令执行完后栈的情况如右图

    movl    28(%esp), %eax
    movl    %eax, (%esp)    # (%esp) = a
这两条指令将 a 的值 1 写入了地址为 8000 的内存块(4字节),这可能是 b 吧?
现在还不要妄下结论。

    call    Double
call 指令的执行,可分为两个步骤:
  1. 将 call 指令的下一条指令的地址压栈
  2. 将 eip 修改为 Double 的地址

然后就跳转到 Double 处取指令执行了。
假设 call 指令之后的那条 movl %eax, 24(%esp) 的地址是 1000,那么 call 执行后,栈的情况会变为右图的样子


    pushl   %ebp
    movl    %esp, %ebp
先将旧的 ebp 压栈,然后把这个时候的 esp 赋值给 ebp,后面会看到这样做的目地

    subl    $16, %esp   # esp -= 16
Double 的局部变量空间就这么被开拓了(虽然有点浪费)

    movl    8(%ebp), %eax
    addl    %eax, %eax
    movl    %eax, -4(%ebp)  # c = b + b
    addl    $1, 8(%ebp)     # ++b
8(%ebp) 当前刚好表示 8000 内存块,从这 4 条指令对 8000 内存块的值进行的处理,我们可以确定 8000 就是参数 b,而 -4(%ebp) 则是 c。

    movl    -4(%ebp), %eax  # eax = c
    leave
    ret
这就是最后的收尾部分了,函数的返回值要存储在累加寄存器 eax 中, leave 等同于以下两条指令:
movl %ebp, %esp
popl %ebp
与刚进入 Double 时的两条指针首尾呼应。
结果就是局部空间被回收了,ebp 也复原了。

而 ret 则相当于 popl %eip,程序就继续执行 call 指令之后的指令了。

虽然 Double 函数的局部空间被回收了,但是其中的值还是保持不变的,一直到之后调用 printf 函数的时候, Double 函数的局部变量以及参数的内容才被覆盖。

整理

  一步步的分析结束后,再从大粒度上回顾一次 Double 函数:

Double:
    pushl   %ebp                #----------帧指针 ebp 切换
    movl    %esp, %ebp          #---------/
    subl    $16, %esp           #----------开拓局部变量空间
    movl    8(%ebp), %eax       #\
    addl    %eax, %eax          #-\
    movl    %eax, -4(%ebp)      #-C 的操作
    addl    $1, 8(%ebp)         #/
    movl    -4(%ebp), %eax      #-----将返回值保存到 eax 寄存器
    leave                       #--------\
    ret                         #---------退栈、恢复帧指针、返回

  其他函数编译为汇编后,指令也可以这样来进行划分。

  由于 Double 函数太过简单,所以没有出现保护寄存器的指令(eax 寄存器不需要保护,每个函数都知道它是用来存返回值的,在函数的最后部分肯定会被修改),复杂的函数会在函数的最前面 pushl 将被修改的寄存器,而在 ret 之前 popl 寄存器以恢复原来的值。

小结

  通过对 Double 函数的分析,我们注意到 ebp 跟 esp 的作用类似,函数中经常也通过 ebp + 偏移 的方式来访问 参数 和 局部变量,现在可以向大家承诺了:esp 或 ebp 加偏移就是局部变量最后的表现形式。关于 帧指针寄存器 ebp 后面会再出一篇来进行分析。

  同时我们也可以明显地看出值传递的过程:调用前将 a 复制到 b(值拷贝,它们分别是不同的内存块),函数调用完后 b 直接被忽略了,也不会再拷贝回 a,所以虽然我在 Double 中 ++b 了,而 a 的值仍然为 1。程序的运行结果如下:

[lqy@localhost temp]$ gcc -o double double.c
[lqy@localhost temp]$ ./double
a:1 d:2
[lqy@localhost temp]$ 

  下一篇中再继续讨论值传递。

【上篇】
【下篇】

抱歉!评论已关闭.