为什么函数堆栈帧中的参数、变量和帧指针之间存在间隙?
Why are there gaps between arguments, variables and the frame pointer in a function stack frame?
我有以下c程序:
void function(int a, int b, int c) {
char buffer1[]="aaaaa";
char buffer2[]="bbbbbbbbbb";
}
int main() {
function(1,2,3);
return 0;
}
当我在执行函数时打印帧信息时,我得到以下 gdb 输出:
(gdb) info frame
Stack level 0, frame at 0x7fffffffe1c0:
rip = 0x40119b in function (ss1.c:4); saved rip = 0x4011ca
called by frame at 0x7fffffffe1d0
source language c.
Arglist at 0x7fffffffe1b0, args: a=1, b=2, c=3
Locals at 0x7fffffffe1b0, Previous frame's sp is 0x7fffffffe1c0
Saved registers:
rbp at 0x7fffffffe1b0, rip at 0x7fffffffe1b8
(gdb)
当打印函数参数和局部变量的地址时,我得到:
(gdb) p/x &c
= 0x7fffffffe184
(gdb) p/x &b
= 0x7fffffffe188
(gdb) p/x &a
= 0x7fffffffe18c
(gdb) p/x &buffer1
= 0x7fffffffe197
(gdb) p/x &buffer2
= 0x7fffffffe19d
为什么 arg a 的地址和 var buffer1 的地址之间有 11 个字节的间隙 - 而不仅仅是 a 大小的 4 个字节的间隙?
为什么buffer2的地址和帧指针(0x7ffffffffe1b0)之间有19字节的间隔,而不仅仅是buffer2大小的11字节的间隔?
谢谢
只是运行简单的程序:
#include <stdio.h>
void function(int a, int b, int c)
{
char buffer1[]="aaaaa";
char buffer2[]="bbbbbbbbbb";
printf("%p = &a\n", &a);
printf("%p = &b\n", &b);
printf("%p = &c\n", &c);
printf("%p = &buffer1 sizeof(buffer1) = %zu\n", buffer1, sizeof(buffer1));
printf("%p = &buffer2 sizeof(buffer2) = %zu\n", buffer2, sizeof(buffer2));
printf("%zu = &buffer - &a\n", (char *)buffer1 - (char *)&a);
}
int main()
{
function(1,2,3);
return 0;
}
结果完全符合预期。
0x7fff9d9d830c = &a
0x7fff9d9d8308 = &b
0x7fff9d9d8304 = &c
0x7fff9d9d8310 = &buffer1 sizeof(buffer1) = 6
0x7fff9d9d8320 = &buffer2 sizeof(buffer2) = 11
4 = &buffer - &a
尝试在您的系统上运行它。
这应该让您走上正确的道路,但没有回答实际的差距:
- 堆栈向下增长,而您正试图向上读取它
- 您看到的
&a
、&b
和 &c
不是传递的参数,而是可以使用的本地存储,如果(例如)您说 a=1
在 function()
内。我相信如果你不这样做,这些会得到优化 -O0
a
、b
和 c
通过寄存器而不是堆栈传递到函数,因此您不会在其中两次找到它们。 IE。调用者不会将它们压入堆栈。
buffer1
和 buffer2
在堆栈中未对齐,并作为字符串打包在一起。
例如在 buffer2
之后(之前),您可以找到保存的 RBP
值,然后是 return 地址。对我来说:
(gdb) p &buffer1
2 = (char (*)[6]) 0x7fffffffde82
(gdb) p &buffer2
3 = (char (*)[11]) 0x7fffffffde77
(buffer1
结束于 0x7fffffffde87
)
然后保存RBP
:
(gdb) p/x (char [8]) *0x7fffffffde88
4 = {0xb0, 0xde, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0}
然后是 return 地址:
(gdb) p/x (char [8]) *0x7fffffffde90
5 = {0x80, 0x51, 0x55, 0x55, 0x55, 0x55, 0x0, 0x0}
你也可以从gdb中看到:
(gdb) info frame
Stack level 0, frame at 0x7fffffffde98:
rip = 0x55555555513f in function (c.c:3); saved rip = 0x555555555180
^^^^^^^^^^^^^^
called by frame at 0x7fffffffdec0
source language c.
Arglist at 0x7fffffffde88, args: a=1, b=2, c=3
Locals at 0x7fffffffde88, Previous frame's sp is 0x7fffffffde98
Saved registers:
rbp at 0x7fffffffde88, rip at 0x7fffffffde90
^^^^^^^^^^^^^^^^^^^^^^
看汇编代码也能看出来:
gcc -S c.c -o c.s
或者如果您更喜欢英特尔:
gcc -masm=intel -S c.c -o c.s
我不知道为什么 gcc 会留下这个空白:
mov DWORD PTR -36[rbp], edi
mov DWORD PTR -40[rbp], esi
mov DWORD PTR -44[rbp], edx
mov DWORD PTR -6[rbp], 1633771873 <-- aaaa
mov WORD PTR -2[rbp], 97 <-- a[=16=]
movabs rax, 7089336938131513954 <-- bbbbbbbb
mov QWORD PTR -17[rbp], rax
mov WORD PTR -9[rbp], 25186 <-- bb
mov BYTE PTR -7[rbp], 0 <-- [=16=]
编译器通常遵循效率定义的 ABI 规范,以优化通过寄存器传递的参数、对齐和 space 代码中可能的深层嵌套表达式。例如,intel ABI 规范说堆栈指针在参数为 16 字节的倍数的函数调用上扩展,所以所有类型的对齐都是可以的。所以,很正常的看到,在入口处,局部变量的space只保留了一个SP减法,然后开始执行代码,直到我们需要另一块space。 ABI 规范说明哪些寄存器用于传递参数,哪些在调用中必须遵守,哪些可以销毁,哪些用于 link 堆栈帧(通常是英特尔中的 EBP)等。这允许编译器相互依赖(语言之间的接口),同时实现优化的代码并确保程序效率。这就是您在 entry/exit 过程调用中看到一些明显的堆栈内存丢失的原因。
我有以下c程序:
void function(int a, int b, int c) {
char buffer1[]="aaaaa";
char buffer2[]="bbbbbbbbbb";
}
int main() {
function(1,2,3);
return 0;
}
当我在执行函数时打印帧信息时,我得到以下 gdb 输出:
(gdb) info frame
Stack level 0, frame at 0x7fffffffe1c0:
rip = 0x40119b in function (ss1.c:4); saved rip = 0x4011ca
called by frame at 0x7fffffffe1d0
source language c.
Arglist at 0x7fffffffe1b0, args: a=1, b=2, c=3
Locals at 0x7fffffffe1b0, Previous frame's sp is 0x7fffffffe1c0
Saved registers:
rbp at 0x7fffffffe1b0, rip at 0x7fffffffe1b8
(gdb)
当打印函数参数和局部变量的地址时,我得到:
(gdb) p/x &c
= 0x7fffffffe184
(gdb) p/x &b
= 0x7fffffffe188
(gdb) p/x &a
= 0x7fffffffe18c
(gdb) p/x &buffer1
= 0x7fffffffe197
(gdb) p/x &buffer2
= 0x7fffffffe19d
为什么 arg a 的地址和 var buffer1 的地址之间有 11 个字节的间隙 - 而不仅仅是 a 大小的 4 个字节的间隙?
为什么buffer2的地址和帧指针(0x7ffffffffe1b0)之间有19字节的间隔,而不仅仅是buffer2大小的11字节的间隔?
谢谢
只是运行简单的程序:
#include <stdio.h>
void function(int a, int b, int c)
{
char buffer1[]="aaaaa";
char buffer2[]="bbbbbbbbbb";
printf("%p = &a\n", &a);
printf("%p = &b\n", &b);
printf("%p = &c\n", &c);
printf("%p = &buffer1 sizeof(buffer1) = %zu\n", buffer1, sizeof(buffer1));
printf("%p = &buffer2 sizeof(buffer2) = %zu\n", buffer2, sizeof(buffer2));
printf("%zu = &buffer - &a\n", (char *)buffer1 - (char *)&a);
}
int main()
{
function(1,2,3);
return 0;
}
结果完全符合预期。
0x7fff9d9d830c = &a
0x7fff9d9d8308 = &b
0x7fff9d9d8304 = &c
0x7fff9d9d8310 = &buffer1 sizeof(buffer1) = 6
0x7fff9d9d8320 = &buffer2 sizeof(buffer2) = 11
4 = &buffer - &a
尝试在您的系统上运行它。
这应该让您走上正确的道路,但没有回答实际的差距:
- 堆栈向下增长,而您正试图向上读取它
- 您看到的
&a
、&b
和&c
不是传递的参数,而是可以使用的本地存储,如果(例如)您说a=1
在function()
内。我相信如果你不这样做,这些会得到优化 -O0 a
、b
和c
通过寄存器而不是堆栈传递到函数,因此您不会在其中两次找到它们。 IE。调用者不会将它们压入堆栈。buffer1
和buffer2
在堆栈中未对齐,并作为字符串打包在一起。
例如在 buffer2
之后(之前),您可以找到保存的 RBP
值,然后是 return 地址。对我来说:
(gdb) p &buffer1
2 = (char (*)[6]) 0x7fffffffde82
(gdb) p &buffer2
3 = (char (*)[11]) 0x7fffffffde77
(buffer1
结束于 0x7fffffffde87
)
然后保存RBP
:
(gdb) p/x (char [8]) *0x7fffffffde88
4 = {0xb0, 0xde, 0xff, 0xff, 0xff, 0x7f, 0x0, 0x0}
然后是 return 地址:
(gdb) p/x (char [8]) *0x7fffffffde90
5 = {0x80, 0x51, 0x55, 0x55, 0x55, 0x55, 0x0, 0x0}
你也可以从gdb中看到:
(gdb) info frame
Stack level 0, frame at 0x7fffffffde98:
rip = 0x55555555513f in function (c.c:3); saved rip = 0x555555555180
^^^^^^^^^^^^^^
called by frame at 0x7fffffffdec0
source language c.
Arglist at 0x7fffffffde88, args: a=1, b=2, c=3
Locals at 0x7fffffffde88, Previous frame's sp is 0x7fffffffde98
Saved registers:
rbp at 0x7fffffffde88, rip at 0x7fffffffde90
^^^^^^^^^^^^^^^^^^^^^^
看汇编代码也能看出来:
gcc -S c.c -o c.s
或者如果您更喜欢英特尔:
gcc -masm=intel -S c.c -o c.s
我不知道为什么 gcc 会留下这个空白:
mov DWORD PTR -36[rbp], edi
mov DWORD PTR -40[rbp], esi
mov DWORD PTR -44[rbp], edx
mov DWORD PTR -6[rbp], 1633771873 <-- aaaa
mov WORD PTR -2[rbp], 97 <-- a[=16=]
movabs rax, 7089336938131513954 <-- bbbbbbbb
mov QWORD PTR -17[rbp], rax
mov WORD PTR -9[rbp], 25186 <-- bb
mov BYTE PTR -7[rbp], 0 <-- [=16=]
编译器通常遵循效率定义的 ABI 规范,以优化通过寄存器传递的参数、对齐和 space 代码中可能的深层嵌套表达式。例如,intel ABI 规范说堆栈指针在参数为 16 字节的倍数的函数调用上扩展,所以所有类型的对齐都是可以的。所以,很正常的看到,在入口处,局部变量的space只保留了一个SP减法,然后开始执行代码,直到我们需要另一块space。 ABI 规范说明哪些寄存器用于传递参数,哪些在调用中必须遵守,哪些可以销毁,哪些用于 link 堆栈帧(通常是英特尔中的 EBP)等。这允许编译器相互依赖(语言之间的接口),同时实现优化的代码并确保程序效率。这就是您在 entry/exit 过程调用中看到一些明显的堆栈内存丢失的原因。