静态库 vs 动态库:-fPIC、PLT/GOT、LD_PRELOAD、链接顺序

发布时间:2026/10/16 7:32:00

静态库 vs 动态库:-fPIC、PLT/GOT、LD_PRELOAD、链接顺序 同一份源码编成静态库.a和动态库.so链接方式完全不同——一个把代码复制进可执行文件、一个在运行时才加载体积和依赖各有取舍。这篇用nm/readelf/ldd/size的真实输出把这套「链接与加载」机制讲透包括动态库为什么必须-fPIC、函数调用怎么走 PLT/GOT 延迟绑定、LD_PRELOAD怎么劫持符号以及-l链接顺序为什么讲究。1. 引子静态 vs 动态size 一眼看穿先看两个要被编进库的函数。它们本身普通得很但「怎么打包进可执行文件」决定了最终的体积和依赖#includeiostream// 这两个函数之后会被分别编成静态库 .a 和动态库 .sointsquare(intx){returnx*x;}intcube(intx){returnx*x*x;}intmain(){std::coutsquare(6) square(6)\n;std::coutcube(3) cube(3)\n;}square(6) 36 cube(3) 27把同样的函数编成静态库和动态库再各自链接出可执行文件用size看体积godbolt gcc 13.2 实测$ size static_demo # 静态链接square/cube 被复制进可执行文件 text data bss dec hex filename 1268 624 8 1900 76c static_demo $ size dyn_demo # 动态链接可执行文件里只有一小段调用桩 text data bss dec hex filename 1356 656 8 2020 7e4 dyn_demo有意思的是square/cube小到各只有一条乘法指令静态链接的 text 段1268反而比动态链接1356还小。动态链接的可执行文件要额外装 PLT 桩、GOT、动态段这些「调用基础设施」。「动态一定更省体积」只在库很大比如整个标准库时才成立对几个小函数静态链接可能更省。这个实测也提醒我们体积结论要看库的大小别记死结论。维度静态库.a动态库.so代码位置链接时复制进可执行文件运行时由动态链接器加载可执行文件体积大库时大、小函数时可能更小大库时小、小函数时要装 PLT/GOT运行时依赖无自包含依赖.so存在且版本匹配库更新需要重新链接换.so即可ABI 兼容前提下多进程共享内存不共享代码段可被多个进程共享官方文档CMakeadd_library说明STATIC与SHARED两种库目标。2. 静态库链接时复制nm 看符号静态库本质是.o目标文件的打包ar归档。链接时链接器只把被引用的目标文件复制进可执行文件没被引用的不复制。$ nm -C libmystatic.a 0000000000000000 T square(int) 0000000000000000 T cube(int)square和cube都是全局符号T链接时按需复制。3. 动态库运行时加载ldd 看依赖动态链接的可执行文件启动时动态链接器ld.so按DT_NEEDED记录去找.so。ldd列出完整依赖链readelf -d直接看 ELF 动态段里的NEEDED$ ldd dyn_demo linux-vdso.so.1 (~~) libmydyn.so /app/build/libmydyn.so (~~) libstdc.so.6 /opt/compiler-explorer/gcc-13.2.0/lib64/libstdc.so.6 (~~) libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 (~~) libgcc_s.so.1 /opt/compiler-explorer/gcc-13.2.0/lib64/libgcc_s.so.1 (~~) libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 (~~) /lib64/ld-linux-x86-64.so.2 (~~) $ readelf -d libmydyn.so | grep NEEDED 0x0000000000000001 (NEEDED) Shared library: [libstdc.so.6] 0x0000000000000001 (NEEDED) Shared library: [libm.so.6] 0x0000000000000001 (NEEDED) Shared library: [libgcc_s.so.1] 0x0000000000000001 (NEEDED) Shared library: [libc.so.6]ldd里的libmydyn.so /app/build/libmydyn.so证明可执行文件里没有square/cube的代码只有一个「去找 libmydyn.so」的记录真正解析函数地址发生在程序启动时。官方文档GNUldd手册 与readelf手册 说明如何查看动态依赖。4. 为什么动态库必须 -fPIC动态库会被加载到任意内存地址每次加载基址都可能不同所以库里的代码不能假设固定地址——必须编译成「位置无关代码」Position-Independent CodePIC。-fPIC让代码通过 GOT全局偏移表间接访问全局变量和函数而不是硬编码地址。下面的calc.o里访问了全局变量global_counter不加-fPIC去编.so会直接报错真实报错$ g -shared -o libbad.so calc.o # calc.o 没有 -fPIC /usr/bin/ld: calc.o: relocation R_X86_64_PC32 against symbol global_counter can not be used when making a shared object; recompile with -fPIC collect2: error: ld returned 1 exit status官方文档GCC-fPIC选项 说明位置无关代码的生成方式。5. PLT/GOT函数调用为什么能「延迟绑定」动态链接的可执行文件调用外部函数时编译期不知道函数真实地址于是走「PLT GOT」两级跳调用 square() 的流程第一次调用后 main 里 call squareplt └─ PLT 条目jmp *GOT[square] GOT 里已存真实地址 └─ 真正的 square() 代码 第一次调用前GOT[square] 指向 PLT 的下一条指令 触发动态链接器去 .so 里解析真实地址写回 GOT —— 这叫「延迟绑定」lazy binding。好处不用的符号不解析启动更快解析一次后后续调用直接跳真实地址几乎零开销。6. LD_PRELOAD劫持符号LD_PRELOAD环境变量指定的.so会最先被加载其中定义的符号优先于其他库。于是可以在不重新编译程序的情况下「劫持」某个函数。先看一个正常返回1的函数这里为演示直接定义在main里真实场景它来自某个.so#includeiostream// 真实场景里 get_value 来自某个动态库这里定义在 main 里便于单文件运行intget_value(){return1;}intmain(){std::coutget_value() get_value()\n;}get_value() 1劫持库定义一个同名函数返回值不同// fake.cpp —— 编成 libfake.soLD_PRELOAD 加载后优先匹配externCintget_value(){return999;}在真实多文件工程里get_value来自libgreet.soLD_PRELOAD劫持效果如下godbolt 容器实测$ ./demo # 正常来自 libgreet.so 的版本 get_value() 1 $ LD_PRELOAD./libfake.so ./demo # 劫持优先匹配 libfake.so 的版本 get_value() 999LD_PRELOAD常用于调试、mock、注入监控代码但生产环境滥用会引入安全和稳定性风险只应在可控环境使用。7. 链接顺序-l 顺序为什么重要静态库链接是单遍扫描链接器从左到右处理-l库只有「当前未解析符号」才会去后面的库里找。所以被依赖的库要放在依赖它的库后面。liba调用了libb的函数正确顺序是-la -lb反过来报 undefined reference$ g main.o -lb -la # 错误处理 libb 时还没有需要它的符号 /usr/bin/ld: liba.a(a.o): undefined reference to b_func $ g main.o -la -lb # 正确 链接成功顺序结果-la -lb被依赖的 b 在后成功-lb -lab 在前undefined reference to b_func官方文档GNUld手册 中-l选项说明库的搜索与扫描顺序。8. 完整示例一个库函数 静态/动态链接对比把「库函数」这个核心概念落成一个能跑的完整程序然后用size/nm/ldd看静态链接与动态链接的差异。#includeiostream// 这就是「库」要提供的能力两个纯函数intsquare(intx){returnx*x;}intcube(intx){returnx*x*x;}intmain(){// 用确定的结果自检证明链接正确std::coutsquare(6)square(6)\n;std::coutcube(3)cube(3)\n;std::cout(square(6)36cube(3)27?自检通过\n:自检失败\n);}square(6)36 cube(3)27 自检通过在真实多文件工程里calc.cpp编成.a和.so各一份main.cpp分别链接体积、符号、依赖对比如下godbolt 实测$ size static_demo # 静态链接代码复制进可执行文件 text data bss dec hex filename 1268 624 8 1900 76c static_demo $ size dyn_demo # 动态链接可执行文件里只有调用桩 text data bss dec hex filename 1356 656 8 2020 7e4 dyn_demo $ nm -C libmystatic.a # 静态库符号全局 T链接时复制 0000000000000000 T square(int) 0000000000000000 T cube(int) $ ldd dyn_demo # 动态链接的可执行文件依赖 .so libmydyn.so /app/build/libmydyn.so (~~) libstdc.so.6 ~~ (~~) libc.so.6 ~~ (~~)9. 延伸阅读GNUldd手册 —— 查看动态依赖的标准工具。GCC-fPIC文档 —— 位置无关代码的生成与适用。GNUld手册 ——-l、--as-needed、静态库扫描顺序。CMakeadd_library——STATIC/SHARED/OBJECT库目标。cppreference语言链接 ——extern C让符号稳定正是LD_PRELOAD能按名劫持的符号基础。Compiler Explorergodbolt —— 自己写个跨库调用看call fooplt与 GOT 重定位的汇编。C Core Guidelines —— 本知识库代码示例遵循的工程基调。本知识库内的相关篇目《C ABI 与跨库兼容为什么换编译器要重编、符号可见性》 —— C ABI 与跨库兼容同体系里最该一起看的另一篇。《C 名字修饰name mangling与 extern “C”C/C 互操作》 —— 用 nm/nm -C 真实符号表讲清名字修饰《异常的实现与代价零成本异常、栈展开与 -fno-exceptions 什么时候用》 —— C 异常常被说「零成本」但它的代价藏在抛出路径上。10. 一句话总结静态库链接时按需复制代码自包含、体积大、无运行时依赖动态库运行时加载体积小、可共享、但依赖版本匹配动态库必须-fPIC以支持任意地址加载跨库函数调用走 PLT/GOT 延迟绑定LD_PRELOAD利用加载顺序劫持符号而静态链接的单遍扫描决定了-l顺序被依赖的库放后面。
延伸阅读

