作为程序员,您不必担心将变量压入或弹出堆栈;生成的机器代码会为您处理所有这些事情。每次调用函数时,程序都会将项目推送到硬件堆栈上。其中一些项目是您传递给函数的数据,但大多数是当前程序状态(寄存器值、返回地址等),以便当函数返回时程序可以在正确的位置继续执行。
一个例子可能会有所帮助。采用以下简单的 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
points to the top of the stack; %ebp
points into the stack frame, between the local variables and function arguments. These lines save the current value of %ebp
by pushing it onto the stack, then write the location of the current top of the stack to %ebp
, and then advance %esp
by 24 bytes (the stack grows "down", or towards decreasing addresses, on x86). Stepping through the execution of this program in a debugger on my system, we see the stack is set up as follows1:
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)
and
219 0025 C745F803 movl $3, -8(%ebp)
将 2 和 3 分配给x
and y
, 分别。请注意,这些位置被称为offsets from %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
and 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
:这次它们是正数(函数参数存储在“下面”%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)
留给我们:
Address 0x00 0x01 0x02 0x03
------- ---- ---- ---- ----
0xbfffd9d8 0xbf 0xff 0xda 0x18 <-- %ebp
0xbfffd9d4 0x00 0x00 0x00 0x02 <-- x
0xbfffd9d0 0x00 0x00 0x00 0x03 <-- y
0xbfffd9cc 0x00 0x00 0x00 0x06 <-- z
0xbfffd9c8 0xbf 0xff 0xd9 0xe8
0xbfffd9c4 0xbf 0xff 0xda 0x44
0xbfffd9c0 0x40 0x01 0x5e 0x2c <-- %esp
请注意,这是特定硬件/软件组合和特定编译器上的情况;编译器之间的细节会有所不同(最新版本的 gcc 使用寄存器将函数参数传递到任何可以的地方,而不是将它们推入堆栈),但一般概念是相同的。只是不要假设这是the做事的方式。
1. The values stored between 0xbfffd9c4 and 0xbfffd9c8 are (most likely) not related to our code; they're just the bit patterns that were left after those memory locations were used in another operation. I think the compiler assumes a minimum amount of space for setting up the local frame.