据我所知,C中有两种变量,堆栈变量和堆变量。堆栈变量很快并由编译器和CPU自动管理。关于堆栈变量的问题是:
答案 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)
分别为x
和y
分配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
。为此,我们首先需要在调用堆栈上推送参数x
和y
:
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之间的值(很可能)与我们的代码无关;它们只是在另一个操作中使用这些内存位置后留下的位模式。我认为编译器假定设置本地帧的空间最小。