【Linux】库制作与原理:从静态库、动态库到 ELF 加载全过程(附实操与图解)

发布时间:2026/11/2 15:27:28

【Linux】库制作与原理:从静态库、动态库到 ELF 加载全过程(附实操与图解) 本文环境x86-64 LinuxGCC GNU Binutilsar / readelf / objdump。文中所有命令输出均为真实运行结果示例代码放在文末可直接复现。本文重点动静态库的制作动静态库的使用动态库的查找可执行程序的 ELF 格式可执行程序的加载过程虚拟地址空间与动态库加载的过程1. 什么是库库是写好的、成熟的、可以复用的代码。现实开发中每个程序都会依赖大量底层库——不可能每个人都从零开始写printf、写网络协议栈所以库的存在意义非常大。本质上库是一种可执行代码的二进制形式可以被操作系统载入内存执行。它由一个或多个目标文件.o组成使用者只需要拿到头文件 库文件就能使用源码可以不公开。类型LinuxWindows静态库.a.lib动态库共享库.so.dll库在 C 程序构建流程中的位置库文件的命名规则lib 库名 后缀例如libmymath.a、libmymath.so库名是mymath链接时写-lmymath。1.1 静态库与动态库的区别静态链接 vs 动态链接对比项静态库.a动态库.so链接时机编译链接阶段代码被拷贝进可执行文件程序运行时由动态链接器加载可执行文件体积大小运行时依赖无必须能找到对应.so内存占用每个进程一份副本多个进程共享一份代码库升级需要重新链接所有程序替换.so即可ABI 不变时2. 示例代码后续所有实验都基于下面这个最小的数学库目录结构demo/ ├── main.c └── mylib/ ├── include/ │ └── mymath.h └── src/ ├── add.c └── sub.cmylib/include/mymath.h#ifndefMYMATH_H#defineMYMATH_Hintadd(inta,intb);intsub(inta,intb);#endifmylib/src/add.c#includemymath.hintadd(inta,intb){returnab;}mylib/src/sub.c#includemymath.hintsub(inta,intb){returna-b;}main.c#includestdio.h#includemymath.hintmain(void){printf(10 3 %d\n,add(10,3));printf(10 - 3 %d\n,sub(10,3));return0;}3. 静态库的制作与使用静态库的制作与使用3.1 制作# ① 把库源码编译成目标文件gcc-cmylib/src/add.c mylib/src/sub.c -Imylib/include# ② 用 ar 把目标文件归档成静态库ar-rcslibmymath.a add.o sub.o# 查看静态库里有哪些目标文件ar-tlibmymath.aar是 GNU 归档工具常用参数rreplace把文件插入归档已存在则替换ccreate归档不存在时自动创建不给出提示s写入符号索引加快链接时的符号查找t列出归档中的文件v显示详细信息可以看出静态库本质就是一堆.o文件打成的包。3.2 使用gcc main.c-oapp_static -Imylib/include -L.-lmymath三个关键选项选项含义示例-I头文件搜索路径-Imylib/include-L库文件搜索路径-L.-l要链接的库名去掉lib前缀和后缀-lmymath如果库和头文件已经安装到系统目录如/usr/include、/usr/lib则-I和-L可以省略但-l永远不能省libc 除外gcc 默认链接。运行结果制作并使用静态库注意当同一目录下同时存在libmymath.a和libmymath.so时gcc默认优先使用动态库。想强制静态链接可以加-static此时连 libc 也会被静态链接进来。4. 动态库的制作与使用动态库的制作4.1 制作# ① 生成位置无关码Position Independent Codegcc-fPIC-cmylib/src/add.c mylib/src/sub.c -Imylib/include# ② 生成共享库gcc-shared-olibmymath.so add.o sub.o-fPIC生成与位置无关的代码库被加载到进程地址空间的任意位置都能正确运行原理见第 8 节。-shared告诉 gcc 输出共享库而不是可执行程序。4.2 使用编译通过运行却失败gcc main.c-oapp_dyn -Imylib/include -L.-lmymath./app_dyn动态库编译成功却运行失败很多初学者会在这里卡住明明-L.告诉了 gcc 库在哪为什么运行还找不到原因是-L只是告诉编译期的链接器ld去哪找库它只在可执行文件里记下一句「我需要libmymath.so」DT_NEEDED并不记录库的位置。程序运行时负责加载库的是另一个程序——动态链接器ld-linux-x86-64.so.2它有自己的一套查找规则。ldd命令可以查看程序依赖哪些动态库、以及这些库是否找得到libmymath.so not found一目了然。5. 动态库的查找5.1 查找顺序动态链接器 ld.so 查找.so 的顺序动态链接器按以下顺序查找详见man ld.soELF 中的DT_RPATH仅在没有DT_RUNPATH时生效已不推荐环境变量LD_LIBRARY_PATHELF 中的DT_RUNPATH缓存文件/etc/ld.so.cache由ldconfig根据/etc/ld.so.conf生成系统默认目录/lib、/usr/lib64 位系统还有/lib64、/usr/lib64等5.2 让程序找到动态库的 4 种方法方法一拷贝到系统默认目录需要 root会污染系统不推荐用于自己的测试库sudocplibmymath.so /usr/lib/方法二在系统默认目录建立软链接sudoln-s$(pwd)/libmymath.so /usr/lib/libmymath.so方法三设置环境变量LD_LIBRARY_PATH只对当前 shell 生效适合调试exportLD_LIBRARY_PATH$LD_LIBRARY_PATH:$(pwd)方法四配置/etc/ld.so.conf.d/再执行ldconfig永久生效echo$(pwd)|sudotee/etc/ld.so.conf.d/mymath.confsudoldconfig补充用 RUNPATH 把搜索路径写进程序本身随程序发布时最推荐gcc main.c-oapp_rpath -Imylib/include -L.-lmymath-Wl,-rpath,$ORIGIN$ORIGIN表示「可执行文件所在目录」这样把程序和.so放在一起拷到任何地方都能运行。两种方式让程序找到动态库5.3 静态链接 vs 动态链接体积对比静态链接 vs 动态链接体积对比全静态链接的程序把 libc 里用到的代码也拷贝了进来体积是动态链接版本的约 49 倍。这也是为什么系统里绝大多数程序都选择动态链接。6. 可执行程序的 ELF 格式在 Linux 中目标文件.o、可执行程序、动态库.so、内核转储core dump用的都是同一种格式——ELFExecutable and Linkable Format。$fileapp_dyn libmymath.so add.o app_dyn: ELF64-bit LSB pie executable, x86-64,..., dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,... libmymath.so: ELF64-bit LSB shared object, x86-64,..., dynamically linked,... add.o: ELF64-bit LSB relocatable, x86-64,...ELF 类型说明示例可重定位文件REL尚未链接地址未确定add.o可执行文件EXEC / DYN-PIE可直接运行app_dyn共享目标文件DYN动态库libmymath.so核心转储CORE进程崩溃时的内存快照core6.1 ELF 的组成一个 ELF 文件由四部分组成ELF Header文件的「身份证」描述文件类型、目标机器、入口地址以及两张表的位置。Program Header Table程序头表描述段Segment告诉加载器如何把文件映射进内存。可执行文件和动态库必须有。Sections节真正的代码和数据如.text、.data、.bss、.rodata、.symtab等。Section Header Table节头表描述每个节的名字、位置、大小链接器和调试器使用。常见的节节内容.text机器指令代码.rodata只读数据如字符串常量.data已初始化的全局 / 静态变量.bss未初始化的全局 / 静态变量文件中不占空间只记录大小.symtab/.strtab符号表 / 字符串表.dynsym/.dynstr动态链接用的符号表.rela.*重定位信息.plt/.got动态库函数调用的跳转表见第 8.3 节.dynamic动态链接信息依赖哪些库、RUNPATH 等.interp动态链接器的路径6.2 ELF Headerreadelf -h app_dynMagic前 4 个字节7f 45 4c 46即\x7f E L F内核靠它识别 ELF 文件。TypeDYN (Position-Independent Executable)现代 gcc 默认生成 PIE 程序。Entry point程序入口地址0x10a0对应的是_start而不是main。6.3 两种视图Section 与 SegmentELF 文件的两种视图ELF 被设计成同时服务两类「读者」链接视图Section给链接器看。粒度细便于符号解析、重定位。执行视图Segment给加载器内核 动态链接器看。把权限相同的多个节合并成一个段一次性mmap进内存。为什么要合并因为内存按页通常 4KB管理、按页设置权限。如果每个节单独映射一个只有几十字节的节也要独占一页造成大量浪费。合并后减少页内碎片减少mmap次数统一设置页权限代码段R E、只读数据段R、数据段RW用readelf -l可以看到段与节的对应关系readelf -l app_dyn可以清楚看到第 03 号段R E可读可执行包含了.init .plt .text .fini这些代码节第 05 号段RW包含了.data .bss .got等可写节。7. 可执行程序的加载过程从敲下回车到 main() 的过程当在 shell 中敲下./app_dyn时大致发生了这些事bashfork()execve()创建子进程并用新程序替换它。内核解析 ELF校验 Magic读取程序头表。内核映射PT_LOAD段根据每个 LOAD 段的偏移、虚拟地址、权限建立映射。注意这里只是建立虚拟地址到文件的映射真正的物理页要到第一次访问时触发缺页中断才会加载按需分页。加载动态链接器内核发现有PT_INTERP段/lib64/ld-linux-x86-64.so.2于是把它也映射进来并把控制权交给动态链接器而不是程序自己的_start。动态链接器工作读取.dynamic中的DT_NEEDED按第 5 节的顺序找到libmymath.so、libc.so.6并映射进内存然后完成重定位。跳转到程序入口_start调用__libc_start_main执行初始化.init、全局构造函数准备好argc / argv / envp。进入main()。静态链接的程序没有PT_INTERP段第 4、5 步直接跳过内核映射完就跳到_start。7.1 编译后的程序在加载前就有「地址」吗有。看readelf -l的输出每个段都有VirtAddrobjdump -d反汇编时每条指令前面也有地址例如119b: call 1070 addplt。这说明编译器在生成程序时就已经按「虚拟地址空间」的布局给代码和数据编好了地址PIE 程序是从 0 开始的相对地址加载时再加上一个随机基址。这就是平坦模式flat model下的逻辑地址 虚拟地址。所以「虚拟地址空间」并不只是操作系统的概念编译器、链接器、加载器、操作系统都在遵守同一套约定编译器按虚拟地址给指令和数据编址加载器按 Program Header 把段放到对应虚拟地址操作系统通过页表把虚拟地址映射到物理内存CPU执行时只认虚拟地址由 MMU 完成转换8. 虚拟地址空间与动态库加载8.1 动态库被放在哪里写一个小程序在运行时打印自己的/proc/pid/maps可以看到真实的内存布局整理如下进程虚拟地址空间关键观察可执行文件本体被映射在低地址0x55f9...分成r--p / r-xp / r--p / rw-p多段与程序头表中的 LOAD 段一一对应。libmymath.so、libc.so.6、ld-linux-x86-64.so.2都被映射在栈和堆之间的共享区mmap 区。每次运行这些基址都不同——这是ASLR地址空间布局随机化的效果。8.2 为什么动态库必须是位置无关的-fPIC既然动态库每次被加载的地址都不同那么库里面如果写死了「add函数在0x7f750ec0c0f9」换个地址就全错了。传统做法是加载时修改代码段里的地址加载时重定位但这样代码页被改写后每个进程都要有一份自己的副本共享就失去了意义。PIC 的做法是库内部的跳转全部使用相对于当前指令RIP的偏移无论库加载在哪里相对距离都不变。需要访问外部符号其他库的函数 / 全局变量时不直接写地址而是通过数据段中的 GOT全局偏移表间接访问。运行时只需要修改 GOT数据页每个进程本来就各有一份代码页保持不变可以被所有进程共享。多个进程共享同一份动态库的物理内存8.3 PLT / GOT动态库函数是怎么被调用到的先看重定位信息在各个阶段的变化重定位 从 main.o 到 PLT/GOT编译后main.ocall add的目标地址还是空的只留下一条重定位项R_X86_64_PLT32 add意思是「等链接时填上add的 PLT 入口」。链接后app_dyncall已经被改成call 1070 addplt同时在 GOT 中预留了槽位对应的重定位项是R_X86_64_JUMP_SLOT add等运行时由动态链接器填入真实地址。完整的调用流程PLT / GOT 调用流程main执行call addplt跳到 PLT 中的一小段桩代码。桩代码执行jmp *GOT[add]到 GOT 表里取目标地址。第一次调用时GOT 槽中还不是add的真实地址而是指向解析逻辑于是进入动态链接器的_dl_runtime_resolve。动态链接器在已加载的库中查找符号add找到后回填到 GOT 槽。跳转到libmymath.so中真正的add执行。之后再调用add第 2 步直接就能拿到真实地址只多一次间接跳转。这种「用到时才解析」的策略叫延迟绑定Lazy Binding可以加快程序启动速度。补充许多发行版出于安全考虑默认开启-z nowFull RELRO在程序启动时就解析全部符号然后把 GOT 设为只读防止 GOT 被篡改。机制不变只是解析时机提前。可以用LD_BIND_NOW1 ./app_dyn观察同样的效果。8.4 静态库为什么不需要这些静态链接时add.o、sub.o的代码被直接拷贝进可执行文件链接器在链接阶段就知道它们最终的地址直接把call的目标改成真实函数地址$ nm app_static|grep-E T (add|sub|main)00000000000011a2 Tadd0000000000001149 T main 00000000000011ba T subadd和main在同一个可执行文件里不需要 PLT / GOT也不需要运行时查找。9. 总结问题结论库是什么一组可复用的目标文件以二进制形式发布静态库怎么做gcc -c生成.oar -rcs打包动态库怎么做gcc -fPIC -c生成位置无关的.ogcc -shared生成.so怎么使用库-I头文件路径-L库路径-l库名动态库运行找不到-L只管编译期运行期看LD_LIBRARY_PATH/ RUNPATH /ld.so.cache/ 系统目录ELF 两种视图Section 给链接器Segment 给加载器同权限的节合并成段程序如何加载内核映射 LOAD 段 → 交给ld.so→ 加载依赖库 重定位 →_start→main动态库为何能共享PIC 让代码页与加载地址无关外部地址集中放在可写的 GOT 中函数如何跨库调用call xxxplt→jmp *GOT→ 首次由ld.so解析并回填常用命令速查ar-tlibxxx.a# 查看静态库里的目标文件nm-Dlibxxx.so# 查看动态库导出的符号ldd ./app# 查看程序依赖的动态库file./app# 查看文件类型readelf-h./app# ELF 头readelf-S./app# 节头表Sectionreadelf-l./app# 程序头表Segment及映射关系readelf-d./app# 动态段NEEDED、RUNPATH 等readelf-r./app# 重定位表objdump-d./app# 反汇编cat/proc/pid/maps# 查看进程的内存映射附录一键复现把第 2 节的代码放好后在demo/目录下新建MakefileINC : -Imylib/include SRC : mylib/src/add.c mylib/src/sub.c .PHONY: static shared clean static: gcc -c $(SRC) $(INC) ar -rcs libmymath.a add.o sub.o gcc main.c -o app_static $(INC) -L. -lmymath -static rm -f add.o sub.o ./app_static shared: gcc -fPIC -c $(SRC) $(INC) gcc -shared -o libmymath.so add.o sub.o gcc main.c -o app_dyn $(INC) -L. -lmymath -Wl,-rpath,$$ORIGIN rm -f add.o sub.o ./app_dyn clean: rm -f *.o *.a *.so app_static app_dyn然后执行make static或make shared即可。注意 Makefile 中命令前必须是Tab$$ORIGIN中的$$是 Makefile 对$的转义。如果本文对你有帮助欢迎点赞、收藏、关注有问题欢迎在评论区交流。
延伸阅读

