就我所知,编译将高级别的C/C++代码转换为机器语言并输出一个可执行文件。在编译文件中如何“分配”内存?难道内存不总是通过虚拟内存管理等东西在RAM中分配吗?
按定义,内存分配不是一个运行时概念吗?
如果我在我的C/C++代码中创建了一个1KB的静态分配变量,那么可执行文件的大小是否会增加同样的数量?
这是其中一个页面上使用“静态分配”标题的页面。 回归基础:内存分配,一次历史之旅
在编译时分配内存意味着编译器会在编译时解析某些内容在进程内存映射中将被分配的位置。
例如,考虑一个全局数组:
int array[100];
编译器在编译时知道数组的大小和int的大小,因此它在编译时知道整个数组的大小。此外,默认情况下,全局变量具有静态存储期:它被分配在进程内存空间的静态内存区域(.data/.bss部分)中。根据这些信息,编译器在编译期间决定该数组将在静态内存区域的哪个地址中。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++代码。这适用于存储类别/持续时间(如此问题)和高级编译器优化。您可以指示编译器生成汇编程序,但在互联网上有很多友好的工具可以做到这一点。我的最爱是GCC Explorer。
编译时分配的内存意味着在运行时不会再有进一步的分配,也就是说不需要调用malloc
, new
, 或其他动态分配内存的方法。即使您并非始终需要所有这些内存,也会具有固定的内存使用量。
难道内存分配不是运行时概念吗?
在运行时之前,内存没有被使用,但在执行开始之前,系统会处理其分配。
如果我在C/C++代码中静态地分配了1KB的变量,它会将可执行文件的大小增加同样多吗?
仅仅声明静态变量不会使可执行文件的大小增加超过几个字节。如果声明具有非零初始值的静态变量,则会增加可执行文件的大小(为了保持初始值)。相反,链接器只会将这1KB的数量添加到系统加载器立即在执行之前为您创建的内存需求中。
static int i[4] = {2 , 3 , 5 ,5 }
,它会增加16个字节的可执行文件大小吗?你说:“仅仅声明静态变量不会使可执行文件的大小增加超过几个字节。但是,如果声明时给它一个非零的初始值,则会增加可执行文件的大小。”声明它带有初始值会有什么意义呢? - Suraj Jain在编译时分配的内存意味着当您加载程序时,一部分内存将立即被分配,并且此分配的大小和(相对)位置是在编译时确定的。
char a[32];
char b;
char c;
这三个变量是在编译时“分配的”,这意味着编译器在编译时计算它们的大小(固定大小)。变量a
将成为内存中的偏移量,比方说,指向地址0,b
将指向地址33,c
指向34(假设没有对齐优化)。因此,“分配1KB的静态数据不会增加代码的大小”,因为它只会改变其中的一个偏移量。 “实际空间将在加载时分配”。
真正的内存分配总是在运行时发生,因为内核需要跟踪它并更新其内部数据结构(每个进程分配了多少内存,页面等等)。区别在于编译器已经知道您将使用的每个数据的大小,这些数据将在程序执行时立即分配。
还要记住,我们谈论的是“相对地址”。变量将位于不同的实际地址。在加载时,内核将为进程保留一些内存,假设在地址x
处,可执行文件中包含的所有硬编码地址都会增加x
字节,因此示例中的变量a
将位于地址x
,b位于地址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
,你实际上能够在二进制文件中找到那个字符串。这种情况下,是的:可执行文件会变大1k,因为它完全包含了该字符串。
但是,如果你在栈上分配了一个int
、char
或long
类型的数组,并在循环中进行赋值,大致如下:
int big_arr[1000];
for (int i=0;i<1000;++i) big_arr[i] = some_computation_func(i);
那么,不会:这不会增加二进制文件的大小...1000*sizeof(int)
在编译时分配意味着您现在理解的含义是(基于您的评论):编译后的二进制文件包含系统需要知道执行时每个函数/块将需要多少内存以及应用程序所需的堆栈大小的信息。这就是系统在执行您的二进制文件时分配的内容,而且您的程序将成为一个进程(好吧,执行您的二进制文件是一个进程...您明白我的意思)。
当然,我没有完整地描述这个过程:二进制文件包含有关堆栈实际需要多大的信息。基于这些信息(还有其他因素),系统将保留一块内存,称为堆栈,程序可以在其上自由使用。堆栈内存仍然在进程(即您的二进制文件被执行后的结果)启动时由系统分配。然后,进程将为您管理堆栈内存。当函数或循环(任何类型的块)被调用/执行时,该块中的本地变量会被推入堆栈,并且它们被删除(堆栈内存被“释放”,以便其他函数/块使用)。因此,声明int some_array[100]
只会向二进制文件添加一些额外的字节信息,告诉系统函数X将需要100*sizeof(int)
+ 一些簿记空间额外。
ls -l a.out
收集数据点,然后编造抽象的“解释”,认为这些数据符合,而不是真正了解正确答案并试图向提问者传达相关部分的知识。 - Quuxplusone.text
中的指令);一旦您拥有了一个堆栈帧,添加更多变量通常不会改变二进制文件的大小。添加一个零初始化的静态变量(在.bss
中)根本不会改变二进制文件的大小(只会将“.bss
大小”的值存储在二进制文件中),并且肯定不会生成任何可执行指令。 - Quuxplusone在许多平台上,每个模块内的全局或静态分配将由编译器合并为三个或更少的合并分配(一个用于未初始化数据(通常称为“bss”),一个用于已初始化可写数据(通常称为“data”)和一个用于常量数据(“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字节的"data"和28字节的"const"。此外,任何对变量的引用都将被替换为区域选择器和偏移量,因此a、b、c、d和e将分别被替换为bss+0、const+0、bss+4、const+24、data+0或bss+204。int i;
```int c;
它生成了汇编器的输出,指示其为变量'c'保留内存。这可能看起来像这样:
global _c
section .bss
_c: resb 4
当汇编程序运行时,它会保持一个计数器,用于跟踪每个项目从内存“段”(或“区域”)的开头偏移的位置。这就像一个非常大的“结构体”中包含了整个文件中的所有内容,但此时并没有分配任何实际的内存,并且可以位于任何地方。它在表格中记录_c
具有特定的偏移量(例如从段开始的510字节),然后将其计数器增加4,因此下一个这样的变量将在(例如)514字节处。对于需要_c
地址的任何代码,它只需在输出文件中放置510,并添加一条注释,指出输出需要稍后添加到其中的包含_c
的段的地址。
链接器获取所有汇编输出文件并对它们进行检查。它为每个段确定一个地址,以便它们不会重叠,并添加必要的偏移量,以便指令仍然引用正确的数据项。对于像 c 占用的未初始化内存这样的情况(汇编程序通过编译器将其放入“.bss”段,这是一个保留未初始化内存的名称),它在输出中包括一个标题字段,告诉操作系统需要保留多少内存。它可能被重新定位(通常是如此),但通常设计为更有效地加载到一个特定的内存地址,并且操作系统将尝试在此地址加载它。此时,我们已经有了c
将使用的虚拟地址的相当好的想法。
物理地址实际上直到程序运行时才确定。但是,从程序员的角度来看,物理地址实际上并不重要——我们甚至永远不会知道它是什么,因为操作系统通常不会告诉任何人,它可能会频繁更改(即使在程序运行时),操作系统的主要目的就是抽象这一点。可执行文件描述了为静态变量分配空间的方法。当您运行可执行文件时,系统会执行此分配操作。因此,您的1kB静态变量不会使可执行文件的大小增加1kB:
static char[1024];
static char[1024] = { 1, 2, 3, 4, ... };
除了“机器语言”(即CPU指令)外,可执行文件还包含所需内存布局的描述。
内存可以以多种方式分配:
现在你的问题是什么是“编译时分配的内存”。显然,这只是一个措辞不当的说法,它应该是指二进制段分配或堆栈分配,或者在某些情况下甚至是堆分配,但在这种情况下,分配是通过不可见的构造函数调用来隐藏的。或者可能说这话的人只是想说内存不是在堆上分配的,但不知道堆栈或段分配,(或者不想深入了解这种细节)。
但在大多数情况下,人们只是想说 分配的内存量在编译时是已知的。
只有当内存在应用程序的代码或数据段中保留时,二进制文件大小才会发生变化。
你说得对。内存实际上是在加载时分配(分页),也就是在可执行文件被带入(虚拟)内存时。内存也可以在那个时候初始化。编译器只是创建了一个内存映射。[顺便提一下,堆栈空间也是在加载时分配的!]