为什么需要 .bss 段?

167

我知道的是全局变量和静态变量存储在.data段中,未初始化的数据存储在.bss段中。我不理解的是为什么我们要为未初始化的变量专门分配一个段?如果未初始化的变量在运行时被赋值,该变量是否仍然只存在于.bss段中?

在下面的程序中,a.data段中,b.bss段中;我的理解是否正确,请纠正我。

#include <stdio.h>
#include <stdlib.h>

int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9};
int b[20]; /* Uninitialized, so in the .bss and will not occupy space for 20 * sizeof (int) */

int main ()
{
   ;
}  

此外,考虑以下程序:

#include <stdio.h>
#include <stdlib.h>
int var[10];  /* Uninitialized so in .bss */
int main ()
{
   var[0] = 20  /* **Initialized, where this 'var' will be ?** */
}

  

10
你可以将BSS理解为“更好的节省空间”。 - smwikipedia
6个回答

116

这样做是为了减小程序的大小。想象一下,如果您的C程序在嵌入式系统中运行,在这些系统中,代码和所有常量都保存在真正的ROM(闪存)中。在调用main()函数之前,必须执行初始的“复制-下降”操作来设置所有静态存储期对象。通常会像这样进行:

for(i=0; i<all_explicitly_initialized_objects; i++)
{
  .data[i] = init_value[i];
}

memset(.bss, 
       0, 
       all_implicitly_initialized_objects);

.data.bss被存储在RAM中,但init_value被存储在ROM中。如果只有一个段,那么ROM将需要填充大量的零,从而显著增加ROM大小。

基于RAM的可执行文件工作方式类似,当然它们没有真正的ROM。

此外,memset很可能是一些非常高效的内联汇编代码,这意味着启动复制向下可以更快地执行。


12
澄清一下:.data 和 .bss 之间唯一的区别是,在启动时,“复制-向下”操作可以顺序运行,因此速度更快。如果没有将它们分成两个段,则初始化必须跳过属于未初始化变量的 RAM 位置,从而浪费时间。 - Jodes
谢谢您对启动过程的解释,但是当.bss中的变量被初始化时会发生什么?它会覆盖0并留在.bss中吗?还是从.bss中删除并写入.data(从而缩短.bss段)? - Axel B

101

.bss段是一种优化。整个.bss段由一个单独的数字描述,可能为4字节或8字节,表示它在运行进程中的大小,而.data部分的大小则等于初始化变量大小的总和。因此,.bss使可执行文件更小,加载更快。否则,变量可以在.data段中具有显式初始化为零;程序很难分辨不同之处。(详细来说,.bss中对象的地址可能与它在.data段中的地址不同。)

在第一个程序中,a将位于可执行文件的.data段中,而b将位于.bss段中。一旦程序加载,区别就变得无关紧要了。运行时,b占用20 * sizeof(int)字节。

在第二个程序中,var被分配空间,并且main()函数中的赋值修改了该空间。恰好var的空间描述在.bss段中而不是.data段中,但这并不影响程序运行时的行为。


19
例如,考虑有许多长度为4096字节的未初始化缓冲区。你希望所有这些4k缓冲区都对二进制文件的大小产生贡献吗?那将浪费很多空间。 - Jeff Mercado
2
@jonathen killer:为什么整个BSS段只用一个数字来描述? - Suraj Jain
@JonathanLeffler,请查看这个问题:“https://dev59.com/omsy5IYBdhLWcg3wsADQ”。我认为所选的最佳答案在解释静态内存分配方面是错误的。您能确认一下吗? - Suraj Jain
3
存储的数字是要填充零的字节数。除非没有这样未初始化的变量,否则bss节的长度不为零,即使加载程序后bss节中所有字节都将为零。 - Jonathan Leffler
1
可执行文件中的.bss节只是一个数字。内存中的进程映像中的.bss节通常是相邻于.data节的内存,并且运行时.data节经常与.bss结合在一起;在运行时内存中没有区分。有时,您可以找到bss开始的位置(edata)。实际上,在进程映像完成后,.bss在内存中不存在;零数据只是.data节的一部分。但具体细节取决于操作系统等因素。 - Jonathan Leffler
显示剩余14条评论

25

来自Jeff Duntemann的《Assembly Language Step-by-Step: Programming with Linux》一书,关于.data节:

  

.data部分包含已初始化数据项的数据定义。初始化数据是指程序运行之前就有值的数据。这些值是可执行文件的一部分。当执行文件被加载进入内存进行执行时,它们会被加载到内存中。

     

需要记住关于.data部分的重要事情是,您定义的已初始化数据项越多,可执行文件的大小就越大,加载它从磁盘进入内存时所需的时间也就越长。

还有.bss节:

  

并非所有数据项都需要在程序开始运行之前具有值。例如,当您从磁盘文件读取数据时,需要为数据分配一个位置以存储读入的数据。像这样的数据缓冲区在程序的.bss部分中定义。您可以为缓冲区分配一定数量的字节并给予其一个名称,但是不需要声明缓冲区中将存在哪些值。

     

在.data部分和.bss部分中定义的数据项之间存在一个关键的区别:在.data部分中定义的数据项会增加可执行文件的大小,而在.bss部分中定义的数据项则不会。一个占据16000字节(或更多,有时远远超过)的缓冲区可以在.bss中定义,并且对于可执行文件的大小几乎没有什么影响(大约50个字节左右用于描述)。


简而言之,在编程中,常量应使用“.data”,指针应使用“.bss”。 - testing_22

11
首先,你样例中的变量并非未初始化;C语言规定,未被显式初始化的静态变量会被初始化为0。因此,使用.bss的原因是为了减小可执行文件的大小,节省空间,使程序加载更快。因为加载器只需分配一堆零,而不需要从磁盘复制数据。
在运行程序时,程序加载器将把.data.bss加载到内存中。对于驻留在.data.bss中的对象的写入操作只会写入到内存中,而不会在任何时候刷新到磁盘上的二进制文件中。

5

System V ABI 4.1 (1997)(也称为 ELF 规范)也包含了答案:

.bss 这个段保存的是未初始化的数据,这些数据会对程序的内存映像做出贡献。根据定义,当程序开始运行时,系统会用零来初始化这些数据。该段不占用文件空间,这一点通过它的段类型 SHT_NOBITS 得到了体现。

上述内容表明段名 .bss 被保留并具有特殊效果,尤其是它不占用文件空间,因此比 .data 更具优势。

当然,缺点是所有字节在操作系统将它们放入内存时都必须设置为 0,这更加严格,但是常见用例,对于未初始化的变量可以正常工作。

SHT_NOBITS 段类型文档重申了这个肯定:

sh_size成员提供了该节的字节数。除非节类型是SHT_NOBITS,否则该节占用文件中的sh_size字节。类型为SHT_NOBITS的节可能具有非零大小,但在文件中不占用空间。

C标准对节没有任何说明,但我们可以使用objdumpreadelf轻松验证变量在Linux中存储的位置,并得出未初始化的全局变量实际上存储在.bss中。例如,请参见此答案:What happens to a declared, uninitialized variable in C?


5
维基百科文章“.bss”提供了一个很好的历史解释,因为该术语来自于20世纪50年代中期(耶,我的生日;-))。
早些时候,每个位都非常宝贵,因此任何用于标记保留的空闲空间的方法都是有用的。这个(.bss)是被采用的其中之一。 .data部分用于不为空的空间,而是将(您的)定义值输入其中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接