动态链接与共享库
# 09.动态链接与共享库
嵌入式 Qt 应用依赖几十个 .so 文件——理解 ELF 结构、符号解析、LD_PRELOAD 和 RPATH 陷阱,才能掌控应用的启动速度和部署体积。
# 目录介绍
# 9.1 案例引入
# 9.1.1 更改 Qt 版本后应用启动慢了 3 秒
某车载仪表盘项目从 Qt 5.15 升级到 Qt 6.5,冷启动从 2.1 秒变成 5.3 秒。定位发现 LD_BIND_NOW=1 被写入了 systemd service 文件——强制在启动时立即解析所有符号,而不是延迟绑定。
LD_BIND_NOW 让启动时遍历 150+ 个 .so 的所有 PLT 条目,每次解析至少做 1 次 stat + 1 次 mmap——这是 O(N_so × N_symbol) 的代价。改回缺省的 lazy binding 后启动恢复。
量化数据(Qt 6.5 HMI 应用,ARM Cortex-A55):
| 指标 | Lazy binding(默认) | LD_BIND_NOW=1 |
|---|---|---|
| 冷启动时间 | 2.1 秒 | 5.3 秒 |
| 启动时解析的符号数 | 0(全部推迟) | ~85,000 |
启动时 stat 次数 | ~150(库查找) | ~85,150 |
| 运行首调延迟 | +5-20μs/符号 | 0 |
| 稳态性能 | 相同 | 相同 |
LD_BIND_NOW 的唯一合理场景——安全敏感的 setuid 程序(防止通过 LD_PRELOAD 劫持延迟绑定的符号)、以及需要确定性启动时间的实时系统。
# 9.1.2 核心问题
- PLT/GOT 的"延迟绑定"到底怎么省时间的?
- 为什么嵌入式设备应该避免
LD_LIBRARY_PATH? - RPATH 和 RUNPATH 有什么区别?在哪种场景下必须用 RUNPATH?
- 静态链接 Qt 对比动态链接的镜像体积差异有多大?
# 9.2 ELF 文件结构
# 9.2.1 ELF Header / Program Header / Section Header
ELF(Executable and Linkable Format)是 Linux 可执行文件和 .so 的统一格式。三层结构:
┌──────────────────┐
│ ELF Header │ ← 魔数 (\x7fELF)、架构 (x86_64/ARM)、入口地址
├──────────────────┤
│ Program Header │ ← 段表(运行时视图):告诉内核如何 mmap
│ Table │
├──────────────────┤
│ Segment 1 .text │
│ Segment 2 .data │
│ ... │
├──────────────────┤
│ Section Header │ ← 节表(链接时视图):告诉链接器段在哪
│ Table │
└──────────────────┘
ELF Header 的关键字段(readelf -h):
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64 # 32/64 位
Type: DYN (Shared object file) # EXEC/REL/DYN
Machine: AArch64 # x86_64/ARM
Entry point address: 0x... # 程序入口
Program Header 是给内核看的——每个 PT_LOAD 段对应一次 mmap:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x0000000000400000 0x0000000000400000 0x1234 0x1234 R E 0x1000 ← 代码段
LOAD 0x002000 0x0000000000401000 0x0000000000401000 0x0100 0x0200 RW 0x1000 ← 数据段(MemSiz > FileSiz 因为 .bss)
Section Header 是给链接器和调试器看的——.text、.data、.symtab 等。
# 9.2.2 .text/.data/.bss/.rodata 段的作用
| 段 | 用途 | 权限 | 是否在文件中 | 典型内容 |
|---|---|---|---|---|
.text | 机器指令 | r-x | ✅ | 函数的汇编代码 |
.rodata | 只读数据 | r-- | ✅ | 字符串常量、const 全局变量 |
.data | 已初始化全局变量 | rw- | ✅(有初值) | int x = 42; |
.bss | 未初始化全局变量 | rw- | ❌(文件大小=0,加载时清零) | static char buf[8192]; |
.plt | 过程链接表 | r-x | ✅ | 动态链接跳板 |
.got | 全局偏移表 | rw- | 部分 | 已解析的外部符号地址 |
.bss 为什么不在文件中占空间? 因为它的初始值全是 0——不需要在文件中实际存储。MemSiz > FileSiz 的差值就是 .bss 的大小。这节省了可执行文件的大小。
查看段大小:
size myapp # 快速查看 .text/.data/.bss
# 输出: text data bss dec hex filename
# 45678 1234 2048 48960 bf40 myapp
# 9.2.3 .dynamic / .dynsym / .plt / .got 动态链接相关段
动态链接的四大核心段:
| 段 | 用途 | 内容 |
|---|---|---|
.dynamic | 动态链接信息表 | 依赖的 .so 列表 (NEEDED)、符号表地址、PLT/GOT 地址 |
.dynsym | 动态符号表 | 所有需要动态解析的符号(函数名+版本) |
.plt | 过程链接表 (Procedure Linkage Table) | 跳板代码,首次调用时触发动态解析 |
.got.plt | 全局偏移表 (Global Offset Table) | 解析后的外部函数实际地址 |
PLT/GOT 的协作流程:
调用 printf("hello")
│
▼
.text: call printf@plt ← 代码段调用 PLT 条目
│
▼
.plt: jmp *printf@GOT ← 跳到 GOT 中存的地址
│
├─ 首次调用:GOT 里是 PLT 自己 → 触发 _dl_runtime_resolve
│ → ld.so 查找 libc.so.6 中的 printf 地址
│ → 更新 GOT 表项为真实地址
│ → 跳转到 printf
│
└─ 后续调用:GOT 里是 printf 真实地址 → 直接跳转(1 次间接跳转)
首次调用 vs 后续调用的开销对比:
| 阶段 | 操作 | 耗时 |
|---|---|---|
| 首次调用 | 触发 _dl_runtime_resolve → 查 .dynsym → stat .so → mmap → 更新 GOT | ~500 μs |
| 后续调用 | 1 次 jmp *GOT[n](间接跳转) | ~2 ns |
# 9.2.4 readelf/objdump 工具实战
查看依赖的 .so:
readelf -d myapp | grep NEEDED
# NEEDED libQt6Core.so.6
# NEEDED libQt6Quick.so.6
# NEEDED libc.so.6
# 更简洁:
objdump -p myapp | grep NEEDED
# 递归查看所有依赖:
ldd myapp
查看导出符号:
# 动态库导出了哪些符号
readelf -s --dyn-syms libmylib.so | grep FUNC
# nm 也可以
nm -D libmylib.so | grep ' T ' # 代码段中的符号(导出的函数)
查看 PLT 条目数:
objdump -d -j .plt myapp | grep '@plt>:' | wc -l
# 这就是启动时 BLIND_NOW 要解析的符号数量
查看 RPATH/RUNPATH:
readelf -d myapp | grep -E 'RPATH|RUNPATH'
# 9.3 动态链接过程
# 9.3.1 ld.so 的加载流程
ld.so(dynamic linker)是内核在加载动态链接的可执行文件时自动调用的"预加载器":
1. 内核 execve("myapp") → 读 ELF Header → 发现 .interp 段指向 /lib/ld-linux-armhf.so.3
2. 内核先 mmap ld.so 到进程地址空间
3. 内核把控制权交给 ld.so(而不是 myapp 的入口点)
4. ld.so 自举(重定位自己)
5. ld.so 读取 myapp 的 .dynamic 段 → 列出所有 NEEDED 的 .so
6. ld.so 逐个 mmap 每个 .so
7. ld.so 执行符号重定位(Symbol Resolution)
8. ld.so 把控制权交给 myapp 的实际入口点
.interp 段——指定动态链接器的路径:
readelf -p .interp myapp
# [ 0] /lib/ld-linux-armhf.so.3 (ARM 32位)
# 或 /lib/ld-linux-aarch64.so.1 (ARM 64位)
# 或 /lib64/ld-linux-x86-64.so.2 (x86_64)
为什么 ld.so 本身不能依赖任何 .so? 它是静态链接的——在加载任何 .so 之前就必须能运行。用 ldd /lib/ld-linux.so 验证——not a dynamic executable。
# 9.3.2 符号解析 (Symbol Resolution)
符号解析 = 把函数名和地址对应起来。ld.so 的处理顺序:
1. 遍历所有 NEEDED 的 .so
2. 每个 .so 的 .dynsym 段包含导出的符号
3. 按"广度优先 + 默认符号可见域"规则绑定
├─ 先找到的符号优先(即使后加载的 .so 也有同名符号)
└─ 除非后加载的 .so 编译时用了 -fvisibility=hidden
符号冲突的场景——菱形依赖中的优先规则:
myapp → libA.so → libCommon.so (v1)
→ libB.so → libCommon.so (v2)
↑ 两个 libCommon.so!哪个被加载?
ld.so 规则:先遇到哪个就加载哪个。如果 libA 先被遍历,
myapp 和 libB 都会用 libCommon v1 的符号——可能导致 ABI 不兼容崩溃。
符号版本控制(-fvisibility=hidden)——解决菱形依赖的神器:
// libA 的构建(CMake)
set(CMAKE_C_VISIBILITY_PRESET hidden) // 默认隐藏所有符号
set(CMAKE_VISIBILITY_INLINES_HIDDEN ON)
// 只导出需要的符号
__attribute__((visibility("default")))
void liba_public_api() { ... }
// 内部符号标记为 hidden——不会污染全局符号空间
void liba_internal() { ... }
# 9.3.3 延迟绑定与 PLT/GOT 机制
没有延迟绑定(LD_BIND_NOW=1):
启动时 ld.so 遍历所有 .dynsym 条目 → 解析每个符号 → 填入 GOT
优点:运行期无额外开销
缺点:启动时间 = O(依赖库数 × 平均符号数)
有延迟绑定(默认):
启动时 GOT 表项全部填 PLT 地址
首次调用 printf → PLT 跳板 → _dl_runtime_resolve → 解析 → 更新 GOT
后续调用 printf → GOT 直接跳转(1 条间接 jmp)
PLT 条目的汇编代码(ARM 64 位简化版):
printf@plt:
adrp x16, _GLOBAL_OFFSET_TABLE_ ; 取 GOT 基址
ldr x17, [x16, #printf@GOT] ; 从 GOT 读 printf 地址
br x17 ; 跳到目标(首次跳到 PLT 自身)
# 首次时 GOT 里是下一条指令的地址:
stp x29, x30, [sp, #-16]! ; 保存栈帧
adrp x0, printf_index ; 符号在 .dynsym 的索引
b _dl_runtime_resolve ; 调用 ld.so 解析
省了多少? 85,000 个符号 × 500 μs 首次解析 = 42.5 秒(理论值)。实际因为批量 mmap 和缓存,降到 ~3 秒——但仍然显著。
# 9.3.4 LD_BIND_NOW 对启动速度的影响
LD_BIND_NOW 的实际成本分解:
73% 符号查找(.dynsym 哈希表查询 + 字符串比较)
15% stat/mmap .so 文件(可能已被缓存)
8% 重定位写入(更新 GOT 表项)
4% 其他开销
何时使用 LD_BIND_NOW?
| 场景 | 是否启用 |
|---|---|
| setuid 程序(安全敏感) | ✅ 必须(防止 GOT 劫持) |
| 实时系统(确定性延迟优先) | ✅ 推荐 |
| 嵌入式系统(启动时间优先) | ❌ 不启用 |
| 桌面/服务器 | ❌ 不启用(默认 lazy 最优) |
| 调试/测试 | ✅ 可开(提前暴露缺失符号) |
# 9.4 动态库搜索路径陷阱
# 9.4.1 RPATH vs RUNPATH 的区别
ld.so 搜索 .so 的优先级顺序:
1. DT_RPATH (ELF 中嵌入的硬编码路径) —— 仅当无 DT_RUNPATH 时
2. LD_LIBRARY_PATH 环境变量
3. DT_RUNPATH (ELF 中嵌入的路径) —— 优先级高于系统默认
4. /etc/ld.so.cache (ldconfig 缓存)
5. /lib, /usr/lib (系统默认路径)
RPATH 和 RUNPATH 的关键差异:
| 特性 | DT_RPATH | DT_RUNPATH |
|---|---|---|
| LD_LIBRARY_PATH 能否覆盖 | ❌ 不能(RPATH 优先级最高) | ✅ 能(LD_LIBRARY_PATH 先于 RUNPATH) |
| 影响子 .so 的搜索 | ✅ 递归生效 | ❌ 只影响直接依赖 |
| 推荐使用 | ❌ 过时 | ✅ 现代首选 |
| 设置方式 | -Wl,-rpath,/path | -Wl,-rpath,/path,--enable-new-dtags |
嵌入式必须用 RUNPATH 的场景——当你分发一个自包含的 AppImage(/opt/myapp/lib 中有自己的 .so),但又允许用户 LD_LIBRARY_PATH 插入调试版本的 .so 来排查问题。
检查当前用的是 RPATH 还是 RUNPATH:
readelf -d myapp | grep -E 'RPATH|RUNPATH'
# 如果有 RUNPATH: Library runpath: [/opt/myapp/lib] → 现代
# 如果只有 RPATH: Library rpath: [/opt/myapp/lib] → 旧式
# 9.4.2 LD_LIBRARY_PATH 的使用与滥用
LD_LIBRARY_PATH 是紧急修复工具——不是长期部署方案。
# 临时用(调试/紧急修复)
LD_LIBRARY_PATH=/opt/new_qt/lib ./myapp
# 滥用:
export LD_LIBRARY_PATH=/opt/new_qt/lib:$LD_LIBRARY_PATH # ← 写在 ~/.bashrc 里
# ❌ 影响所有后续启动的程序——可能引入 ABI 不兼容的 .so
LD_LIBRARY_PATH 的危害:
| 问题 | 描述 |
|---|---|
| 全局副作用 | 环境变量对所有子进程生效 |
| ABI 混淆 | 新 .so 的符号签名可能与预期不同 |
| 安全风险 | setuid 程序忽略 LD_LIBRARY_PATH,但普通程序可能被劫持 |
| 排查困难 | 一台机器正常工作,另一台因 LD_LIBRARY_PATH 不同而崩溃 |
嵌入式最佳实践——用 RUNPATH 替代 LD_LIBRARY_PATH。在交叉编译时嵌入正确的路径:
# CMake 中设置 RUNPATH
set(CMAKE_INSTALL_RPATH "$ORIGIN/../lib")
set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
# $ORIGIN = 可执行文件所在目录
# $ORIGIN/../lib = 相对于 myapp 的 lib 目录
$ORIGIN 的价值——让整个 <app_bundle>/bin/myapp + <app_bundle>/lib/*.so 可以整体移动到任意路径,无须重新编译。
# 9.4.3 /etc/ld.so.conf 与 ldconfig
系统级 .so 搜索路径配置:
cat /etc/ld.so.conf
# include /etc/ld.so.conf.d/*.conf
# 在 /etc/ld.so.conf.d/myapp.conf 中加自定义路径:
echo "/opt/myapp/lib" > /etc/ld.so.conf.d/myapp.conf
# 更新 ld.so 缓存
ldconfig
ldconfig 做什么?
- 扫描
ld.so.conf中列出的所有路径 - 找到每个 .so 文件 → 读
SONAME→ 建立SONAME → 完整路径的映射表 - 写入
/etc/ld.so.cache(二进制格式) - 运行时
ld.so直接查缓存(mmap),无需遍历目录
# 9.4.4 嵌入式设备上的库路径管理最佳实践
三层推荐的嵌入式库布局:
/usr/bin/myapp # 可执行文件
/usr/lib/myapp/ # 应用专属 .so(不被其他程序依赖)
├── libmyplugin.so
└── libmycore.so
/usr/lib/ # 系统级 .so(Qt、其他)
├── libQt6Core.so.6
├── libQt6Quick.so.6
└── libcrypto.so.3
每个目录的配置方式:
| 目录 | 搜索方式 | 原因 |
|---|---|---|
/usr/lib/ | 系统默认 | 所有程序共享,由 ldconfig 管理 |
/usr/lib/myapp/ | RUNPATH=$ORIGIN/../lib/myapp | 应用私有,不污染全局 |
/opt/vendor/sdk/lib/ | /etc/ld.so.conf.d/vendor.conf + ldconfig | 第三方 SDK |
嵌入式文件系统优化——用硬链接代替多份 .so 拷贝:
# Qt .so 只用一份物理拷贝,其余位置用硬链接
ln /usr/lib/libQt6Core.so.6 /opt/myapp/lib/libQt6Core.so.6
# du -sh → 只占一份磁盘空间
# 9.5 动态加载 dlopen
# 9.5.1 dlopen/dlsym/dlclose 的使用
运行时动态加载库——"插件模式"的基础:
#include <dlfcn.h>
// 加载库
void *handle = dlopen("libmyplugin.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen: %s\n", dlerror());
exit(1);
}
// 查找符号
typedef void (*init_fn)(void *);
init_fn plugin_init = (init_fn)dlsym(handle, "plugin_init");
if (!plugin_init) {
fprintf(stderr, "dlsym: %s\n", dlerror());
dlclose(handle);
exit(1);
}
// 调用
plugin_init(context);
// 卸载
dlclose(handle);
dlsym 的特殊技巧——取全局变量地址:
int *counter = (int *)dlsym(handle, "global_counter");
if (counter) *counter = 42; // 修改 .so 中的全局变量
# 9.5.2 RTLD_LAZY vs RTLD_NOW
| 标志 | 解析时机 | 适用场景 |
|---|---|---|
RTLD_LAZY | 首次调用时解析 | 大部分插件(加载快) |
RTLD_NOW | dlopen 返回前全部解析 | 需提前暴露符号缺失错误 |
RTLD_NOW \| RTLD_GLOBAL | 全部解析 + 符号全局可见 | 插件需要注册符号给后续插件用 |
RTLD_GLOBAL 的典型场景——Qt 插件系统:
// 图像格式插件被 Qt 主程序加载时:
dlopen("libqjpeg.so", RTLD_NOW | RTLD_GLOBAL);
// 插件初始化函数中调用 qRegisterMetaType——此符号必须在全局空间可见
# 9.5.3 dlerror 错误处理
dlerror 返回最近一次 dl 系列 API 的错误描述。注意:每次调用 dlerror 会清空错误信息:
// ❌ 错误:dlerror 被调了两次——第二次返回 NULL
dlopen("nonexistent.so", RTLD_LAZY);
printf("error: %s\n", dlerror()); // 第一行正确
printf("error: %s\n", dlerror()); // 第二行打印 "(null)"
// ✅ 正确:保存到变量
char *err = dlerror();
if (err) printf("error: %s\n", err);
# 9.5.4 插件架构在嵌入式中的应用
Qt 插件系统就是你熟悉的 dlopen 模式。自定义插件架构:
// 插件接口(头文件,主程序和插件共编)
typedef struct {
const char *name;
int (*init)(void *ctx);
int (*process)(void *ctx, void *data);
void (*cleanup)(void *ctx);
} plugin_ops_t;
// 每个插件 .so 导出唯一符号
plugin_ops_t plugin = {
.name = "modbus",
.init = modbus_init,
.process = modbus_process,
.cleanup = modbus_cleanup,
};
// 主程序加载插件
void *handle = dlopen(path, RTLD_NOW);
plugin_ops_t *ops = (plugin_ops_t *)dlsym(handle, "plugin");
ops->init(ctx);
嵌入式插件系统的内存优化——按需加载:不要在启动时加载所有插件——按场景动态加载。网关设备不上 Modbus 时,不加载 libmodbus_plugin.so。
# 9.6 静态链接与动态链接的抉择
# 9.6.1 体积 vs 内存 vs 安全性对比
| 维度 | 动态链接 | 静态链接 |
|---|---|---|
| 磁盘体积(单应用) | ~500 KB + .so | ~40+ MB |
| 磁盘体积(多应用) | 共享 .so → 总计小 | 每个应用一份 → 总计大 |
| 内存(RSS) | 共享 .so → 多应用共享同一物理页 | 每应用独立映射 |
| 启动时间 | 需要加载 ld.so + 符号解析 | 零链接开销,直接运行 |
| ABI 兼容 | 依赖系统 .so 版本 | 自包含,无版本冲突 |
| 安全补丁 | 更新 .so = 所有应用立即受益 | 每应用重编译 |
| 嵌入式部署 | 需确保目标系统 .so 版本匹配 | 一层镜像包含一切 |
快速估算:
# 动态链接 Qt 应用
ls -lh myapp # ~500 KB
du -sh /usr/lib/libQt* # ~60 MB(所有 Qt .so 之和)
# 静态链接 Qt 应用
ls -lh myapp_static # ~45 MB
# 但!无需系统装 Qt .so —— 部署到嵌入式设备只有一个文件
# 9.6.2 Qt 静态编译实践
Qt 静态编译的步骤:
# 1. 配置 Qt 源码为静态构建
./configure -static -release -prefix /opt/qt-static \
-skip qtwebengine -skip qt3d -no-opengl
# 2. 编译(数小时)
make -j$(nproc) && make install
# 3. 用静态 Qt 编译应用
/opt/qt-static/bin/qmake myapp.pro
make
# 4. 应用静态插件(图片格式、平台插件)
# main.cpp 中导入静态插件:
Q_IMPORT_PLUGIN(QJpegPlugin)
Q_IMPORT_PLUGIN(QMinimalIntegrationPlugin)
静态链接的体积分析(Qt 6.5 + QML + 基本控件):
动态版:myapp = 0.5 MB + libQt6Core.so.6 (5 MB) + libQt6Quick.so.6 (8 MB) +
libQt6Qml.so.6 (3 MB) + 其他 Qt .so (~20 MB) = ~36 MB
→ 部署需要所有 .so
静态版:myapp = ~42 MB(所有 Qt 代码 + QML 引擎 + 基本控件)
→ 部署只需要一个文件
嵌入式推荐——动态但自包含:编译 Qt 为共享库,用 linuxdeployqt 打包所有需要的 .so 到一个 AppDir,用户只需拷贝整个目录。
# 9.6.3 嵌入式启动速度优化 (prelink/fast-load)
prelink——预链接,提前完成符号重定位,把 GOT 表中填好地址,写入 ELF 文件:
# 对目标设备上所有 .so 执行预链接
prelink --all --conserve-memory --verbose
# 效果:
# - 启动时 ld.so 不再需要做符号解析(GOT 已填好)
# - 但 .so 文件变大(因为写入了 GOT 数据)
# - 如果 .so 被更新或地址布局变化,需要重新 prelink
更现代的方案——-Wl,-z,now + AppImage 自包含:
# 放弃延迟链接,启用 BIND_NOW + 适当减少 .so 数量
gcc -Wl,-z,now -o myapp myapp.c
# 代价:启动时一次性解析所有符号
# 收益:如果 .so 数量少(< 20),启动时间反而更短(免去每次调用的 PLT 跳转)
嵌入式启动速度排障顺序:
1. ldd myapp | wc -l # 依赖数量——过多是根本原因
2. readelf -d myapp | grep NEEDED # 是否有不该依赖的库
3. LD_DEBUG=statistics ./myapp # 统计 ld.so 耗时
4. strace -e openat ./myapp 2>&1 | wc -l # 看库搜索访问了多少文件
# 9.7 关键结论与速查表
核心结论五条:
- PLT/GOT 是动态链接的性能魔法——延迟绑定使启动时间与符号数解耦,首次调用多花 ~500μs,后续调用几乎零开销
- 用 RUNPATH +
$ORIGIN替代LD_LIBRARY_PATH——自包含应用可整体移动,不受环境变量污染 - 嵌入式默认用 lazy binding——
LD_BIND_NOW适合安全/实时场景,但代价是 O(N_so × N_symbol) 的启动时间 -fvisibility=hidden+dlopen= 可控的符号空间——避免菱形依赖中的符号冲突,是插件架构的基石- 动态链接 vs 静态链接不是二选一——嵌入式优先级:自包含动态打包 > 纯静态 > 依赖系统 .so
ELF 分析工具速查表:
| 工具 | 常用选项 | 用途 |
|---|---|---|
readelf -h | 查看 ELF Header | 快速了解架构/类型 |
readelf -d | grep NEEDED/RPATH/RUNPATH | 查依赖和搜索路径 |
readelf -s --dyn-syms | 动态符号表 | 看导出/导入的符号 |
objdump -p | grep NEEDED | 同 readelf -d |
ldd | 递归显示依赖 | 查库缺失(not found) |
nm -D | 显示动态符号 | 快速查看符号列表 |
size | text/data/bss | 快速估算体积 |
strip | 去除调试符号 | 减小 .so 体积 |
编译选项速查表:
| 选项 | 效果 | 推荐场景 |
|---|---|---|
-fPIC | 位置无关代码 | .so 必备 |
-fvisibility=hidden | 默认隐藏符号 | 库开发(防止符号污染) |
-Wl,-rpath,'$ORIGIN/../lib' | 嵌入 RUNPATH | 自包含应用 |
-Wl,-z,now | BIND_NOW | 安全/实时 |
-Wl,--as-needed | 只链接实际使用的 .so | 减少不必要的 NEEDED |
-Wl,--strip-all | 去除所有符号 | 生产发布 |
LD_DEBUG 环境变量速查:
LD_DEBUG=libs ./myapp # 显示库搜索过程
LD_DEBUG=symbols ./myapp # 显示符号绑定过程
LD_DEBUG=statistics ./myapp # 显示链接统计信息
LD_DEBUG=help ./myapp # 查看所有可调试选项
下一步:在你的 Qt 应用上跑 LD_DEBUG=statistics ./myapp 2>&1,看有多少符号被解析、多少时间花在重定位上——然后对比 LD_BIND_NOW=1 和默认 lazy binding 的实际差异。
延伸阅读:
man 8 ld.so—— 动态链接器完整文档man 3 dlopen—— 动态加载 APIman 1 ldd—— 依赖查看工具man 5 elf—— ELF 格式详述- 《Linkers and Loaders》(John R. Levine)