什么“编译时分配的内存”真的意味着什么?
在像C和C ++这样的编程语言中,人们通常会引用静态和dynamic内存分配。 我理解这个概念,但是“在编译期间所有内存被分配(保留)”这句话总是令我困惑。
据我了解,编译将高级C / C ++代码转换为机器语言并输出可执行文件。 如何在编译的文件中“分配”内存? 是不是所有的虚拟内存pipe理的东西内存总是分配在内存中?
内存分配是不是定义一个运行时的概念?
如果我在我的C / C ++代码中创build一个1KB的静态分配的variables,是否会增加相同数量的可执行文件的大小?
这是在“静态分配”标题下使用短语的页面之一。
回到基础:内存分配,走下历史
在编译时分配的内存意味着编译器在编译时parsing某些东西将在进程内存映射中分配的地方。
例如,考虑一个全局数组:
int array[100];
编译器在编译时知道数组的大小和int
的大小,所以它在编译时知道数组的整个大小。 另外,全局variables默认情况下具有静态存储持续时间:它被分配在进程内存空间(.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(初始化程序)[注意]:
静态存储持续时间的每个对象在程序启动之前进行零初始化,然后进行任何其他初始化。 在某些情况下,稍后会进行额外的初始化。
我总是build议人们反编译他们的代码,看看编译器真的用C ++代码做什么。 这从存储类/持续时间(如这个问题)适用于高级编译器优化。 您可以指示您的编译器生成程序集,但有一些很好的工具可以在Internet上以友好的方式执行此操作。 我最喜欢的是GCC资源pipe理器 。
在编译时分配的内存仅仅意味着在运行时不会有进一步的分配 – 不需要调用malloc,new或其他dynamic分配方法。 即使你不需要所有的内存,你也会有固定的内存使用量。
内存分配是不是定义一个运行时的概念?
内存在运行时间之前未被使用 ,但在执行之前立即开始其分配由系统处理。
如果我在我的C / C ++代码中创build一个1KB的静态分配的variables,是否会增加相同数量的可执行文件的大小?
简单地声明静态不会增加超过几个字节的可执行文件的大小。 用一个非零的初始值声明它(为了保持初始值)。 链接器只是简单地将这个1KB的数量添加到系统加载程序在执行之前立即为您创build的内存需求。
在编译时分配的内存意味着当您加载程序时,内存的一部分将被立即分配,并在编译时确定该分配的大小和(相对)位置。
char a[32]; char b; char c;
这3个variables是“在编译时分配的”,这意味着编译器在编译时计算它们的大小(这是固定的)。 variablesa
将是内存中的偏移量,假设指向地址0, b
将指向地址33, c
指向34(假设没有alignment优化)。 所以, 分配1Kb的静态数据不会增加代码的大小 ,因为它只会改变它内部的偏移量。 实际空间将在加载时分配 。
真正的内存分配总是在运行时发生,因为内核需要跟踪它并更新其内部数据结构(为每个进程分配多less内存,页面等等)。 不同之处在于,编译器已经知道要使用的每个数据的大小,只要程序执行时就会分配这些数据。
请记住我们正在谈论相对地址 。 variables的实际地址将会不同。 在加载时,内核将为进程保留一些内存,比如说在地址x
,并且可执行文件中包含的所有硬编码地址都将增加x
字节,以便示例中的variablesa
位于地址x
, b在地址x+33
等等。
在堆栈中添加占用N个字节的variables不(必然)将bin的大小增加N个字节。 事实上,大部分时间只会添加几个字节。
让我们以一个例子来说明如何在您的代码中添加1000个字符以线性方式增加bin的大小。
如果1k是一个string,一千个字符,这是宣布如此
const char *c_string = "Here goes a thousand chars...999";//implicit \0 at end
然后你要vim your_compiled_bin
,你实际上可以看到bin中的string。 在这种情况下,是的:可执行文件将会大1k,因为它包含完整的string。
但是,如果您在堆栈中分配int
s, char
s或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包含系统需要知道在执行的时候需要多less内存的信息,以及关于您的应用程序需要的堆栈大小。 这就是系统在执行bin时所分配的内容,并且你的程序变成了一个进程(好吧,你的bin的执行过程就是…呃,你明白我的意思了)。
当然,我并没有在这里画出完整的画面:垃圾箱包含有关垃圾桶实际上需要多大堆栈的信息。 根据这些信息(除其他外),系统将预留一大块名为堆栈的内存,程序可以自由统治。 堆栈内存仍然由系统分配,当进程(正在执行的bin的结果)启动时。 该过程然后为您pipe理堆栈内存。 当一个函数或循环(任何types的块)被调用/执行时,该块的局部variables被推送到堆栈,并被移除(堆栈内存被“释放”这样说)供其他function/块。 所以声明int some_array[100]
将只添加几个字节的附加信息到bin,这告诉系统函数X将需要100*sizeof(int)
+一些额外的簿记空间。
在许多平台上,每个模块中的所有全局或静态分配将由编译器合并为三个或更less的合并分配(一个用于未初始化数据(通常称为“bss”),一个用于初始化可写数据(通常称为“数据” ),一个用于常量数据(“const”)),程序中每种types的所有全局或静态分配将由链接器合并到每个types的一个全局中。 例如,假设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”。 此外,任何对variables的引用都将被replace为区域select符和偏移量,所以a,b,c,d和e将被replace为bss + 0,const + 0,bss + 4,const + 24,data +0或bss + 204。
当一个程序被链接时,所有模块的所有bss区域被连接在一起; 同样的数据和常量区域。 对于每个模块,任何bss相关variables的地址都将增加所有前面模块的bss区域的大小(同样,数据和const也一样)。 因此,当链接器完成时,任何程序将有一个bss分配,一个数据分配和一个常量分配。
当一个程序被加载时,通常会发生四件事情之一,这取决于平台:
-
可执行文件将显示每种数据需要多less字节,以及 – 初始化的数据区域,可以find初始内容。 它还将包含所有使用bss-,data-或const-relative地址的指令列表。 操作系统或加载程序将为每个区域分配适当的空间量,然后将该区域的起始地址添加到每个需要它的指令中。
-
操作系统将分配一块内存来保存所有这三种数据,并给应用程序一个指向这块内存的指针。 任何使用静态或全局数据的代码将相对于该指针取消引用(在许多情况下,指针将被存储在寄存器中以保存应用程序的生命周期)。
-
操作系统最初不会为应用程序分配任何内存,除了保存二进制代码的内容之外,应用程序所做的第一件事是从操作系统请求合适的分配,这将永远保存在一个寄存器中。
-
操作系统最初不会为应用程序分配空间,但应用程序将在启动时请求一个合适的分配(如上所述)。 应用程序将包含一个指令列表,其中需要更新地址以反映分配内存的位置(与第一种样式一样),而不是由OS加载程序修补应用程序,应用程序将包含足够的代码来修补自身。
所有四种方法都有优点和缺点。 然而,在任何情况下,编译器都会将任意数量的静态variables合并成一个固定的less量内存请求,链接器将把所有这些variables合并成less量的合并分配。 即使应用程序必须从操作系统或加载程序接收大量内存,它也是编译器和链接程序,它们负责将大块中的各个块分配给需要它的所有单个variables。
你的问题的核心是:“在编译的文件中如何分配内存”是不是内存总是分配在所有虚拟内存pipe理内存的RAM?是不是内存分配定义为运行时的概念?
我认为问题在于内存分配涉及两个不同的概念。 在其基本的内存分配过程中,我们说“这个数据项存储在这个特定的内存块中”。 在现代计算机系统中,这涉及到两个步骤:
- 有些系统被用来决定物品的虚拟地址
- 虚拟地址映射到物理地址
后一个过程是纯粹的运行时间,但前者可以在编译时完成,如果数据具有已知的大小并且需要固定的数目。 这基本上是如何工作的:
-
编译器看到一个包含一个看起来有点像这样的行的源文件:
int c;
-
它为汇编程序产生输出,指示它为variables'c'保留内存。 这可能看起来像这样:
global _c section .bss _c: resb 4
-
当汇编程序运行时,它会保存一个计数器,用于跟踪内存“段”(或“部分”)开始时每个项目的偏移量。 这就像一个非常大的“结构”的部分,它包含了整个文件中的所有内容,在这个时候没有任何实际的内存分配给它,而且可能在任何地方。 它在表中注意到
_c
有一个特定的偏移量(比如从段开始的510个字节),然后递增它的计数器4,所以下一个这样的variables将在(例如)514个字节。 对于任何需要_c
地址的代码,它只是在输出文件中放置510,并添加一个注释,输出需要包含_c
的段的地址稍后添加。 -
链接器接收所有汇编器的输出文件,并检查它们。 它确定每个段的地址,使它们不会重叠,并添加所需的偏移量,以便指令仍然引用正确的数据项。 对于像
c
占用的未初始化的内存(汇编程序被告知由于编译器将其放入'.bss'段(这是一个为未初始化的内存保留的名称)而未初始化的内存),它包含输出中的标题字段告诉操作系统有多less需要保留。 它可能会被重新定位(通常是),但是通常被devise为在一个特定的内存地址上被更有效地加载,操作系统会尝试在这个地址加载它。 在这一点上,我们有一个相当好的主意,什么虚拟地址是将被使用c
-
物理地址在程序运行之前不会被确定。 然而,从程序员的angular度来看,物理地址实际上是不相关的 – 我们甚至都不知道它是什么,因为操作系统通常不会告诉任何人,它可以频繁地(甚至在程序运行的时候)改变,操作系统的主要目的是把这个抽象出来。
可执行文件描述了为静态variables分配的空间。 当您运行可执行文件时,该分配由系统完成。 所以你的1kB静态variables不会增加1kB的可执行文件的大小:
static char[1024];
除非你指定了一个初始化器:
static char[1024] = { 1, 2, 3, 4, ... };
因此,除了“机器语言”(即CPU指令)之外,可执行文件还包含所需内存布局的描述。
内存可以以多种方式分配:
- 在应用程序堆中(程序启动时整个堆由OS分配给您的应用程序)
- 在操作系统堆(所以你可以抓越来越多)
- 在垃圾收集器控制的堆(同上)
- 在堆栈(所以你可以得到一个堆栈溢出)
- 保留在你的二进制代码/数据段(可执行文件)
- 在远程的地方(文件,networking – 你会收到一个句柄,而不是指向那个内存的指针)
现在你的问题是什么是“在编译时分配的内存”。 当然,这只是一个错误的措辞,应该指的是二进制分配或堆栈分配,或者在某些情况下,甚至是堆分配,但是在这种情况下,通过无形的构造函数调用来隐藏程序员眼中的分配。 或者可能是这样的人,他只是想说内存不是分配在堆上,而是不知道堆栈或段的分配(或者不想进入这种细节)。
但是在大多数情况下,人们只是想说在编译时分配的内存量是已知的 。
二进制大小只会在应用程序的代码或数据段中保留内存时才会更改。
你是对的。 内存实际上在加载时分配(分页),即当可执行文件被带入(虚拟)内存时。 内存也可以在那个时候初始化。 编译器只是创build内存映射。 [顺便说一下,堆栈和堆空间也在加载时分配!
我想你需要退后一步。 在编译时分配的内存….这意味着什么? 这是否意味着尚未制造的芯片尚未被devise的计算机内存被某种方式保留下来? 不,不,时间旅行,没有可以操纵宇宙的编译器。
所以,这意味着编译器会生成指令在运行时以某种方式分配内存。 但是如果从正确的angular度来看,编译器会生成所有的指令,那么可能会有什么不同。 区别在于编译器决定,并且在运行时,您的代码不能更改或修改其决策。 如果在编译时决定需要50个字节,运行时就不能决定分配60个字节 – 这个决定已经完成了。
如果你学习汇编编程,你将会看到你必须为数据,堆栈和代码等分割出一些段。数据段就是你的string和数字所在的地方。 代码段是您的代码所在的位置。 这些段被内置到可执行程序中。 当然堆栈的大小也很重要,你不想堆栈溢出 !
所以如果你的数据段是500字节,你的程序有500字节的区域。 如果将数据段更改为1500字节,则程序的大小将增加1000个字节。 数据被汇编成实际的程序。
编译高级语言时会出现这种情况。 实际的数据区被编译成可执行程序时被分配,增加了程序的大小。 程序也可以即时请求内存,这是dynamic内存。 你可以从RAM请求内存,CPU将给你使用,你可以放开它,你的垃圾收集器将释放它回到CPU。 如果需要的话,甚至可以通过一个好的内存pipe理器将其交换到硬盘。 这些function是高级语言为您提供的。
我想借助几个图解释这些概念。
确实,编译时不能分配内存。 但是,那么事实上在编译时会发生什么。
这里解释。 比如说,一个程序有四个variablesx,y,z和k。 现在,在编译时,它只是build立一个内存映射,在这里确定这些variables相对于彼此的位置。 这张图将更好地说明这一点。
现在想象一下,没有程序在内存中运行。 这个我用一个很大的空矩形显示。
接下来,执行该程序的第一个实例。 您可以按照以下方式将其可视化。 这是实际分配内存的时间。
当该程序的第二个实例正在运行时,内存将如下所示。
而第三..
等等等等。
我希望这个可视化能很好的解释这个概念