考虑以下程序:
#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)
-O3 -flto
我用
-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
“消息对象”的解析器文件,...
printf
有任何关系吗。<stdio.h>
有任何关系。
int main(void)
{
return 0;
}
您仍然获得相同的 695 KB 可执行文件。感谢@SparKot 指出这个方向的评论。
-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
$ _
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