我们如何访问堆栈变量而不弹出它们?

时间:2013-11-26 16:04:15

标签: c stack heap

据我所知,C中有两种变量,堆栈变量和堆变量。堆栈变量很快并由编译器和CPU自动管理。关于堆栈变量的问题是:

  • 堆栈变量是否真的存储在堆栈FILO数据结构中?
  • 如果是这样,为什么我们可以使用它们而不会弹出它们并失去它们的价值?
  • 为什么堆栈用于存储它们?出列或列表有什么问题?

3 个答案:

答案 0 :(得分:2)

C标准没有谈论堆栈只是自动变量(局部变量,通常会在堆栈上分配)和动态变量(通过malloc等...通常会在堆上分配),也有静态变量。存储自动,动态和静态变量的地方是实现定义的行为

在大多数现代系统中,自动变量确实会存储在堆栈中,但是例如 x86 上的gcc通常只会在堆栈上分配空间然后use offsets to access堆栈而弹出推送

答案 1 :(得分:2)

在谈论堆和堆栈时,堆栈不是通常的数据结构,学生在带有指针和东西的“数据结构”讲座中重新实现。堆栈通常只是一块内存,用于为其局部变量分配内存。

Stack是伪FILO:它的基本元素是''function frame'' - 特定函数使用的所有堆栈变量的集合。在函数框架内,您可以访问所需的所有变量而不会降低性能。但是,如果没有弹出(即返回),则无法从其他功能帧访问变量。

答案 2 :(得分:2)

作为程序员,您不必担心在堆栈上推送或弹出变量;生成的机器代码处理所有这些。每次调用函数时,程序都会将项目推送到硬件堆栈。其中一些项是传递给函数的数据,但大多数是当前程序状态(寄存器值,返回地址等),这样程序可以在函数返回时在正确的位置继续执行。

一个例子可能有所帮助。采取以下简单的C程序:

#include <stdio.h>

int afunc( int a, int b )
{
  int c = a * b;
  return c;
}

int main( void )
{
  int x;
  int y;
  int z;

  x = 2;
  y = 3;

  z = afunc( x, y );
  printf( "z = %d\n", z );

  return 0;
}

在老化的Red Hat盒子上使用gcc 2.96进行编译,如下所示:

gcc -o demo -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=demo.lst demo.c

给出了以下输出列表:

   1                            .file   "demo.c"
   2                            .version        "01.01"
   5                    .text
   6                    .Ltext0:
 165                            .align 4
 169                    .globl afunc
 171                    afunc:
   1:demo.c        **** #include <stdio.h>
   2:demo.c        ****
   3:demo.c        **** int afunc( int a, int b )
   4:demo.c        **** {
 173                    .LM1:
 174                    .LBB2:
 175 0000 55                    pushl   %ebp
 176 0001 89E5                  movl    %esp, %ebp
 177 0003 83EC04                subl    $4, %esp
   5:demo.c        ****   int c = a * b;
 179                    .LM2:
 180 0006 8B4508                movl    8(%ebp), %eax
 181 0009 0FAF450C              imull   12(%ebp), %eax
 182 000d 8945FC                movl    %eax, -4(%ebp)
   6:demo.c        ****   return c;
 184                    .LM3:
 185 0010 8B45FC                movl    -4(%ebp), %eax
 186 0013 89C0                  movl    %eax, %eax
   7:demo.c        **** }
 188                    .LM4:
 189                    .LBE2:
 190 0015 C9                    leave
 191 0016 C3                    ret
 192                    .Lfe1:
 197                    .Lscope0:
 199                                    .section        .rodata
 200                    .LC0:
 201 0000 7A203D20              .string "z = %d\n"
 201      25640A00
 202                    .text
 203 0017 90                    .align 4
 205                    .globl main
 207                    main:
   8:demo.c        ****
   9:demo.c        **** int main( void )
  10:demo.c        **** {
 209                    .LM5:
 210                    .LBB3:
 211 0018 55                    pushl   %ebp
 212 0019 89E5                  movl    %esp, %ebp
 213 001b 83EC18                subl    $24, %esp
  11:demo.c        ****   int x;
  12:demo.c        ****   int y;
  13:demo.c        ****   int z;
  14:demo.c        ****
  15:demo.c        ****   x = 2;
 215                    .LM6:
 216 001e C745FC02              movl    $2, -4(%ebp)
 216      000000
  16:demo.c        ****   y = 3;
 218                    .LM7:
 219 0025 C745F803              movl    $3, -8(%ebp)
 219      000000
  17:demo.c        ****
  18:demo.c        ****   z = afunc( x, y );
 221                    .LM8:
 222 002c 83EC08                subl    $8, %esp
 223 002f FF75F8                pushl   -8(%ebp)
 224 0032 FF75FC                pushl   -4(%ebp)
 225 0035 E8FCFFFF              call    afunc
 225      FF
 226 003a 83C410                addl    $16, %esp
 227 003d 89C0                  movl    %eax, %eax
 228 003f 8945F4                movl    %eax, -12(%ebp)
  19:demo.c        ****   printf( "z = %d\n", z );
 230                    .LM9:
 231 0042 83EC08                subl    $8, %esp
 232 0045 FF75F4                pushl   -12(%ebp)
 233 0048 68000000              pushl   $.LC0
 233      00
 234 004d E8FCFFFF              call    printf
 234      FF
 235 0052 83C410                addl    $16, %esp
  20:demo.c        ****
  21:demo.c        ****   return 0;
 237                    .LM10:
 238 0055 B8000000              movl    $0, %eax
 238      00
  22:demo.c        **** }
 240                    .LM11:
 241                    .LBE3:
 242 005a C9                    leave
 243 005b C3                    ret
 244                    .Lfe2:
 251                    .Lscope1:
 253                            .text
 255                    .Letext:
 256                            .ident  "GCC: (GNU) 2.96 20000731 (Red Hat Linux 7.2 2.96-112.7.2)"