更多相关文章

2026/11/2 15:27:28

从一行代码慢十倍说起:CPU 缓存行与伪共享

性能优化的话题里,有一类问题的隐蔽性远超算法复杂度:代码逻辑完全正确,汇编也不多,但性能就是上不去,profile 里也看不到任何热点函数。伪共享属于这一类——它的病灶不在代码里,在 CPU 缓存的物理结构里。…

2026/11/2 15:22:27

Python量化实战:回测工程避坑之财报偏差伪信号(附排查清单与代码)

TL;DR 成因:cyq_perf(筹码胜率)依赖当日逐笔成交与清算数据,最终值普遍在 15:30–23:00 才更新,盘中拿不到当日正式信号;而多数回测框架默认「当日因子值 + 当日收盘价成交」,等于用收盘后才产生的信息倒回当日交易,属于隐性未来函数。 自查:分时段(14:50 / 15:00 / …

2026/11/2 16:27:57

ProF脚本刷写流程:状态机、参数传递与现场避坑

简介:这份PDF教程面向汽车电子标定与ECU编程方向的学习者和工程师,聚焦INCA软件中ProF编程脚本的入门与梳理,帮助读者理解如何用脚本组织闪存编程等自动化流程。内容围绕.prm主控制流、.pri子流程和.cnf控制器配置三类文件展开,并…

2026/11/2 16:27:57

GPU Kernel内追踪:二进制插桩与探针拼接实现高保真

处理 GPU kernel 性能问题的时候,我最怕的不是算不动,而是看不到。Nsight Compute 能告诉你 occupancy 偏低、内存吞吐上不去,可当你真的需要知道"是第几条指令、哪个 warp、卡在哪个分支上出了事",大多数工具立刻哑火。…

2026/11/2 16:27:57

Redis命令体系解析与高效使用指南

1. Redis命令概述与基础架构 Redis作为内存数据结构存储系统,其命令体系构成了整个系统的操作接口。与关系型数据库的SQL语句不同,Redis命令采用"动词-键-参数"的简单结构,这种设计使得单个命令的执行时间通常保持在微秒级别。命令…

2026/11/2 16:27:57

Codex跨系统小工具开发:如何确保结果正确而非仅能运行

跨系统小工具最让人抓狂的地方,不是它跑不起来,而是它跑起来了、日志干干净净、退出码是 0,可你一看输出结果,数字对不上、路径全错、时间差了几个小时。我用 Codex 辅助写这类工具已经有一段时间了,从最初"能跑就…

2026/11/2 16:27:57

动态规划与递归:核心差异与工程优化实践

1. 动态规划与递归的本质解析动态规划和递归是算法设计中两个既相互关联又存在本质区别的核心方法论。在二叉树遍历这类经典场景中,递归实现往往只需要3-5行代码就能完成深度优先搜索,而对应的迭代实现则需要借助栈结构,代码量通常增加2-3倍。…

2026/11/2 16:22:54

DeepSeek-R1显式推理机制解析:符号约束与可调试推理链

简介:本资源是一份面向AI研发工程师与大模型应用实践者的深度技术指南,聚焦国产开源推理模型DeepSeek-R1的原理认知、场景适配与高阶提示语设计。针对逻辑推理、代码生成、复杂文本分析等高密度任务需求,系统解析推理模型与通用模型的本质差异…

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

2026/11/2 1:04:04

工业级SPI隔离设计:硬件抗扰与信号完整性实战指南

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

2026/11/2 1:04:04

PLFM_RADAR:主动探测数据通道,构建低误报的数据感知系统

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

2026/11/2 1:04:04

工业SPI隔离设计:MAX14431与STM32F746硬件抗干扰实战

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

2026/11/2 6:08:01

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/2 6:39:12

SSM电子书店源码全解析:结构、原理、部署与排错实战

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

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

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

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