.bss节中的零初始化变量在elf文件中占用空间吗?

46

如果我理解正确,ELF文件中的.bss部分用于为零初始化的变量分配空间。我们的工具链生成ELF文件,因此我的问题是:是否实际上需要将所有这些零保存在.bss部分中?当我分配一个全局10兆字节的数组时,ELF文件中会产生10兆字节的零,似乎这是一种非常浪费空间的方式。我在哪里看错了呢?


1
快速回答的方法:使用 int is[1000000] 创建一个 hello world 程序,再创建一个没有这个数组的程序,编译并查看编译后的大小 :-) 然后为了真正理解,可以使用 binutils 进行反汇编,或者使用 -S 编译成汇编代码。 - Ciro Santilli OurBigBook.com
4个回答

69

我已经有一段时间没有使用 ELF 了。但我想我仍然记得这些东西。不,它并不在物理上包含那些零。如果您查看 ELF 文件程序头,那么您将看到每个头都有两个数字:一个是文件中的大小,另一个是在虚拟内存中分配时该部分所占的大小(readelf -l ./a.out):

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  PHDR           0x000034 0x08048034 0x08048034 0x000e0 0x000e0 R E 0x4
  INTERP         0x000114 0x08048114 0x08048114 0x00013 0x00013 R   0x1
      [Requesting program interpreter: /lib/ld-linux.so.2]
  LOAD           0x000000 0x08048000 0x08048000 0x00454 0x00454 R E 0x1000
  LOAD           0x000454 0x08049454 0x08049454 0x00104 0x61bac RW  0x1000
  DYNAMIC        0x000468 0x08049468 0x08049468 0x000d0 0x000d0 RW  0x4
  NOTE           0x000128 0x08048128 0x08048128 0x00020 0x00020 R   0x4
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x4

类型为LOAD的头文件在文件执行时被复制到虚拟内存中。其他头文件包含其他信息,例如所需的共享库。正如您所看到的,包含bss段的头文件(第二个LOAD)的FileSizeMemSiz存在显著差异:

0x00104 (file-size) 0x61bac (mem-size)

对于这个示例代码:

int a[100000];
int main() { }

ELF规范指出,对于内存大小大于文件大小的段,在虚拟内存中只是用零填充。第二个LOAD头的段与节的映射如下:

03     .ctors .dtors .jcr .dynamic .got .got.plt .data .bss

除了用于C++构造函数/析构函数的部分和Java部分之外,还有一些其他部分。然后它包含了一个.dynamic部分的副本和其他有用于动态链接的内容(我认为这里包含了所需的共享库等其他内容)。之后是包含已初始化全局变量和本地静态变量的.data部分。最后,出现了.bss部分,在加载时由零填充,因为文件大小不覆盖它。

顺便说一下,您可以使用-M链接器选项查看特定符号将放置在哪个输出节中。对于gcc,您可以使用-Wl,-M将该选项传递给链接器。上面的示例显示a被分配在.bss中。它可以帮助您验证未初始化对象确实最终在.bss中而不是其他位置:

.bss            0x08049560    0x61aa0
 [many input .o files...]
 *(COMMON) 
 *fill*         0x08049568       0x18 00
 COMMON         0x08049580    0x61a80 /tmp/cc2GT6nS.o
                0x08049580                a
                0x080ab000                . = ALIGN ((. != 0x0)?0x4:0x1) 
                0x080ab000                . = ALIGN (0x4) 
                0x080ab000                . = ALIGN (0x4) 
                0x080ab000                _end = .

默认情况下,GCC会将未初始化的全局变量保存在COMMON section中,以便与旧编译器兼容,该编译器允许在程序中多次定义全局变量而不出现多重定义错误。使用-fno-common可以使GCC将未初始化的全局变量存储在.bss节而非COMMON section中(对于最终链接的可执行文件没有影响,因为它仍然会进入.bss输出section中。这由链接器脚本控制。使用ld -verbose命令显示)。但是这不应该让您感到担心,这只是一个内部细节。请参阅gcc的manpage。


