编译链接与 ELF
七月 18, 2025 [linux] #compiler #linker #elf #dynamic-lib #build编译链接与 ELF
动态库搜索路径
一个编译好的可执行文件在运行时按以下顺序搜索动态库:
- 可执行文件的
rpath中列出的目录 LD_LIBRARY_PATH环境变量- 可执行文件的
runpath指定的目录 /etc/ld.so.conf中的目录列表- 默认系统库目录:
/lib和/usr/lib
rpath 与 runpath 的区别
rpath在LD_LIBRARY_PATH之前搜索runpath在LD_LIBRARY_PATH之后搜索
设置 rpath:-Wl,-rpath,.
设置 runpath:-Wl,--enable-new-dtags,-rpath,.
$ORIGIN 变量
rpath/runpath 支持 $ORIGIN 表示可执行文件所在目录:
g++ -o main main.o -lrandom -L. -Wl,-rpath,"\$ORIGIN"
安全注意:setuid 程序会忽略 LD_LIBRARY_PATH 和包含 $ORIGIN 的 rpath/runpath,防止权限提升攻击。
rpath 调试备忘录
遇到 error while loading shared libraries 时:
ldd <executable>查看缺失依赖readelf -d <executable> | grep NEEDED检查直接依赖LD_DEBUG=libs ldd <executable>查看搜索过程- 通过
LD_LIBRARY_PATH或 rpath/runpath 添加搜索目录 - 如果
ldd没有缺失项但仍报错,检查是否有 setuid 权限
为什么动态库里不能静态链接
将 .a 静态链接进 .so 的问题:
- 装载问题:
execve()系统调用只负责装载动态库(.so),静态库.a不会被单独装载 - 符号冲突:Linux 动态链接器的符号调解策略是"先入为主",来自
.a的符号可能与系统libstdc++.so冲突 - 全局变量重复:
libstdc++/libpthread维护的全局状态会出现两份,导致运行时异常
正确做法:将 .a 解包成 .o 文件再链接进 .so,并用 visibility 隐藏符号。但即使如此,全局变量重复的问题仍然存在,不推荐这种做法。
若需静态链接标准库到可执行文件:使用 -static(全静态)或 -static-libstdc++(仅静态链接 libstdc++)。
链接器对比:ld.bfd / ld.gold / mold
| 链接器 | 特点 |
|---|---|
| ld.bfd | GNU 默认链接器,兼容性最好 |
| ld.gold | 速度更快,但不支持 --copy-dt-needed-entries |
| mold | 现代化设计,速度极快(构建 LLVM 快 2 倍) |
ld.gold 注意事项:
- 不支持
--copy-dt-needed-entries,间接依赖必须显式指定 - 默认不生成
.gnu.hash段,需加-Wl,--hash-style=gnu或--hash-style=both - 使用
-fuse-ld=gold启用
mold 在 Cargo 中的配置:
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
也可直接 mold -run cargo build。
CMake 配置:
-DCMAKE_EXE_LINKER_FLAGS="-fuse-ld=mold"
ELF 二进制分析
GNU Binutils 常用工具
| 工具 | 用途 |
|---|---|
readelf | 查看 ELF 头、段表、节表、符号表、重定位 |
objdump | 反汇编、查看节内容 |
nm | 列出目标文件符号 |
strings | 提取可打印字符串 |
strip | 剥离符号 |
c++filt | 还原 C++ mangled 名称 |
addr2line | 地址转文件名和行号 |
ldd | 查看动态库依赖 |
常用命令
readelf -h <elf> # ELF 头部
readelf -S <elf> # 节表
readelf -l <elf> # 段表(program headers)
readelf -s <elf> # 符号表
readelf --relocs <elf> # 重定位信息
readelf -d <elf> | grep NEEDED # 动态依赖
objdump -s -j .rodata <elf> # 查看 .rodata 节内容
objdump -b binary -D <bin> -m i386 # 反汇编裸二进制
ldd <elf> # 递归查看动态依赖
LD_DEBUG=bindings ldd /usr/bin/ls # 调试动态链接过程
LD_DEBUG 选项
libs — 库搜索路径
reloc — 重定位处理
symbols — 符号表查找
bindings — 符号绑定
versions — 版本依赖
all — 以上所有
生成 Map 文件
gcc -o main main.c -Wl,-Map,main.map
查看编译器预定义宏
g++ -E -dM - < /dev/null
CFLAGS / LDFLAGS
CFLAGS="-g -O2"
LDFLAGS="-L/opt/lib -Wl,-R/opt/lib" # -R 同时设置运行时搜索路径
LIBS="-lpthread -liconv"
LDFLAGS 指定 -L 只能让链接器在编译时找到库,运行时仍需通过 rpath 或 LD_LIBRARY_PATH 指定;添加 -Wl,-R<path> 可将路径写入 ELF。
常用编译选项
gcc -mxgot ... # MIPS 大 GOT 支持
gcc -Wl,-z,max-page-size=4096 # 控制页对齐
gcc -fno-pie -no-pie ... # 禁用 PIE
gcc -static ... # 全静态链接
gcc -static-libstdc++ ... # 仅静态链接 libstdc++
gcc -Wl,--verbose ... # 查看链接器详细过程
# 查看编译选项
readelf -p .comment <elf> # 查看编译器版本信息
Rust 编译调试
export RUSTFLAGS="-C link-arg=-Wl,--verbose"
export RUSTC_LOG=rustc_codegen_ssa::back::link=info
cargo build --bin http-proxy
AT&T 与 Intel 汇编语法
| 区别 | AT&T | Intel |
|---|---|---|
| 寄存器前缀 | %eax | eax |
| 立即数前缀 | $1 | 1 |
| 操作数顺序 | movl $1, %eax (源→目的) | mov eax, 1 (目的←源) |
| 内存寻址 | immed32(base, index, scale) | [base + index*scale + immed32] |
Switch Case 中定义变量
C/C++ 中 switch 的 case 标签后不能直接定义变量,因为变量的初始化可能因条件不满足而被跳过。解决方案:
- 用
{}括起来创建作用域 - 将变量定义在
switch之外
sockaddr 与 sockaddr_in
sockaddr:通用套接字地址,sa_data[14]混合存放地址和端口sockaddr_in:IPv4 专用,将端口(sin_port)和地址(sin_addr)分开存储- 二者长度相同(16 字节),可以强制类型转换
- 使用时先填充
sockaddr_in,再转为sockaddr传入函数 htons()/htonl()将主机字节序转为网络字节序inet_addr()将 IP 字符串转为网络字节序整数