C语言栈帧的组织

  • Post category:C

C语言栈帧是函数调用时的一个关键概念。它是一块内存,存储了函数调用时的参数、局部变量以及返回地址等重要信息。本篇攻略将详细讲解C语言栈帧的组织,以及如何在实际开发中使用它。

什么是C语言栈帧

C语言栈帧是函数调用时的内存布局,它有以下几个重要组成部分:

  • 参数空间:用于存储函数调用时的参数。在调用函数时,这些参数会被依次压入栈中。

  • 局部变量空间:用于存储函数内部定义的变量,包括定义在函数内部的变量和在函数嵌套的内部函数内部定义的变量。

  • 返回地址:用于存储函数返回后的下一条指令的地址。

  • 帧指针:用于保存当前栈帧的首地址,方便访问栈上的参数和局部变量。

栈帧的组织方式

栈帧的组织方式有两种不同的方式:传统的方式和基于硬件的方式。传统的方式是指使用帧指针作为栈帧的基地址,而基于硬件的方式是指使用栈指针来定位栈帧。

传统的栈帧组织方式

下面是使用传统的方式组织栈帧的示例代码:

void foo(int x, int y) {
    int z = x + y;
    printf("%d\n", z);
}

int main() {
    foo(1, 2);
    return 0;
}

使用gcc编译上述代码,并使用objdump命令分析编译后的可执行文件:

$ gcc -g -o example example.c
$ objdump -d example

可以看到以下i386汇编代码,expample.c中foo函数的具体实现:

08048414 <foo>:
 8048414:   55                      push   %ebp
 8048415:   89 e5                   mov    %esp,%ebp
 8048417:   8b 55 08                mov    0x8(%ebp),%edx
 804841a:   8b 45 0c                mov    0xc(%ebp),%eax
 804841d:   01 d0                   add    %edx,%eax
 804841f:   89 45 fc                mov    %eax,-0x4(%ebp)
 8048422:   8b 45 fc                mov    -0x4(%ebp),%eax
 8048425:   89 04 24                mov    %eax,(%esp)
 8048428:   e8 d3 fe ff ff          call   8048300 <printf@plt>
 804842d:   90                      nop
 804842e:   5d                      pop    %ebp
 804842f:   c3                      ret    

08048430 <main>:
 8048430:   55                      push   %ebp
 8048431:   89 e5                   mov    %esp,%ebp
 8048433:   83 ec 10                sub    $0x10,%esp
 8048436:   68 02 00 00 00          push   $0x2
 804843b:   68 01 00 00 00          push   $0x1
 8048440:   e8 cf ff ff ff          call   8048414 <foo>
 8048445:   b8 00 00 00 00          mov    $0x0,%eax
 804844a:   c9                      leave  
 804844b:   c3                      ret    

在foo函数的汇编代码中,可以看到以下内容:

08048414 <foo>:
 8048414:   55                      push   %ebp         ; 保存旧的栈帧基址
 8048415:   89 e5                   mov    %esp,%ebp    ; 设置新的栈帧基址
 8048417:   8b 55 08                mov    0x8(%ebp),%edx ; 读取函数的第一个参数
 804841a:   8b 45 0c                mov    0xc(%ebp),%eax ; 读取函数的第二个参数
 804841d:   01 d0                   add    %edx,%eax      ; 将两个参数相加
 804841f:   89 45 fc                mov    %eax,-0x4(%ebp) ; 将结果存储到局部变量z中
 8048422:   8b 45 fc                mov    -0x4(%ebp),%eax ; 将局部变量z的值保存到eax寄存器中
 8048425:   89 04 24                mov    %eax,(%esp) ; 将z的值压入栈中,准备调用printf函数
 8048428:   e8 d3 fe ff ff          call   8048300 <printf@plt> ; 调用printf函数
 804842d:   90                      nop              ; 不执行任何操作
 804842e:   5d                      pop    %ebp         ; 恢复旧的栈帧基址
 804842f:   c3                      ret              ; 返回

上述汇编代码中,函数参数x和y的值存储在栈帧中,被函数使用时会被读取出来。函数中定义的局部变量z的值也存储在栈帧中。

基于硬件的栈帧组织方式

如下是基于硬件的方式组织栈帧的示例代码:

void foo(int x, int y) {
    int z = x + y;
    printf("%d\n", z);
}

int main() {
    __asm__ __volatile__ (
        "movl $1, %ecx;"
        "movl $2, %edx;"
        "call foo;"
    );
    return 0;
}

