如果我理解正确,ELF文件中的.bss
部分用于为零初始化的变量分配空间。我们的工具链生成ELF文件,因此我的问题是:是否实际上需要将所有这些零保存在.bss
部分中?当我分配一个全局10兆字节的数组时,ELF文件中会产生10兆字节的零,似乎这是一种非常浪费空间的方式。我在哪里看错了呢?
我已经有一段时间没有使用 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
)的FileSize
和MemSiz
存在显著差异:
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。
.bss
节用于静态数据,这些数据在编程时没有被初始化,但在运行时保证被设置为零。下面是一个小例子,可以解释它们之间的区别。int main() {
static int bss_test1[100];
static int bss_test2[100] = {0};
return 0;
}
bss_test1
被放置在 .bss
中。然而,bss_test2
与一堆零一起被放置在 .data
段中。运行时加载器基本上会分配为 .bss
保留的空间大小,并在任何用户空间代码开始执行之前将其清零。objdump
、nm
或类似的实用程序来查看差异:moozletoots$ objdump -t a.out | grep bss_test
08049780 l O .bss 00000190 bss_test1.3
080494c0 l O .data 00000190 bss_test2.4
这通常是嵌入式开发人员遇到的第一个“惊喜”之一...不要显式将静态变量初始化为零。运行时加载程序(通常)会处理这个问题。一旦您显式初始化任何内容,就告诉编译器/链接器将数据包含在可执行映像中。
.bss
段不存储在可执行文件中。在ELF文件中,最常见的段(.text
、.data
、.bss
)中,只有.text
(实际代码)和.data
(已初始化数据)存在。
.bss
部分至少在 ELF 可执行文件中被存储。但其内容未被存储,因此文件中的 .bss
大小是一个小常数。在具有内存保护的操作系统中,需要以某种方式存储 .bss
部分,以便加载程序可以在该位置安排可写内存。当然,在某些格式中,.bss
的剩余部分可能只是对已分配但未复制大小字段的贡献。 - textshell没错,.bss 在文件中并不存在,它的大小信息只是为了动态加载器来分配应用程序的 .bss 段。
作为一个规则,只有 LOAD、TLS 段获得应用程序的内存,其余部分用于动态加载器。
对于静态可执行文件,bss 段也在可执行文件中给予空间。
在没有加载器的嵌入式应用程序中,这很常见。
Suman
int is[1000000]
创建一个 hello world 程序,再创建一个没有这个数组的程序,编译并查看编译后的大小 :-) 然后为了真正理解,可以使用 binutils 进行反汇编,或者使用-S
编译成汇编代码。 - Ciro Santilli OurBigBook.com