为什么静态链接的“hello world”程序这么大(超过 650 KB)?

问题描述 投票:0回答:4

考虑以下程序:

#include <stdio.h>
int main(void)
{
  printf("hello world\n");
  return 0;
}

如果我使用 GCC 构建它,优化大小并使用静态链接,然后剥离它以进一步最小化大小(也许):

$ gcc -Os -static hello.c -o hello
$ strip hello

我得到了一个大小约为 695 KB 的可执行文件。

为什么这么大?我意识到这不仅仅是我的目标代码,还有存根和其他什么,但仍然是巨大的。 备注:

操作系统:Devuan GNU/Linux Chimaera (~= Debian Bullseye)
  • 编译器:GCC 10.2
  • libc:glibc 2.31-13
  • 处理器架构:x86_64
  • 如果我使用
  • -O3 -flto
  • 进行构建,它不会得到改善。
    
        
c static-linking size-reduction binary-size
4个回答
4
投票

我用

-static

以及特殊的编译器参数

-Wl,-Map,a.map
编译了你的程序,它要求链接器写出一个文件
a.map
(你可以在该咒语中的第二个逗号后面放置任何你喜欢的名称),这解释了为什么每个目标文件包含在链接中。 这些是该文件的前几行,为了可读性稍作编辑:
Archive member included to satisfy reference by file (symbol)

/usr/lib/libc.a(libc-start.o)
                              /usr/lib/crt1.o (__libc_start_main)
/usr/lib/libc.a(check_fds.o)
                              /usr/lib/libc.a(libc-start.o) (__libc_check_standard_fds)
/usr/lib/libc.a(libc-tls.o)
                              /usr/lib/libc.a(libc-start.o) (__libc_setup_tls)
/usr/lib/libc.a(errno.o)
                              /usr/lib/libc.a(check_fds.o) (__libc_errno)
/usr/lib/libc.a(assert.o)
                              /usr/lib/libc.a(libc-start.o) (__assert_fail)
/usr/lib/libc.a(dcgettext.o)
                              /usr/lib/libc.a(assert.o) (__dcgettext)

这意味着,
在链接器之前

查看程序的代码,当它仍在处理调用main的函数的传递依赖项时,它需要拉入打印的代码断言失败消息,以及

that
代码拉入用于动态加载和打印本地化的代码(翻译成用户的本机语言)错误消息。 看起来你的 600K 二进制可执行文件中的大部分是代码及其依赖项,其中包括所有 malloc、所有
fprintf
、所有
iconv
gettext
“消息对象”的解析器文件,...
    


4
投票

  • 与您使用printf有任何关系吗。
  • 与编译器优化代码的能力有任何关系。 是否
  • 没有
  • 与您包含<stdio.h>有任何关系。
    
    
  • 为什么?因为即使你编译一个空程序:

int main(void) { return 0; }

您仍然获得相同的 695 KB 可执行文件。

感谢@SparKot 指出这个方向的评论。


1
投票
-static

您将代码编译为静态可执行文件,因此它从 

libc.a

链接所有库(例如 stdio),提取使用的模块并将它们链接到程序代码中,而不是从

libc.so
链接。 不幸的是,
printf()
(以及许多标准函数)是一个复杂的例程,它处理输入/输出的缓冲,并且当您必须在程序中包含代码时使您的可执行文件变得更大。
另一方面,如果您允许链接器执行

动态链接

,您将获得大约 16 kb 的动态可执行文件,并且无需额外的加载工作,因为 libc 始终会在系统内存中为您预加载(因为几乎每个程序使用它,因此一旦系统中动态链接的第一个程序启动,它就会被加载 - 例如 systemd

init
)。 如果标准库是共享对象,则内核不需要加载标准库,因为它通常在程序启动时已经加载。 当您静态链接程序时,不会发生这种情况。 该库的使用代码包含在您的可执行文件中,但由于其中不存在共享代码...必须从可执行文件将整个兆字节加载到内存中,启动时加载会降低性能。
您可以保持在该大小以下,但为此您必须在汇编程序中完成,而不是链接标准 C 库和运行时模块。

下一个程序,用汇编程序,减少到 8488 字节。 除了使用的代码仅是以下字节(反汇编后),其余部分用于 ELF 合规性(默认重定位表等)

401000: b8 04 00 00 00 mov $0x4,%eax 401005: bb 01 00 00 00 mov $0x1,%ebx 40100a: b9 00 20 40 00 mov $0x402000,%ecx 40100f: ba 0d 00 00 00 mov $0xd,%edx 401014: cd 80 int $0x80 401016: b8 01 00 00 00 mov $0x1,%eax 40101b: bb 00 00 00 00 mov $0x0,%ebx 401020: cd 80 int $0x80

列表是:

你好.s

.globl _start .section .data output: .ascii "Hello, world\n" output_end: .section .text _start: movl $4, %eax ; system call (4, write) movl $1, %ebx ; output descriptor (stdout, 1) movl $output, %ecx ; output string (above) movl $(output_end - output), %edx ; string size int $0x80 ; system call. movl $1, %eax ; system call (1, exit) movl $0, %ebx ; exit code (0) int $0x80 ; system call.

只有 34 字节的程序,但剥离后可执行文件仍然是 8kb。  这是因为它被格式化为 ELF 二进制可执行文件,这需要一些额外的数据空间用于空符号表等(某些部分被填充到一整页代码,可能是 
.text

部分的一页,并且另一个用于

.data
部分)
另一方面,如果您想查看链接阶段包含的内容,请将 

-v

添加到编译器调用中,这样它就会打印用于调用链接器的命令行,您将看到链接中链接的所有内容最终可执行文件。

要编译/链接上述程序,只需执行以下操作:

$ as hello.s -o hello.o $ ld -o hello hello.o $ ./hello Hello, world $ _



0
投票
gcc -static

缩小单文件脚本(无外部依赖项)失败了。我建议大家使用

musl-gcc
(在 Ubuntu 上,安装
musl-tools
)。
之前:

gcc foo.c -O -static -s # ./a.out 814K

之后:

musl-gcc foo.c -O -static -s # ./a.out 35K

	
© www.soinside.com 2019 - 2024. All rights reserved.