调用函数激活记录时,C究竟使用了多少堆栈空间?

调用函数激活记录时,C究竟使用了多少堆栈空间?,c,memory-management,compiler-optimization,C,Memory Management,Compiler Optimization,环境:Windows10上的gcc版本6.3.0 MinGW.org gcc-6.3.0-1 我在命令行中编译和运行代码 这是我的密码: 函数中的堆栈顶部和函数外的堆栈顶部之间的间隙大小为48字节 然后我将arr的大小更改为1,结果是: stack top before func 0061FF28 stack top in func 0061FEFC stack top after func 0061FF24 间隙刚刚缩小,在运行时堆栈顶部保持不变。间隙大小现在为44字节

环境:Windows10上的gcc版本6.3.0 MinGW.org gcc-6.3.0-1

我在命令行中编译和运行代码

这是我的密码:

函数中的堆栈顶部和函数外的堆栈顶部之间的间隙大小为48字节

然后我将arr的大小更改为1,结果是:

stack top before func   0061FF28
stack top in func       0061FEFC
stack top after func    0061FF24
间隙刚刚缩小,在运行时堆栈顶部保持不变。间隙大小现在为44字节

当arr的大小为3时,它停止收缩

新的间隙大小为52字节

这是一种内存管理策略吗


如果它可以使用44字节,而选择使用52字节,并且在编译时可以知道函数调用之前变量的大小,那么它有什么好处呢?

这是因为gcc的堆栈对齐

在gcc中,堆栈对齐默认为16字节,而在我的emvironment中至少是这样。我使用compile选项-mprefered stack boundary=2将其更改为4个字节,和int的大小相同

然后,每当我声明一个新的int时,stack top-in函数都会移动


感谢and的评论,它引入了一个我以前不知道的新领域。

这是因为gcc的堆栈对齐

在gcc中,堆栈对齐默认为16字节,而在我的emvironment中至少是这样。我使用compile选项-mprefered stack boundary=2将其更改为4个字节,和int的大小相同

然后,每当我声明一个新的int时,stack top-in函数都会移动


感谢and的评论,它引入了一个我以前不知道的新领域。

我认为您对堆栈和编译器的工作方式做出了一些毫无根据的假设。即:

变量是在声明时分配的, 最后一个变量占据堆栈的顶部, 变量只占用所需的空间, 这有一个明确而确定的答案。 以下是在C、gcc、x86平台中调用函数时发生的大致情况,无需优化:

参数(如果有)存储在寄存器和/或堆栈中。详细信息在32位和64位、整数/指针、浮点和不同大小的结构、参数数、vararg等之间有所不同。 执行call指令,它将返回地址推送到堆栈上,在32位和64位中占用8字节,尽管原因不同,但我认为这会将处理器重定向到新地址。 在按下BP 4或8字节的原始值后,堆栈指针保存在BP寄存器中。 堆栈指针递减的字节数足以容纳所有局部变量。 回国后,

BP寄存器的值覆盖堆栈指针,自动对步骤4求反。然后弹出BP的原始值。 执行ret指令,弹出返回地址并跳到那里。 应该指出的是,这并不是普遍的,也不是保证的。可以优化简单函数以跳过步骤3、4和5。原则上,第4步可能发生多次。可以对堆栈指针执行额外的魔法,例如将其对齐到SSE指令操作数的两个边界(如128的倍数)的特定幂,分配称为红色区域、alloca函数等。存在许多异常和特殊情况。更多细节将取决于gcc命令行参数,或每个发行版的内置默认值。其他编译器可能遵循略有不同但兼容的约定。但让我们坚持这个模式

需要注意的是,所有局部变量通常在步骤4中一起分配,所采用的大小可能是所需的总大小或更大。例如,根据约定,编译器可以确保堆栈指针在任何一点都是16的倍数,以便函数本身可以依赖于此,在这种情况下,它也会根据步骤1到步骤3中所采取的措施取整到最接近的倍数。在该区域内,本地人被分配与BP或SP偏移的地址,以满足其大小和对齐要求

您的示例,尤其是main中的代码,无法工作,因为编译器不会按照您的意愿,仅在从f返回后才为j分配空间。它与arr和i一起出现在函数的开头,变量的顺序未指定,可能是为了将它们最好地打包到可用的空间中,INT以32位或64位边界获取地址。即使是这样,如果将j的地址作为func之后的栈顶,计算也会出错:充其量,它将是func和allocation之后的栈顶。通常,在C调用约定中,func之后的栈顶必须与func之前的栈顶相同

