1. 问题描述
C 语言编写 hello world如下:
1 2 3 4 5 6 |
#include <stdio.h> int main() { printf("Hello\n"); return 0; } |
编译之后,发现在 debian testing 下生成的二进制文件过大(约17KB)
Distro | Release | GCC Version | LD Version | Libc Version | ABI | FileSize |
---|---|---|---|---|---|---|
Debian | buster | 7.4.0/8.2.0 | 2.31.1 | 2.28-5 | 3.2.0 | 17 KB |
Debian | stretch | 6.3.0-18 | 2.28 | 2.24-11 | 2.6.32 | 8.5 KB |
Ubuntu | 18.04 | 7.3.0 | 2.30 | 2.27-3 | 3.2.0 | 8.2 KB |
Arch | 2019.01 | 8.2.1 | 2.32.1 | 2.28-5 | 3.2.0 | 17KB |
2. 排查过程
2.1 检查 gcc 优化级别、尝试 strip 移除符号
- 不同的 gcc -O 优化级别对 ELF 文件大小基本无影响,问题未解决
- strip 移除符号,文件体积大概减少 2KB,问题未解决
2.2 比较 ELF 头部
使用 readelf、nm、objdump 等工具对比 ELF 文件信息,甚至使用了 010 editor 查看文件内容
下面列一下对两个文件执行 readelf -S 的输出,左侧为 debian stretch 下编译的 8.5KB 小文件,右侧为 debian buster 下编译的 17KB 大文件。
可以看出来的是,右侧文件很多section 都以 4096(0x1000) 为长度进行了对齐,链接器似乎是问题的关键
2.3 排查 ld 的问题
1) 使用 gcc -c 或者 nasm 编译汇编,生成中间文件 .o
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
cat > hello.asm << 'EOF' use64 stdout equ 1 syscall_write equ 1 syscall_exit equ 60 section .data hello db 'Hello, world!', 10 hello_len equ $-hello section .text global _start _start: mov rax, syscall_write mov rdi, stdout mov rsi, hello mov rdx, hello_len syscall mov rax, syscall_exit mov rdi, 0 syscall EOF # 使用 nasm 编译汇编代码,生成中间文件 nasm -f elf64 -o hello.o hello.asm |
随后使用 ld hello.o
对文件进行链接,发现在 debian buster 和 debian stretch 下的文件大小分别为 9 KB 和 1.1 KB
2) 检查链接过程
使用命令 ld --verbose hello.o
,输出详细的链接过程信息,并对比不同系统下的输出,发现一些明显不同的地方(右侧为 ld 2.31 的输出)
3)改变 ld align 参数
参考链接:https://stackoverflow.com/questions/15400910/ld-linker-script-producing-huge-binary
-
方案一: ld --nmagic(-n)或者 ld --omagic(-N)
实际测试表明,使用 -n/-N 参数,可以将汇编写的hello.asm文件体积控制在1KB左右,同时 ELF 二进制文件正常运行;
但是,文档说这个参数会禁用动态链接,将此参数通过 gcc 传递给 ld 链接 C 库时确实发生了错误:12345gcc hello.c -Xlinker -n # 或 gcc hello.c -Wl,-n# 输出如下:/usr/bin/ld: cannot find -lgcc_s/usr/bin/ld: cannot find -lgcc_scollect2: error: ld returned 1 exit status) -
方案二:gcc -z max-page-size=0x1000
实际测试结果:
debian buster 上 ELF 文件体积无显著变化,减小该值 ELF 二进制无法运行; 增大该值,ELF 正常运行,文件体积增大 (系统PAGESIZE大小: getconf PAGESIZE ==> 4096)
debian stable 上减少该值 ELF 文件体积稍微减小,低于一定数值ELF二进制文件则无法运行; 增大该值,文件体积不变 (系统PAGESIZE大小:getconf PAGESIZE=4096) -
方案三: 使用 gold 替代 ld
手动使用 gold 进行链接,或者gcc hello.c -fuse-ld=gold
, 在 debian buster 上文件大小为 7.8 KB,ELF 文件正常运行
备注:系统里 gold 版本同 ld; 使用gcc -fuse-ld=bfd
即可使用默认的 ld
2.4 最终的答案
Binutils 2.31 will enable -z separate-code by default for x86 to avoid
mixing code pages with data to improve cache performance as well as
security. To reduce x86-64 executable and shared object sizes, the
maximum page size is reduced from 2MB to 4KB. But x86-64 kernel must
be aligned to 2MB. Pass -z max-page-size=0x200000 to linker to force
2MB page size regardless of the default page size used by linker.Tested with Linux kernel 4.15.6 on x86-64.
参考链接:https://lore.kernel.org/patchwork/patch/934898/
这段话说的很明白了,因此如果我们想要获得较小体积的二进制文件,同时又不能减小 max-page-size 的话(上述实验可知,不合适的max-page-size会导致链接后的程序无法运行),我们可以选择关闭 separate-code 选项; 因此最终解决方案如下:
gcc hello.c -z noseparate-code