更多相关文章

2026/10/16 7:27:00

二关节机械臂计算力矩控制Simulink实现

简介:本资源是一份面向机器人控制初学者与高校自动化专业学生的Simulink实践教学材料,聚焦二关节机械臂的计算力矩控制原理与仿真实现。通过逆动力学建模、力矩控制器设计及正弦/轨迹跟踪两类典型控制策略对比,帮助学习者深入理解机器人动力学…

2026/10/16 8:37:14

运放电阻电路核心知识:虚短虚断、负反馈与五大基本电路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 8:37:14

Claude高效提问的五个基础原则:告别无效对话

“帮我写份策划案”“帮我看看这段代码”“给我提点修改意见”——这些句式你是不是特别眼熟?几乎每次想把活儿交给Claude的时候,脱口而出的就是这种一句话需求。结果也很统一:答非所问、泛泛而谈、抓不住重点,最后还得自己动手从…

2026/10/16 8:37:14

cuSPARSE状态码深度解析:从cusparseStatus_t到高效错误处理机制

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 8:37:14

Vue3+TypeScript全栈实战:从组件类型到前后端共享模型的完整指南

大概半年前,我接手了一个已经迭代三年的后台管理系统。这个系统由两代前端轮番维护,代码里到处是any传给组件的痕迹,接口返回的字段全靠运行时去猜。最讽刺的一次,后端把一个列表字段从数组改成了对象,前端在开发环境里…

2026/10/16 8:32:13

SAP接口实现差旅费报销自动生成财务凭证的完整方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/16 0:01:11

Cephadm实战指南:Linux系统工程师的生产级Ceph集群部署与排障

简介:本资源是一份完整的Ceph分布式存储系统中文技术文档合集,面向Linux系统运维工程师、云平台架构师及存储方向开发者,帮助读者系统掌握Ceph集群的部署、配置与运维核心能力。文档覆盖对象存储(RGW)、块设备&#xf…

2026/10/16 0:01:11

ITK中SpatialObject几何体系解析:坐标系、变换与管道集成

1. 空间对象的定位:为什么 ITK 需要一套几何描述体系很多刚开始接触 ITK 的朋友,包括我自己,最初都是被它的图像滤波、配准、分割这些“正统”功能吸引进来的。等真正上手做点实际项目,比如把分割出来的器官边界做量化分析、给血管…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