GCC生成的二进制文件充满了零

6

我正在努力弄清楚为什么GCC生成的二进制文件如此庞大。

考虑这个空程序:

int main() {
    return 0;
}

现在我使用GCC 9.2.1 20190827 (Red Hat 9.2.1-1)glibc 2.29构建它,没有任何额外的参数:

gcc -o test test.c

生成的二进制文件大小为21984字节(约22 KB)。使用xxd查看生成的文件,多处都有长串的空字节。
00000370: 006c 6962 632e 736f 2e36 005f 5f6c 6962  .libc.so.6.__lib
00000380: 635f 7374 6172 745f 6d61 696e 0047 4c49  c_start_main.GLI
00000390: 4243 5f32 2e32 2e35 005f 5f67 6d6f 6e5f  BC_2.2.5.__gmon_
000003a0: 7374 6172 745f 5f00 0000 0200 0000 0000  start__.........
000003b0: 0100 0100 0100 0000 1000 0000 0000 0000  ................
000003c0: 751a 6909 0000 0200 1d00 0000 0000 0000  u.i.............
000003d0: f03f 4000 0000 0000 0600 0000 0100 0000  .?@.............
000003e0: 0000 0000 0000 0000 f83f 4000 0000 0000  .........?@.....
000003f0: 0600 0000 0200 0000 0000 0000 0000 0000  ................
00000400: 0000 0000 0000 0000 0000 0000 0000 0000  ................
<3040 bytes of zeroes>
00000ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00001000: f30f 1efa 4883 ec08 488b 05e9 2f00 0048  ....H...H.../..H
<not zeroes>
00001190: f30f 1efa c300 0000 f30f 1efa 4883 ec08  ............H...
000011a0: 4883 c408 c300 0000 0000 0000 0000 0000  H...............
000011b0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
<3632 bytes of zeros>
00001ff0: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00002000: 0100 0200 0000 0000 0000 0000 0000 0000  ................
00002010: 011b 033b 3400 0000 0500 0000 10f0 ffff  ...;4...........
<not zeroes>
000020e0: 410e 2842 0e20 420e 1842 0e10 420e 0800  A.(B. B..B..B...
000020f0: 1000 0000 ac00 0000 98f0 ffff 0500 0000  ................
00002100: 0000 0000 0000 0000 0000 0000 0000 0000  ................
<3376 bytes of zeroes>
00002e40: 0000 0000 0000 0000 0000 0000 0000 0000  ................
00002e50: 0011 4000 0000 0000 d010 4000 0000 0000  ..@.......@.....
...

因此,生成的二进制文件大小约为10 KB,几乎有一半是空的。
使用 size -A 查看,大小更接近于一个仅返回退出代码的程序。
test  :
section                 size      addr
.interp                   28   4194984
.note.ABI-tag             32   4195012
.note.gnu.build-id        36   4195044
.gnu.hash                 28   4195080
.dynsym                   72   4195112
.dynstr                   56   4195184
.gnu.version               6   4195240
.gnu.version_r            32   4195248
.rela.dyn                 48   4195280
.init                     27   4198400
.text                    373   4198432
.fini                     13   4198808
.rodata                   16   4202496
.eh_frame_hdr             52   4202512
.eh_frame                192   4202568
.init_array                8   4210256
.fini_array                8   4210264
.dynamic                 400   4210272
.got                      16   4210672
.got.plt                  24   4210688
.data                      4   4210712
.bss                       4   4210716
.comment                  44         0
.gnu.build.attributes   4472   4218912
Total                   5991

使用 GCC 9.2.0musl 1.1.23 进行 PowerPC 的交叉编译,二进制文件大小更大。使用 xxd 查看,有连续的 64074 字节全为零,二进制文件大小为 67872 字节(约 67 KB)。
但是,size -A 报告此版本的大小更小:
test  :
section              size        addr
.interp                26   268435796
.note.gnu.build-id     36   268435824
.hash                  36   268435860
.dynsym                64   268435896
.dynstr                39   268435960
.rela.plt              12   268436000
.init                  28   268436012
.text                 496   268436048
.fini                  28   268436544
.eh_frame_hdr          28   268436572
.eh_frame              80   268436600
.init_array             4   268566284
.fini_array             4   268566288
.dynamic              216   268566292
.branch_lt              8   268566508
.got                   12   268566516
.plt                    4   268566528
.data                   4   268566532
.bss                   28   268566536
.comment               17           0
Total                1170

我还尝试使用我手边现成的旧版GCC(版本为GCC 4.7.2和 uClibc 1.0.12 )编译程序。使用这种组合,得到的二进制文件仅有4769个字节(约4 KB),并且其中没有明显的空字节运行。
为了确保这不仅发生在什么也不做的小程序上,我查看了一些我用 GCC 9.2.0 和 musl 1.1.23 交叉编译的真实程序。例如,使用 -Os 编译和剥离的tcpdump二进制文件中包含32628个字节长的连续空字节运行。那么,为什么零会占用所有磁盘空间呢?

你对 objdump 和链接器的交叉引用有什么研究发现吗?你尝试过剥离调试信息吗? - the busybee
1
尝试使用-Os -flto -ffunction-sections -fdata-sections编译,并使用-flto -Wl,--gc-sections链接。这样应该可以摆脱一些不必要的东西。 - Erlkoenig
虽然-gc-sections不是零的实际原因,但它确实减小了二进制文件的大小。我可能会启用它们来构建系统。 - Antti Laine
3个回答

7
最近的binutils默认使用-z separate-code,它会向程序添加额外的PT_LOAD段,需要进一步对齐。
你可以像这样覆盖默认设置:
gcc -Wl,-z,noseparate-code -o test test.c

由于对齐要求,此更改仍将保留一些零。

这实际上是在 PowerPC 上启用的(或者说是禁用了分离代码),但你的回答促使我从正确的方向查看。请参见被接受的答案。 - Antti Laine

3

Florian Weimer的答案 帮助我找到了正确的方向。问题不在于-z separate-code,而是在于-z relro。

通过将-Wl,-z,norelro添加到PowerPC GCC选项中,一个空程序的文件大小从67872字节减少到3772字节! 在x64上的影响较小:从21984字节到18584字节。 在一个小但实际可用的程序上,PowerPC上的差异约为50%较小,并且与我之前比较过的tcpdump相比,差异几乎达到32 KB。

显然,relro链接器选项会创建一个新段,用于重新映射全局偏移表并将其标记为只读,从而保护程序免受堆栈溢出攻击。这个解释可能不准确;在我试图弄清楚它时,我没有理解太多。

PPC上的大小差异正好为62 KB。 为什么会创建这么大的区域,我不知道。

虽然该设置作为加固措施应该被保留启用,但不幸的是,我的目标板只有11 MB的可用闪存,我正在尝试将基于Linux的系统安装在上面,因此每个字节都很重要,我将禁用该设置以保持二进制文件大小。


1
这让我想起了gcc的另一个问题。如果您的程序有:static int g [1000000] = {[999999] = 1};那么编译器会在可执行文件中创建一个4MB的数据块,其中包含3.9999 MB的零和最后的1。而icc编译器实际上使用lzma压缩初始数据块,并在程序启动时解压它。 - M.M

1
所以,为什么零字节会试图消耗我的磁盘空间?
因为在大多数现代系统上,磁盘上的额外22K字节是无关紧要的。
您观察到的某些成本是由于动态链接,有些则是由于填充,有些则是为了帮助您进行调试(例如.comment,.note.gnu.build-id,.eh_frame*)。
通过不使用libc并静态链接和剥离,我可以将二进制文件降至624个字节。
cat t.c
void _start()
{
  __asm__("movq $60,%rax; xorq %rdi,%rdi; syscall");
}

gcc -O3 t.c -static -nostdlib -Wl,-z,noseparate-code,--build-id=none &&
strip --strip-all a.out &&
./a.out && ls -l a.out
-rwxr-x--- 1 me mygroup 624 Nov 25 19:34 a.out

仍然可以删除.comment.eh_frame


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