ELF 二进制分析与常用编译链接工具
七月 18, 2026 [Linux] #elf #gcc #ld #readelf #objdump #编译拿到一个陌生二进制,想知道它是怎么编译出来的?或者构建时遇到链接问题,需要快速定位?本文整理了 ELF 文件分析、编译选项提取以及常见链接器技巧。
从 ELF 中提取编译信息
编译器版本与 GLIBC 版本
# 查找 GCC 版本
strings libxxx.so | grep GCC # GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
# 查找 GLIBC 版本
strings libxxx.so | grep GLIBC # GLIBC_2.31, GLIBC_2.34 等
# 查找架构相关编译选项
strings /path/to/binary | grep march # -march=skylake
通过 DWARF 调试信息读取
如果二进制包含调试信息(未 strip),用 readelf 最精确:
readelf --debug-dump=info /path/to/binary | grep DW_AT_producer
.debug_info 节中的关键字段:
| 字段 | 含义 | 示例 |
|---|---|---|
DW_AT_producer | 编译器版本与编译选项 | GNU C++17 9.3.0 -march=skylake -O2 -flto |
DW_AT_language | 源码语言 | C++(值 4)、C(值 1) |
DW_AT_name | 源文件名 | main.cpp |
DW_AT_comp_dir | 编译时工作目录 | /home/user/project/build |
# 查看 ELF 有多少调试节
readelf -S /path/to/binary | grep debug
# 查看各个调试节内容
readelf -wi /path/to/binary # .debug_info
readelf -wl /path/to/binary # .debug_line
没有调试信息怎么办
若二进制已被 strip,可以通过 .comment 节和 .note 节获取部分信息:
readelf -p .comment /path/to/binary
# 通常包含 GCC 版本字符串
readelf -n /path/to/binary
# 包含 build ID、GNU 属性等
ELF 文件结构速览
入口点
ELF 文件头中的 e_entry 字段指定了程序第一条指令的地址。链接器默认设为 _start 符号的地址,可用 -e 覆盖:
readelf -h /path/to/binary | grep Entry
# Entry point address: 0x401040
# 查看链接器默认入口
ld -verbose | grep -i entry
节与段
# 查看节头表
readelf -S /path/to/binary
# 查看程序头(段)
readelf -l /path/to/binary
# 查看特定节内容
readelf -p .rodata /path/to/binary
objdump -s -j .rodata /path/to/binary
GOT 与 PLT
动态链接的核心机制:
# 查看 PLT
objdump -j .plt -d /path/to/binary
# 查看 GOT
readelf --hex-dump .got.plt /path/to/binary
# 重定位信息
readelf -r /path/to/binary
常用二进制分析工具
readelf
# 所有节头
readelf -S
# 程序头(段)
readelf -l
# 重定位表
readelf -r
# 符号表
readelf -s
# 动态段
readelf -d
objdump
# 反汇编
objdump -d /path/to/binary
# 反汇编特定节
objdump -j .plt -d /path/to/binary
# 以十六进制查看节内容
objdump -s -j .rodata /path/to/binary
ldd — 查看动态库依赖
ldd /path/to/binary
# 列出所有依赖的共享库及其路径
如果某个库找不到,会显示 not found,通常需要调整 LD_LIBRARY_PATH 或安装对应包。
编译链接实用技巧
CFLAGS / LDFLAGS 用法
# 编译/链接选项
./configure CFLAGS='-g -O2 -march=native' LDFLAGS='-L/usr/local/lib'
# 运行时库路径
LDFLAGS='-L/var/xxx/lib -Wl,-R/var/xxx/lib'
# 查看完整编译链接命令
make VERBOSE=1 # CMake 项目
make V=1 # autotools 项目
-ffunction-sections / -fdata-sections
将每个函数或数据放入独立节,配合 --gc-sections 可剔除未使用的代码,减小二进制体积。
LTO(Link Time Optimization)
# GCC/Clang
CFLAGS="-flto"
# thinLTO(Clang,并行化更好)
CFLAGS="-flto=thin"
Gold / ld / lld
| 链接器 | 特点 |
|---|---|
ld.bfd | GNU 默认,稳定但较慢 |
ld.gold | 支持并行,链接大项目快 |
lld | LLVM 实现,性能极佳 |
# 切换链接器
-fuse-ld=gold
-fuse-ld=lld
-fpic vs -fPIC
两者都生成位置无关代码用于共享库。-fpic 使用较小的 GOT 尺寸(架构相关限制),-fPIC 支持更大范围,通用性更强。不确定时用 -fPIC。
-pie
生成位置无关可执行文件(PIE),利于 ASLR 安全加固。现代发行版默认开启。
-Wl,--wrap=symbol
用 __wrap_symbol 替换程序中对 symbol 的所有引用,同时可通过 __real_symbol 调用原始函数。常用于 mock 或 hook 系统调用:
void *__wrap_malloc(int c) {
printf("malloc(%d) called\n", c);
return __real_malloc(c);
}
DSO missing from command line
编译时链接顺序导致符号找不到:
# 临时解决(不推荐长期使用)
export LDFLAGS="-Wl,--copy-dt-needed-entries"
根本解法是正确排列库顺序:被依赖的库放在依赖者之后。
GCC 内联编译测试
g++ -xc++ - <<< $'#include <iostream>\nint main(){std::cout << "hello\\n";}' -o /tmp/main
VA_ARGS 与 ##VA_ARGS
// 基本用法
#define log(fmt, ...) printf(fmt, __VA_ARGS__)
// 防止空参数时末尾多逗号
#define log(fmt, ...) printf(fmt, ##__VA_ARGS__)
##__VA_ARGS__ 是 GNU 扩展:当可变参数为空时,自动删除前面的逗号,避免编译错误。
参考链接: