自从我与 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 类型的标头是在加载文件执行时复制到虚拟内存中的标头。其他标头包含其他信息,例如所需的共享库。如您所见,FileSize 和 MemSiz 对于包含 bss 部分的标头(第二个 LOAD 之一)明显不同:
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 部分,以与旧编译器兼容,允许在程序中定义两次全局变量而不会出现多个定义错误。使用-fno-common 使 GCC 将 .bss 部分用于目标文件(对最终链接的可执行文件没有影响,因为正如您所见,它无论如何都会进入 .bss 输出部分。这由 链接器脚本。用ld -verbose 显示它)。但这不应该吓到你,这只是一个内部细节。请参阅 gcc 的联机帮助页。