编译链接与 ELF

七月 18, 2025 [linux] #compiler #linker #elf #dynamic-lib #build

编译链接与 ELF

动态库搜索路径

一个编译好的可执行文件在运行时按以下顺序搜索动态库:

  1. 可执行文件的 rpath 中列出的目录
  2. LD_LIBRARY_PATH 环境变量
  3. 可执行文件的 runpath 指定的目录
  4. /etc/ld.so.conf 中的目录列表
  5. 默认系统库目录:/lib/usr/lib

rpath 与 runpath 的区别

设置 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 时:

  1. ldd <executable> 查看缺失依赖
  2. readelf -d <executable> | grep NEEDED 检查直接依赖
  3. LD_DEBUG=libs ldd <executable> 查看搜索过程
  4. 通过 LD_LIBRARY_PATH 或 rpath/runpath 添加搜索目录
  5. 如果 ldd 没有缺失项但仍报错,检查是否有 setuid 权限

为什么动态库里不能静态链接

.a 静态链接进 .so 的问题:

  1. 装载问题execve() 系统调用只负责装载动态库(.so),静态库 .a 不会被单独装载
  2. 符号冲突:Linux 动态链接器的符号调解策略是"先入为主",来自 .a 的符号可能与系统 libstdc++.so 冲突
  3. 全局变量重复libstdc++/libpthread 维护的全局状态会出现两份,导致运行时异常

正确做法:将 .a 解包成 .o 文件再链接进 .so,并用 visibility 隐藏符号。但即使如此,全局变量重复的问题仍然存在,不推荐这种做法。

若需静态链接标准库到可执行文件:使用 -static(全静态)或 -static-libstdc++(仅静态链接 libstdc++)。

链接器对比:ld.bfd / ld.gold / mold

链接器特点
ld.bfdGNU 默认链接器,兼容性最好
ld.gold速度更快,但不支持 --copy-dt-needed-entries
mold现代化设计,速度极快(构建 LLVM 快 2 倍)

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&TIntel
寄存器前缀%eaxeax
立即数前缀$11
操作数顺序movl $1, %eax (源→目的)mov eax, 1 (目的←源)
内存寻址immed32(base, index, scale)[base + index*scale + immed32]

Switch Case 中定义变量

C/C++ 中 switchcase 标签后不能直接定义变量,因为变量的初始化可能因条件不满足而被跳过。解决方案:

  1. {} 括起来创建作用域
  2. 将变量定义在 switch 之外

sockaddr 与 sockaddr_in