因此,从main开始,我们有以下几行:

 211 0018 55                    pushl   %ebp
 212 0019 89E5                  movl    %esp, %ebp
 213 001b 83EC18                subl    $24, %esp

%esp指向堆栈的顶部; %ebp指向堆栈框架,位于局部变量和函数参数之间。这些行通过将其推入堆栈来保存当前值%ebp,然后将堆栈当前顶部的位置写入%ebp,然后将%esp提前24个字节(堆栈在x86上“向下”或向减少地址增长。在我的系统上的调试器中逐步执行该程序,我们看到堆栈设置如下 1

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp, 0xbfffda18 is the previous value
0xbfffd9d4     0x08  0x04  0x96  0x40  <-- x
0xbfffd9d0     0x08  0x04  0x95  0x40  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c  <-- %esp

然后我们有了行

216 001e C745FC02              movl    $2, -4(%ebp)

219 0025 C745F803              movl    $3, -8(%ebp)

分别为xy分配2和3。请注意,这些位置被称为%ebp偏移。所以现在我们的堆栈看起来像这样:

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c <-- %esp

现在我们致电afunc。为此,我们首先需要在调用堆栈上推送参数xy

 222 002c 83EC08                subl    $8, %esp
 223 002f FF75F8                pushl   -8(%ebp)
 224 0032 FF75FC                pushl   -4(%ebp)

所以现在我们的堆栈看起来像

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  <-- %ebp
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c
0xbfffd9bc     0x40  0x14  0xd7  0xf0
0xbfffd9b8     0x40  0x14  0xe8  0x38
0xbfffd9b4     0x00  0x00  0x00  0x03 <-- b
0xbfffd9b0     0x00  0x00  0x00  0x02 <-- a, %esp

现在我们致电afunc。我们要做的第一件事是保存%ebp的当前值,然后再次调整我们的寄存器:

 175 0000 55                    pushl   %ebp
 176 0001 89E5                  movl    %esp, %ebp
 177 0003 83EC04                subl    $4, %esp

离开我们

Address        0x00  0x01  0x02  0x03    
-------        ----  ----  ----  ----
0xbfffd9d8     0xbf  0xff  0xda  0x18  
0xbfffd9d4     0x00  0x00  0x00  0x02  <-- x
0xbfffd9d0     0x00  0x00  0x00  0x03  <-- y
0xbfffd9cc     0x08  0x04  0x84  0x41  <-- z
0xbfffd9c8     0xbf  0xff  0xd9  0xe8
0xbfffd9c4     0xbf  0xff  0xda  0x44
0xbfffd9c0     0x40  0x01  0x5e  0x2c 
0xbfffd9bc     0x40  0x14  0xd7  0xf0
0xbfffd9b8     0x40  0x14  0xe8  0x38
0xbfffd9b4     0x00  0x00  0x00  0x03 <-- b
0xbfffd9b0     0x00  0x00  0x00  0x02 <-- a
0xbfffd9ac     0x08  0x04  0x84  0x9a 
0xbfffd9a8     0xbf  0xff  0xd9  0xd8 <-- %ebp
0xbfffd9a4     0x40  0x14  0xd7  0xf0 <-- c, %esp

现在我们在afunc中执行计算:

 180 0006 8B4508                movl    8(%ebp), %eax
 181 0009 0FAF450C              imull   12(%ebp), %eax
 182 000d 8945FC                movl    %eax, -4(%ebp)

注意相对于%ebp的偏移:这次它们是正的(函数参数存储在“{1}}下面”,其中局部变量存储在“它上面”。然后将结果存储在%ebp

c

函数返回值存储在寄存器Address 0x00 0x01 0x02 0x03 ------- ---- ---- ---- ---- 0xbfffd9d8 0xbf 0xff 0xda 0x18 0xbfffd9d4 0x00 0x00 0x00 0x02 <-- x 0xbfffd9d0 0x00 0x00 0x00 0x03 <-- y 0xbfffd9cc 0x08 0x04 0x84 0x41 <-- z 0xbfffd9c8 0xbf 0xff 0xd9 0xe8 0xbfffd9c4 0xbf 0xff 0xda 0x44 0xbfffd9c0 0x40 0x01 0x5e 0x2c 0xbfffd9bc 0x40 0x14 0xd7 0xf0 0xbfffd9b8 0x40 0x14 0xe8 0x38 0xbfffd9b4 0x00 0x00 0x00 0x03 <-- b 0xbfffd9b0 0x00 0x00 0x00 0x02 <-- a 0xbfffd9ac 0x08 0x04 0x84 0x9a 0xbfffd9a8 0xbf 0xff 0xd9 0xd8 <-- %ebp 0xbfffd9a4 0x00 0x00 0x00 0x06 <-- c, %esp 中。现在我们从函数返回:

%eax

当我们从函数返回时,在我们输入 185 0010 8B45FC movl -4(%ebp), %eax 186 0013 89C0 movl %eax, %eax 190 0015 C9 leave 191 0016 C3 ret 之前,我们将所有内容从堆栈中弹回到%esp指向的位置(那里有一些魔法,但是认识到afunc指向到包含旧值%ebp)的地址:

%ebp

现在我们将结果保存到Address 0x00 0x01 0x02 0x03 ------- ---- ---- ---- ---- 0xbfffd9d8 0xbf 0xff 0xda 0x18 <-- %ebp 0xbfffd9d4 0x00 0x00 0x00 0x02 <-- x 0xbfffd9d0 0x00 0x00 0x00 0x03 <-- y 0xbfffd9cc 0x08 0x04 0x84 0x41 <-- z 0xbfffd9c8 0xbf 0xff 0xd9 0xe8 0xbfffd9c4 0xbf 0xff 0xda 0x44 0xbfffd9c0 0x40 0x01 0x5e 0x2c <-- %esp

z

离开我们:

 228 003f 8945F4                movl    %eax, -12(%ebp)

请注意,这是特定硬件/软件组合和特定编译器的外观。编译器之间的细节会有所不同(最新版本的gcc使用寄存器在任何可能的地方传递函数参数,而不是将它们推送到堆栈),但一般概念将是相同的。只是不要认为这是完成事情的 方式。

<小时/> 1。存储在0xbfffd9c4和0xbfffd9c8之间的值(很可能)与我们的代码无关;它们只是在另一个操作中使用这些内存位置后留下的位模式。我认为编译器假定设置本地帧的空间最小。