使用gcc编译上述代码后,使用objdump命令分析编译后的可执行文件:

$ gcc -g -o example example.c
$ objdump -d example

可以看到以下i386汇编代码:

08048440 <foo>:
 8048440:   55                      push   %ebp         ; 保存旧的栈帧基址
 8048441:   89 e5                   mov    %esp,%ebp    ; 设置新的栈帧基址
 8048443:   8b 55 08                mov    0x8(%ebp),%edx ; 读取函数的第一个参数
 8048446:   8b 45 0c                mov    0xc(%ebp),%eax ; 读取函数的第二个参数
 8048449:   01 d0                   add    %edx,%eax      ; 将两个参数相加
 804844b:   89 45 fc                mov    %eax,-0x4(%ebp) ; 将结果存储到局部变量z中
 804844e:   8b 45 fc                mov    -0x4(%ebp),%eax ; 将局部变量z的值保存到eax寄存器中
 8048451:   89 04 24                mov    %eax,(%esp) ; 将z的值压入栈中,准备调用printf函数
 8048454:   e8 e7 fe ff ff          call   8048340 <printf@plt> ; 调用printf函数
 8048459:   5d                      pop    %ebp         ; 恢复旧的栈帧基址
 804845a:   c3                      ret              ; 返回

0804845b <main>:
 804845b:   b9 02 00 00 00          mov    $0x2,%ecx     ; 将参数1压入ecx寄存器中
 8048460:   ba 01 00 00 00          mov    $0x1,%edx     ; 将参数2压入edx寄存器中
 8048465:   e8 d6 ff ff ff          call   8048440 <foo> ; 调用foo函数
 804846a:   b8 00 00 00 00          mov    $0x0,%eax
 804846f:   c3                      ret   

在main函数的汇编代码中,可以看到以下内容:

0804845b <main>:
 804845b:   b9 02 00 00 00          mov    $0x2,%ecx        ; 将参数1压入ecx寄存器中
 8048460:   ba 01 00 00 00          mov    $0x1,%edx        ; 将参数2压入edx寄存器中
 8048465:   e8 d6 ff ff ff          call   8048440 <foo>   ; 调用foo函数

在基于硬件的栈帧组织方式中,传入参数时,直接将参数压入相应的寄存器中,然后调用函数。函数中的参数就可以从相应的寄存器中获取。这种方式可以避免使用传统的方式中的栈访问指令。这样使得函数调用更加高效。

如何使用C语言栈帧

在实际开发中,C语言栈帧提供了一种非常有用的手段,在函数调用时可以保存参数、局部变量和返回地址等重要信息。使用C语言栈帧时,必须注意以下几点:

  • 栈帧的大小方案-计算栈帧大小时,必须考虑函数所有的参数、局部变量和返回地址。在传统的栈帧组织方式中,栈帧大小由栈指针和帧指针来控制;在基于硬件的组织方式中,栈帧大小取决于传递给函数的参数和函数内部的局部变量。

  • 存储顺序-存储顺序是影响栈帧的最主要因素之一。在传统的栈帧组织方式中,参数和返回地址的存储顺序是从右到左;而在基于硬件的组织方式中,参数和返回地址的存储顺序取决于特定的硬件架构。

  • 栈指针-在使用C语言栈帧时,必须了解栈指针的概念。在大多数情况下,栈指针指向栈的最后一个元素。但是,在进行函数调用时,栈指针会不断上移或下移,并移到空闲位置上。

下面是一个使用C语言栈帧的示例,展示如何在使用传统的栈帧组织方式时使用栈:

#include <stdio.h>

void foo(int x, int y) {
    int z = x + y;
    printf("%d\n", z);
}

int main() {
    int a = 1, b = 2, c = 3, d = 4;
    int esp_old;
    __asm__ __volatile__ (
        "movl %%esp, %0;"
        "subl $48, %%esp;"
        "movl %4, 0(%%esp);"
        "movl %5, 4(%%esp);"
        "movl %6, 8(%%esp);"
        "movl %7, 12(%%esp);"
        "call foo;"
        "movl %0, %%esp;"
        : "=r"(esp_old) // 输出操作数
        : "a" (a), "b" (b), "c" (c), "d" (d) // 输入操作数
        : "memory" // %esp的值在内存中被修改
    );
    return 0;
}

在上述示例中,我们展示了如何使用C语言栈帧来调用函数。使用C语言栈帧可以将堆栈的大小减小到最小,同时提高函数调用的效率,具有很强的实用性。