关于内存:是否在C中的堆栈或堆上创建了激活记录?

Are activation records created on stack or heap in C?

我在读内存分配和激活记录。我有些怀疑。有人能把下面的水晶弄清楚吗?

A)我的第一个疑问是"激活记录是在C中的堆栈还是堆上创建的"?

B)以下是我所指的摘要中的几行:-->

Even though memory on stack area is created during run time- the
amount of memory (activation record size) is determined at compile
time. Static and global memory area is compile time determined and
this is part of the binary. At run time, we cannot change this. Only
memory area freely available for the process to change during runtime
is heap.At compile time compiler only reserves the stack space for
activation record. This gets used (allocated on actual memory) only
during program run. Only DATA segment part of the program like static
variables, string literals etc. are allocated during compile time. For
heap area, how much memory to be allocated is also determined at run
time.

我什么都听不懂,有人能详细解释一下吗?我相信这种解释对我来说是非常必要的。


在C中(考虑到它几乎是普遍实现的*)激活记录与堆栈帧是完全相同的,而堆栈帧与调用帧是完全相同的。它们总是在堆栈上创建的。

堆栈段是进程在创建时从操作系统"免费"获得的内存区域。它不需要mallocfree它。在x86上,一个机器寄存器(如RSP指向段的末尾),堆栈帧/激活记录/调用帧通过将该寄存器中的指针减量多少字节来"分配"。例如:

1
2
3
4
5
6
7
int my_func() {
    int x = 123;
    int y = 234;
    int z = 345;
    ...
    return 1;
}

未优化的C编译器可以生成程序集代码,以便将这三个变量保存在堆栈框架中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
my_func:
    ;"allocate" 24 bytes of stack space
    sub  rsp, 24    
    ; Initialize the allocated stack memory
    mov  [rsp], 345      ; z = 345
    mov  [rsp+8], 234    ; y = 234
    mov  [rsp+16], 134   ; x = 123
    ...
    ;"free" the allocated stack space
    add  rsp, 24
    ; return 1
    mov  rax, 1
    ret
  • 在其他上下文和语言中,激活记录可以以不同的方式实现。例如,使用链接列表。但是由于语言是C语言,上下文是低级编程,所以我认为讨论这一点并不有用。

理论上,C99(或C11)兼容的实现(例如C编译器和C标准库实现)甚至不需要(在所有情况下)调用堆栈。例如,我们可以想象一个完整的程序编译器(尤其是独立的C实现),它可以分析整个程序,并确定堆栈帧是不需要的(例如,每个局部变量可以静态分配,或放入寄存器)。或者可以想象一个在其他地方(例如,在一些"堆"中)将调用帧分配为连续帧的实现(可能是在编译器的cps转换之后),使用类似于Appel旧书中描述的连续编译(描述SML/NJ编译器)的技术。

(请记住,编程语言是一种规范,而不是一些软件,通常用英语编写,可能在某些技术报告或标准文档中附加形式化。Afaik、C99或C11标准甚至没有提到任何堆栈或激活记录。但实际上,大多数C实现都是由编译器和标准库实现组成的。)

在实践中,分配记录是调用帧(对于C,它们是同义词;嵌套函数的情况更复杂),并且在我所知道的所有合理的C实现上都分配在硬件辅助调用堆栈上。在z/体系结构中,没有硬件堆栈指针寄存器,所以这是一种约定(指定一些寄存器来扮演堆栈指针的角色)。

所以首先看一下调用堆栈维基页面。它有一幅很好的图画,值得多说几句。

Are activation records created on stack or heap

实际上,它们(激活记录)是调用堆栈上的调用帧(按照调用约定和ABI分配)。当然,调用帧的布局、槽的使用和大小是由编译器在编译时计算的。

实际上,局部变量可能对应于调用帧内的某个槽。但有时,编译器只将其保存在寄存器中,或者将同一插槽(在调用帧中具有固定偏移量)用于各种用途,例如用于不同块中的几个局部变量等。

但大多数C编译器都在优化编译器。它们能够内联一个函数,或者有时对其进行尾部调用(然后调用方的调用帧被重用为被调用方的调用帧,或者被调用方的调用帧覆盖),因此细节更加复杂。

另请参见,C是如何移植到没有硬件堆栈的体系结构的?关于复古的问题。


作为一个快速的答案,我甚至不知道什么是激活记录。其余的引言英语很差,很容易引起误解。

老实说,抽象是指绝对性,而实际上,根本没有绝对性。您确实在编译时定义了一个主堆栈,是的(尽管您也可以在运行时创建许多堆栈)。

是的,当您想要分配内存时,通常会创建一个指针来存储该信息,但是您将它放在什么地方完全取决于您自己。它可以是堆栈,也可以是全局内存,也可以是来自另一个分配的堆,或者您只需要泄漏内存,而不需要将它存储在任何地方。也许这就是激活记录的含义?

或者,它意味着当动态内存被创建时,在内存的某个地方,必须有某种信息来跟踪已使用和未使用的内存。对于许多分配器,这是存储在已分配内存中某个位置的指针列表,尽管其他分配器将其存储在不同的内存块中,有些甚至可以将其放在堆栈上。这完全取决于存储系统的需要。

最后,动态内存的分配来源也可能有所不同。它可以来自对操作系统的调用,但在某些情况下,它也可以覆盖到现有的全局(甚至是堆栈)内存上——这在嵌入式编程中并不少见。

如您所见,这个抽象甚至不接近动态内存所代表的内容。

其他信息:

许多人都在我面前跳来跳去,说‘C’在标准中没有堆栈。对的。也就是说,有多少人真正用C编码而没有一个?我暂时不谈这个了。

定义内存,如您所调用的,是在函数内用"static"关键字声明的任何内容,或者是在函数外声明的任何变量,而在函数前面没有"extern"关键字。这是编译器知道的内存,可以在没有任何其他帮助的情况下为其保留空间。

已分配内存-这不是一个好术语,因为定义的内存也可以视为已分配。相反,使用术语动态内存。这是在运行时从堆中分配的内存。一个例子:

1
2
3
4
5
6
7
8
9
10
char *foo;
int my_value;

int main(void)
{
    foo = malloc(10 * sizeof(char));
    // Do stuff with foo
    free(foo);
    return 0;
}

如您所说,foo是一个"定义"的指针。如果不做任何其他事情,它只会保留那么多的内存,但是当malloc在main()中到达时,它现在也指向至少10个字节的动态内存。一旦达到空闲状态,该内存就可以供程序使用。它的分配大小是"动态的"。与my_value相比,my_value的大小总是和int一样大,其他的都没有。