我假设必须将NOBITS节类型设置为允许这样做? - Wouter Lievens
Wouter。嗯,我从未使用过那个标志。我的GCC机器头文件看起来像这样:#define BSS_SECTION_ASM_OP "\t.section\t.bss, "aw""。 - Johannes Schaub - litb
你们两个都是对的 - 对于gcc来说,只需要知道符号应该放在.bss中。 ld会负责在这个部分上设置NOBITS,并表示“文件中没有数据 - 零它”。 - Shirkrin
讲解得很清楚。我还有一个问题:您知道使用哪个工具包来转储所有静态或全局变量的地址吗?例如,在您上面的代码中,该工具包应将'a'的地址给出为'0x08049580'。我尝试了readelf -s,但它仅适用于单个obj,而我想从最终可执行文件中获取这些地址。谢谢! - lukmac
很好的解释。你从哪里获取了所有这些信息?有没有推荐的人/文档/书籍? - kikeenrique
@kikeenrique:ELF文件格式规范+经验。这个家伙有很多经验。 - The Mask

21
在ELF文件中,.bss节用于静态数据,这些数据在编程时没有被初始化,但在运行时保证被设置为零。下面是一个小例子,可以解释它们之间的区别。
int main() {
    static int bss_test1[100];
    static int bss_test2[100] = {0};
    return 0;
}

在这种情况下,由于其未初始化,bss_test1 被放置在 .bss 中。然而,bss_test2 与一堆零一起被放置在 .data 段中。运行时加载器基本上会分配为 .bss 保留的空间大小,并在任何用户空间代码开始执行之前将其清零。
您可以使用 objdumpnm 或类似的实用程序来查看差异:
moozletoots$ objdump -t a.out | grep bss_test
08049780 l     O .bss   00000190              bss_test1.3
080494c0 l     O .data  00000190              bss_test2.4

这通常是嵌入式开发人员遇到的第一个“惊喜”之一...不要显式将静态变量初始化为零。运行时加载程序(通常)会处理这个问题。一旦您显式初始化任何内容,就告诉编译器/链接器将数据包含在可执行映像中。


4
在我的平台上,gcc 将 bss_test2 放入了 .bss 段中。你可以提到 -fno-zero-initialized-in-bss 编译选项,它可以控制这个行为。 - tristan
1
从手册中:如果目标支持BSS节,GCC默认将初始化为零的变量放入BSS中。 - OrangeDog

3

.bss段不存储在可执行文件中。在ELF文件中,最常见的段(.text.data.bss)中,只有.text(实际代码)和.data(已初始化数据)存在。


1
这并不是任意可执行文件上的readelf告诉我的。文件中有很多节,包括.bss节。 - Wouter Lievens
它不取决于ELF本身,而是取决于您的编译链(语言、工具、选项,如调试等)。您还可以拥有自己的自定义节。 - mouviciel
1
.bss 部分至少在 ELF 可执行文件中被存储。但其内容未被存储,因此文件中的 .bss 大小是一个小常数。在具有内存保护的操作系统中,需要以某种方式存储 .bss 部分,以便加载程序可以在该位置安排可写内存。当然,在某些格式中,.bss 的剩余部分可能只是对已分配但未复制大小字段的贡献。 - textshell

1

没错,.bss 在文件中并不存在,它的大小信息只是为了动态加载器来分配应用程序的 .bss 段。

作为一个规则,只有 LOAD、TLS 段获得应用程序的内存,其余部分用于动态加载器。

对于静态可执行文件,bss 段也在可执行文件中给予空间。

在没有加载器的嵌入式应用程序中,这很常见。

Suman


你说,TLS也作为PT_LOAD加载吗?我看到PT_TLS包含在PT_LOAD中。 - osgx

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