为了对你的职能有更具体的了解,我建议:

编译后研究汇编。godbolt.co的工具 m在这方面很好:由gcc 8.2在x86-64中编译,如图所示

堆栈指针应减少16行6加8,即第4行的RBP大小加上第28行存储返回地址所需的任何调用,8为64位模式

使用调试器:


您可以在这里看到rsp减少了0x20==32。

我认为您对堆栈和编译器的工作方式做出了一些毫无根据的假设。即:

变量是在声明时分配的, 最后一个变量占据堆栈的顶部, 变量只占用所需的空间, 这有一个明确而确定的答案。 以下是在C、gcc、x86平台中调用函数时发生的大致情况,无需优化:

参数(如果有)存储在寄存器和/或堆栈中。详细信息在32位和64位、整数/指针、浮点和不同大小的结构、参数数、vararg等之间有所不同。 执行call指令,它将返回地址推送到堆栈上,在32位和64位中占用8字节,尽管原因不同,但我认为这会将处理器重定向到新地址。 在按下BP 4或8字节的原始值后,堆栈指针保存在BP寄存器中。 堆栈指针递减的字节数足以容纳所有局部变量。 回国后,

BP寄存器的值覆盖堆栈指针,自动对步骤4求反。然后弹出BP的原始值。 执行ret指令,弹出返回地址并跳到那里。 应该指出的是,这并不是普遍的,也不是保证的。可以优化简单函数以跳过步骤3、4和5。原则上,第4步可能发生多次。可以对堆栈指针执行额外的魔法,例如将其对齐到SSE指令操作数的两个边界(如128的倍数)的特定幂,分配称为红色区域、alloca函数等。存在许多异常和特殊情况。更多细节将取决于gcc命令行参数,或每个发行版的内置默认值。其他编译器可能遵循略有不同但兼容的约定。但让我们坚持这个模式

需要注意的是,所有局部变量通常在步骤4中一起分配,所采用的大小可能是所需的总大小或更大。例如,根据约定,编译器可以确保堆栈指针在任何一点都是16的倍数,以便函数本身可以依赖于此,在这种情况下,它也会根据步骤1到步骤3中所采取的措施取整到最接近的倍数。在该区域内,本地人被分配与BP或SP偏移的地址,以满足其大小和对齐要求

您的示例,尤其是main中的代码,无法工作,因为编译器不会按照您的意愿,仅在从f返回后才为j分配空间。它与arr和i一起出现在函数的开头,变量的顺序未指定,可能是为了将它们最好地打包到可用的空间中,INT以32位或64位边界获取地址。即使是这样,如果将j的地址作为func之后的栈顶,计算也会出错:充其量,它将是func和allocation之后的栈顶。通常,在C调用约定中,func之后的栈顶必须与func之前的栈顶相同

为了对你的职能有更具体的了解,我建议:

编译后研究汇编。godbolt.com上的工具在这方面非常有用:如图所示,由gcc 8.2在x86-64中编译

堆栈指针应减少16行6加8,即第4行的RBP大小加上第28行存储返回地址所需的任何调用,8为64位模式

使用调试器:


您可以在这里看到rsp减少了0x20==32。

脱离主题:您应该为printf将指针转换为void*。这完全取决于实现。它可能会占用比实际需要更多的空间来进行对齐。可能是数据结构对齐。分道扬镳主题:您应该为printf将指针转换为void*。这完全依赖于实现。它可能会占用比实际需要更多的空间来进行对齐。可能是数据结构对齐。看见
stack top before func   0061FF2C
stack top in func       0061FEFC
stack top after func    0061FF28
stack top before func   0061FF28
stack top in func       0061FEFC
stack top after func    0061FF24
(gdb) b 11
(gdb) b 4
(gdb) run
Starting program: [redacted]
stack top before func   0x7fffffffd2dc

Breakpoint 1, main () at a.c:11
11      i = func();
(gdb) print $rsp
$1 = (void *) 0x7fffffffd2d0
(gdb) c
Continuing.

Breakpoint 2, func () at a.c:4
4       printf("stack top in func \t%p\n", &c);
(gdb) print $rsp
$2 = (void *) 0x7fffffffd2b0