在像C和C ++这样的编程语言中,人们经常引用静态和动态内存分配。我理解这个概念,但是“在编译时分配(保留)所有内存”这句话总是让我感到困惑。
据我了解,编译将高级C / C ++代码转换为机器语言并输出可执行文件。如何在已编译文件中“分配”内存?难道不是所有虚拟内存管理工具都总是在RAM中分配内存吗?
按照定义,内存分配不是运行时的概念吗?
如果我在C / C ++代码中创建一个1KB的静态分配变量,这将使可执行文件的大小增加相同的数量吗?
这是在“静态分配”标题下使用该短语的页面之一。
在像C和C ++这样的编程语言中,人们经常引用静态和动态内存分配。我理解这个概念,但是“在编译时分配(保留)所有内存”这句话总是让我感到困惑。
据我了解,编译将高级C / C ++代码转换为机器语言并输出可执行文件。如何在已编译文件中“分配”内存?难道不是所有虚拟内存管理工具都总是在RAM中分配内存吗?
按照定义,内存分配不是运行时的概念吗?
如果我在C / C ++代码中创建一个1KB的静态分配变量,这将使可执行文件的大小增加相同的数量吗?
这是在“静态分配”标题下使用该短语的页面之一。
Answers:
在编译时分配的内存意味着编译器在编译时解析,其中某些内容将在进程内存映射内分配。
例如,考虑一个全局数组:
int array[100];
编译器在编译时知道数组的大小和的大小int,因此它在编译时知道数组的整个大小。默认情况下,全局变量还具有静态存储持续时间:它在进程存储空间的静态存储区域(.data / .bss节)中分配。有了这些信息,编译器将在编译期间决定该数组将位于该静态存储区的哪个地址。
当然,内存地址是虚拟地址。该程序假定它具有自己的整个内存空间(例如,从0x00000000到0xFFFFFFFF)。这就是为什么编译器可以进行诸如“好的,数组将位于地址0x00A33211”的假设的原因。在运行时,MMU和OS会将地址转换为实际/硬件地址。
值初始化的静态存储有些不同。例如:
int array[] = { 1 , 2 , 3 , 4 };
在我们的第一个示例中,编译器仅决定将数组分配到的位置,并将该信息存储在可执行文件中。
在使用值初始化的情况下,编译器还将数组的初始值注入可执行文件中,并添加代码,该代码告诉程序加载器在程序开始分配数组后,应使用这些值填充数组。
这是编译器(带有x86目标的GCC4.8.1)生成的汇编的两个示例:
C ++代码:
int a[4];
int b[] = { 1 , 2 , 3 , 4 };
int main()
{}
输出组件:
a:
.zero 16
b:
.long 1
.long 2
.long 3
.long 4
main:
pushq %rbp
movq %rsp, %rbp
movl $0, %eax
popq %rbp
ret
如您所见,这些值将直接注入到程序集中。在数组中a,编译器会生成16字节的零初始化,因为标准表示静态存储的事物应默认初始化为零:
8.5.9(初始化程序)[注]:
静态存储持续时间的每个对象在程序启动时都将其初始化为零,然后再进行其他初始化。在某些情况下,稍后会进行其他初始化。
我总是建议人们反汇编他们的代码,以查看编译器对C ++代码的实际作用。这适用于从存储类/持续时间(如此问题)到高级编译器优化。您可以指示编译器生成程序集,但是在Internet上有许多很棒的工具以友好的方式完成了该程序集。我最喜欢的是GCC Explorer。
在编译时分配的内存仅意味着在运行时将不再进行任何分配-无需调用malloc,new或其他动态分配方法。即使您不需要一直使用所有内存,您的内存使用量也将保持固定。
按照定义,内存分配不是运行时的概念吗?
内存在运行时尚未使用,而是在执行开始之前立即由系统处理。
如果我在C / C ++代码中创建一个1KB的静态分配变量,这将使可执行文件的大小增加相同的数量吗?
简单地声明为static不会使可执行文件的大小增加几个字节以上。用非零的初始值声明它(以便保留该初始值)。而是,链接器仅将这1KB的内存添加到系统加载程序在执行之前为您创建的内存需求中。
static int i[4] = {2 , 3 , 5 ,5 }话,可执行文件大小会增加16个字节。您说:“仅声明静态变量不会将可执行文件的大小增加几个字节以上。使用非零的初始值声明可执行文件将是一个错误。”使用初始值声明它意味着什么。
在编译时分配的内存意味着在加载程序时,将立即分配一部分内存,并且在编译时确定此分配的大小和(相对)位置。
char a[32];
char b;
char c;
这三个变量是“在编译时分配的”,这意味着编译器会在编译时计算其大小(固定)。该变量a将是内存中的偏移量,比方说,指向地址0,b将指向地址33和c34(假设没有对齐优化)。因此,分配1Kb的静态数据不会增加代码的大小,因为它只会更改其内部的偏移量。实际空间将在加载时分配。
实际内存分配总是在运行时发生,因为内核需要跟踪它并更新其内部数据结构(为每个进程,页面等分配多少内存)。不同之处在于,编译器已经知道您将要使用的每个数据的大小,并且在程序执行后立即分配了该大小。
还请记住,我们在谈论相对地址。变量将位于的实际地址将不同。在加载时,内核将为该进程保留一些内存,比方说在address处x,并且可执行文件中包含的所有硬编码地址将按x字节递增,因此a示例中的变量将在address x,b在address x+33和以此类推。
在堆栈上添加占用N个字节的变量不会(有必要)将bin的大小增加N个字节。实际上,大多数时候它只会增加几个字节。
让我们从一个示例开始,该示例如何向代码中添加1000个字符将以线性方式增加bin的大小。
如果1k是一千个字符的字符串,则声明为
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
然后vim your_compiled_bin,您实际上可以在某处的垃圾箱中看到该字符串。在那种情况下,是的:可执行文件将大1 k,因为它包含完整的字符串。
但是,如果您分配ints,chars或long在堆栈上分配 s并循环分配,则沿这些方向的内容
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
然后,否:不会增加bin ...通过1000*sizeof(int)
在编译时分配,意味着您现在已经了解它的含义(基于您的评论):已编译的bin包含系统需要知道多少内存的信息执行时需要什么功能/块,以及应用程序所需的堆栈大小信息。这就是系统在执行bin时将分配的内容,并且您的程序成为一个进程(嗯,bin的执行就是……嗯,您明白我的意思了)。
当然,我不会在此处绘制全部图片:垃圾箱包含有关垃圾箱实际需要多大的信息。基于此信息(除其他事项外),系统将保留一块称为栈的内存,程序可以自由支配该内存。当启动进程(执行bin的结果)时,堆栈内存仍由系统分配。然后,该过程将为您管理堆栈存储器。当一个函数或循环(任何类型的块)被调用/执行时,该块本地的变量被压入堆栈,并被删除(可以说是“释放了”堆栈存储器),以供其他人使用。功能/块。所以宣布int some_array[100]只会向bin中添加一些字节的附加信息,告诉系统功能X将需要100*sizeof(int) +一些簿记空间。
i不会“释放”或两者都不释放。如果i要驻留在内存中,它只会被压入堆栈,从某种意义上讲,这是没有释放的,无视它,i或者c将一直保存在寄存器中。当然,这全都取决于编译器,这意味着它不是黑白的。
free()调用的意义上被释放,但是一旦列出的函数返回,它们使用的堆栈内存就可以由其他函数释放。我删除了代码,因为它可能会使某些人感到困惑
在许多平台上,编译器会将每个模块内的所有全局或静态分配合并为三个或更少的合并分配(一个用于未初始化的数据(通常称为“ bss”),一个用于已初始化的可写数据(通常称为“数据”)。 ),以及一个用于常量数据(“ const”)的数据,并且程序中每种类型的所有全局或静态分配都将由链接器合并为每种类型的一个全局变量。例如,假设int有四个字节,则模块将以下内容作为其唯一的静态分配:
int a;
const int b[6] = {1,2,3,4,5,6};
char c[200];
const int d = 23;
int e[4] = {1,2,3,4};
int f;
它会告诉链接器它需要208字节的bss,16字节的“数据”和28字节的“ const”。此外,对变量的任何引用都将被区域选择器和偏移量替代,因此a,b,c,d和e将被bss + 0,const + 0,bss + 4,const + 24,data替代。 +0或bss + 204。
链接程序时,所有模块中的所有bss区域都串联在一起。同样是数据和常量区域。对于每个模块,任何bss相对变量的地址都将增加所有先前模块的bss区域的大小(同样,对于data和const同样)。因此,完成链接程序后,任何程序都将具有一个bss分配,一个数据分配和一个const分配。
加载程序时,取决于平台,通常会发生以下四种情况之一:
可执行文件将指示每种数据以及已初始化数据区域(可能在其中找到初始内容)所需的字节数。它还将包括所有使用bss,数据或常量相关地址的指令的列表。操作系统或加载程序将为每个区域分配适当的空间量,然后将该区域的起始地址添加到需要它的每个指令中。
操作系统将分配一个内存块来保存所有三种数据,并为应用程序提供指向该内存块的指针。任何使用静态或全局数据的代码都将相对于该指针取消引用(在许多情况下,该指针将在应用程序的生存期内存储在寄存器中)。
操作系统最初不会为应用程序分配任何内存,除了持有其二进制代码的内存之外,但是应用程序要做的第一件事就是向操作系统请求适当的分配,它将永远保存在寄存器中。
操作系统最初不会为应用程序分配空间,但是应用程序将在启动时请求适当的分配(如上所述)。该应用程序将包括指令列表,这些指令的地址需要更新以反映分配内存的位置(与第一种样式一样),但是该应用程序将包含足够的代码来对其自身进行修补,而不是由操作系统加载程序对其进行修补。 。
这四种方法都有优点和缺点。但是,在每种情况下,编译器都会将任意数量的静态变量合并为固定数量的固定内存请求,而链接程序会将所有这些静态变量合并为少量的合并分配。即使应用程序必须从操作系统或加载器接收大块内存,还是由编译器和链接器负责将大块中的各个块分配给需要它的所有单个变量。
您的问题的核心是:“如何在已编译的文件中“分配”内存?难道内存中不总是将所有虚拟内存管理内容分配给内存吗?按定义,内存分配不是运行时概念吗?”
我认为问题在于内存分配涉及两个不同的概念。从根本上说,内存分配是一个过程,我们说“此数据项存储在此特定的内存块中”。在现代计算机系统中,这涉及两个步骤:
后一个过程纯粹是运行时,但是如果数据具有已知大小并且需要固定数目的数据,则前一个过程可以在编译时完成。基本上是这样的:
编译器会看到一个源文件,其中包含如下所示的一行:
int c;它为汇编器产生输出,指示汇编器为变量“ c”保留内存。可能看起来像这样:
global _c
section .bss
_c: resb 4汇编器运行时,会保留一个计数器,该计数器跟踪每个项目从内存“段”(或“节”)开始的偏移量。这就像一个非常大的“结构”的部分,该结构包含整个文件中的所有内容,此时它没有分配任何实际的内存,并且可以在任何地方。它在_c具有特定偏移量(例如,从段的开头起为510字节)的表中进行注释,然后将其计数器增加4,因此下一个此类变量将为(例如)514字节。对于需要地址的任何代码_c,它只将510放在输出文件中,并添加一条注释,即输出需要包含_c稍后添加的段的地址。
链接器获取所有汇编程序的输出文件,并对其进行检查。它为每个段确定一个地址,以便它们不会重叠,并添加必要的偏移量,以便指令仍引用正确的数据项。对于未初始化的内存,例如c(由于编译器将其放在'.bss'段中,而汇编程序被告知该内存将未初始化,这是为未初始化的内存保留的名称),它在输出中包括一个标头字段,用于告知操作系统需要保留多少。它可能会被重定位(通常是),但通常被设计为在一个特定的内存地址更有效地加载,操作系统将尝试在该地址加载它。在这一点上,我们已经有了一个很好的主意,即将会使用什么虚拟地址c。
在程序运行之前,实际不会确定物理地址。但是,从程序员的角度来看,物理地址实际上是无关紧要的-我们甚至永远都找不到它的含义,因为操作系统通常不会费心告诉任何人,它可以经常更改(即使在程序运行时),并且该操作系统的主要目的是无论如何都要对此进行抽象。
内存可以通过多种方式分配:
现在您的问题是什么是“在编译时分配的内存”。无疑,这只是一个用词不正确的说法,它是指二进制段分配或堆栈分配,或者在某些情况下甚至是堆分配,但是在那种情况下,这种分配由于看不见的构造函数调用而对程序员是隐藏的。或者可能是说那句话的人只是想说内存不是在堆上分配的,但是却不知道堆栈或段的分配(或者不想讲这种细节)。
但是在大多数情况下,人们只是想说在编译时就知道分配的内存量。
只有在应用程序的代码或数据段中保留了内存时,二进制大小才会更改。
.data和之间相对更重要的区别.bss。
你是对的。实际上是在加载时分配(分页)内存,即在将可执行文件放入(虚拟)内存时。内存也可以在那一刻初始化。编译器仅创建一个内存映射。[顺便说一下,堆栈和堆空间也在加载时分配!]
我认为您需要退后一步。在编译时分配的内存...。这意味着什么?这是否意味着以某种方式保留了尚未制造的芯片上,尚未设计的计算机上的内存?不,不,时间旅行,没有可以操纵宇宙的编译器。
因此,这必须意味着编译器生成指令以在运行时以某种方式分配该内存。但是,如果您从正确的角度看待它,则编译器会生成所有指令,因此有什么区别。不同之处在于,编译器决定,并且在运行时,您的代码无法更改或修改其决定。如果它决定在编译时需要50个字节,那么在运行时,您将无法决定分配60个字节-该决定已经做出。
如果您学习汇编程序设计,您将看到必须为数据,堆栈和代码等划分段。数据段是字符串和数字所在的位置。代码段是您的代码所在的地方。这些段内置在可执行程序中。当然,堆栈大小也很重要...您不会希望堆栈溢出!
因此,如果您的数据段为500字节,则您的程序具有500字节的区域。如果将数据段更改为1500字节,则程序的大小将大1000字节。数据被组装到实际程序中。
这是编译高级语言时发生的情况。当将实际数据区域编译为可执行程序时,将对其进行分配,从而增加了程序的大小。该程序也可以动态请求内存,这就是动态内存。您可以从RAM请求内存,而CPU会将内存交给您使用,您可以放开它,然后垃圾回收器会将其释放回CPU。如有必要,甚至可以由一个好的内存管理器将其交换到硬盘上。这些功能是高级语言为您提供的。
我想借助几个图表来解释这些概念。
的确,确实不能在编译时分配内存。但是,实际上在编译时会发生什么。
这里是解释。假设某个程序有四个变量x,y,z和k。现在,在编译时,它仅生成一个内存映射,在此确定这些变量相对于彼此的位置。该图将更好地说明它。
现在想象一下,没有程序在内存中运行。我用一个大的空矩形显示。

接下来,执行该程序的第一个实例。您可以将其可视化如下。这是实际分配内存的时间。

当该程序的第二个实例正在运行时,内存将如下所示。

还有第三..

等等等等。
我希望这种可视化可以很好地解释这个概念。
接受的答案中有很好的解释。以防万一我会发布我发现有用的链接。 https://www.tenouk.com/ModuleW.html