资讯动态

Linux基础:动静态库制作与原理

发布时间:2026/8/28 7:41:41 来源:尧图企业网站定制
文章目录从零搞懂Linux动静态库从手搓库到扒开ELF底裤一、先认俩兄弟静态库 vs 动态库二、手搓静态库有手就行的打包艺术1. 准备源码2. 编译打包ar工具登场2.5 新手红线千万别把main打进静态库2.6 打包发布把你的库做成「预制菜大礼包」发给别人3. 使用静态库的三种姿势三、手搓动态库共享才是内存的救星1. 生成动态库灵魂拷问为什么要加-fPIC2. 动态库的「天坑」运行时找不到四、扒开底裤ELF文件到底是什么1. ELF的四种形态2. 两套视图链接看section运行看segment3. 为什么要合并成segment五、动态链接的灵魂GOT和PLT是怎么演戏的1. GOT放在数据段的「地址本」2. PLT 延迟绑定能偷懒就偷懒第一次调用函数的完整流程以puts为例第二次调用同一个函数3. 安全补丁RELRO保护六、一张表总结动静态库怎么选附录1常用命令速查表附录2新手常见报错速查表从零搞懂Linux动静态库从手搓库到扒开ELF底裤写代码就像做饭总不能每次都从种小麦、磨面粉开始吧库就是程序员的「预制菜」——别人写好、测过、打包好的成熟代码拿来就能复用。今天咱们就从手搓一个库开始把Linux下动静态库的原理、制作、使用连带着ELF文件、GOT/PLT这些底层玩意儿一次性讲明白。一、先认俩兄弟静态库 vs 动态库本质上库就是二进制代码的压缩包Linux下分两大流派类型后缀核心特点通俗类比静态库.a编译链接时直接把库代码全拷进你的可执行文件运行时不需要库了把预制菜连汤带料全倒进你的外卖盒吃的时候不用找商家动态库.so编译时只记「函数花名册」运行时才加载到内存多个程序共享同一份大家都去同一家食堂打饭不用每家每户都开个厨房很多人学到后面都搞混核心区别静态链接是编译时搞定一切动态链接是运行时再上门。静态链接把代码直接塞进你的程序里动态链接只留个联系方式跑起来的时候再上门加载。系统里其实到处都是它们的身影比如C标准库# 动态库 ls -l /lib/x86_64-linux-gnu/libc-2.31.so # 静态库 libc.a ls -l /lib/x86_64-linux-gnu/libc.agcc默认优先用动态库找不到动态的才会用静态的加-static可以强制全静态链接。动态库最大的优势就是多进程共享物理内存。每个进程都有自己独立的虚拟地址空间各自把so映射到自己的共享区虚拟地址上但在内核层面大家的页表都指向同一份物理内存页。代码段只读所以大家共用一份也不会乱数据段是私有的哪个进程要写数据内核就触发「写时复制」COW给它单独分配一份物理页互不影响。一份库代码所有进程共用内存直接省一大截。二、手搓静态库有手就行的打包艺术咱们就用一套简化版的「自实现stdio字符串函数」来练手全程三步搞定。1. 准备源码先写两个源文件和对应的头文件// my_string.h #pragma once int my_strlen(const char *s);// my_string.c #include my_string.h int my_strlen(const char *s) { const char *end s; while(*end ! \0) end; return end - s; }// my_stdio.h #pragma once #define SIZE 1024 typedef struct IO_FILE { int flag; int fileno; char outbuffer[SIZE]; int cap; int size; } mFILE; mFILE *mfopen(const char *filename, const char *mode); int mfwrite(const void *ptr, int num, mFILE *stream); void mfflush(mFILE *stream); void mfclose(mFILE *stream);my_stdio.c就不贴全了核心就是封装open/write/close系统调用加个用户层缓冲区。2. 编译打包ar工具登场静态库本质就是一堆.o目标文件的压缩包用ar归档工具来做。先搞懂最核心的-c参数-c的意思是只编译不链接——把.c源码翻译成二进制的.o目标文件。你可以把.o理解成「半成品零件」还没组装不能直接运行静态库本质就是把一堆零件打包成一个「零件包」最后链接的时候再和主程序的零件拼成完整的可执行程序。如果不加-cgcc会一口气完成「编译链接」两步直接生成可执行文件就没法单独打包库了。# Makefile libmystdio.a: my_stdio.o my_string.o ar -rc $ $^ # r替换旧文件 c创建新库 %.o: %.c gcc -c $ # 只编译不链接生成.o .PHONY: clean clean: rm -rf *.a *.o这里出现了三个奇奇怪怪的符号它们是Make的自动变量专门帮你少写重复文件名$代表当前规则的目标也就是冒号左边的文件名这里就是libmystdio.a$^代表所有依赖文件也就是冒号右边的全部内容这里是my_stdio.o my_string.o$代表第一个依赖文件模式匹配里就对应当前的.c文件另外提三个新手必知的Make冷知识make默认只执行文件里第一条规则所以我们把生成库的规则放最上面敲make就能直接出结果像clean这种目标就要显式敲make clean。.PHONY: clean是声明「clean是个伪目标不是真的文件」——万一你目录里真有个叫clean的文件make会以为文件已经最新了不执行加了这个就不会踩坑。如果你改了代码再敲make提示xxx is up to date.别慌这是make发现目标文件比依赖文件新觉得不用重新编译。执行一下make clean清掉旧产物再编就好了。执行make你就得到了libmystdio.a。用ar -tv可以查看库里装了啥$ ar -tv libmystdio.a rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o2.5 新手红线千万别把main打进静态库静态库是「函数仓库」只装功能代码main是程序的入口一个可执行文件只能有一个main。如果你把带main的.o也打包进.a里别人用你的库的时候就会报multiple definition of mainmain重定义的错——相当于一个外卖盒里塞了两份米饭根本没法吃。记住库文件里只放功能实现主程序单独编译最后链接的时候再拼到一起。2.6 打包发布把你的库做成「预制菜大礼包」发给别人自己写自己用很简单但要把库发给同事/别人用总不能发一堆散乱的文件吧标准的交付姿势是把头文件和库文件整理好打成一个压缩包别人解压就能用。完整三步打包法建目录把头文件和库文件分开存放mkdir -p lib/include # 放所有.h头文件 mkdir -p lib/lib # 放.a静态库-p参数的意思是「父目录不存在就自动创建」哪怕目录已经存在也不会报错写脚本必加。复制文件把对应的文件拷进去cp -f *.h lib/include cp -f *.a lib/lib-f是强制覆盖目标位置有同名文件直接替换不会弹提示问你——写脚本/Makefile一定要加不然脚本会卡着等你输入。❌ 新手巨坑cp 文件名 目标文件夹的时候如果目标文件夹不存在cp不会帮你建文件夹反而会生成一个叫「目标文件夹」的普通文件保险写法是末尾加斜杠cp my_string.h lib/include/强制告诉cp这是个目录。打包压缩整个文件夹打成一个压缩包tar -czf lib.tgz lib最终你只需要把lib.tgz这一个文件发给别人就行。别人拿到手解压tar -zxf lib.tgz就能得到整齐的头文件库文件用-I和-L指定路径就能编译用了。3. 使用静态库的三种姿势写个测试程序main.c#include my_stdio.h #include my_string.h #include stdio.h int main() { const char *s hello lib!; printf(len: %d\n, my_strlen(s)); mFILE *fp mfopen(./log.txt, w); mfwrite(s, my_strlen(s), fp); mfclose(fp); return 0; }根据库和头文件的位置有三种编译方式场景1安装到系统路径头文件放/usr/include库放/usr/libgcc main.c -lmystdio场景2库和源码在同一目录gcc main.c -L. -lmystdio场景3头文件和库在独立目录比如include/和lib/gcc main.c -I./include -L./lib -lmystdio三个参数记牢-I指定头文件搜索路径-L指定库文件搜索路径-l指定库名去掉前缀lib和后缀.a/.so比如libmystdio.a就写-lmystdio编译完删掉静态库程序照样跑——因为代码已经全拷进可执行文件里了。三、手搓动态库共享才是内存的救星动态库是现在的主流毕竟能省磁盘又省内存制作起来也只比静态库多两个参数。1. 生成动态库关键两个编译选项-fPIC和-shared# Makefile libmystdio.so: my_stdio.o my_string.o gcc -o $ $^ -shared # 生成共享库格式 %.o: %.c gcc -fPIC -c $ # 生成位置无关代码 .PHONY: clean clean: rm -rf *.so *.o灵魂拷问为什么要加-fPICPIC全称位置无关代码Position‑Independent Code。动态库不像可执行文件有固定加载地址——不同进程的地址空间不一样同一个.so可能被映射到内存的不同角落。如果代码里写死了绝对地址换个位置直接崩给你看。说到这儿肯定有人继续追问那运行时知道库加载到哪了直接把指令里的地址改了不就行了这就是动态链接最核心的一个约束代码段.text是只读的。你想啊多个进程共享同一份库的物理内存如果A进程把指令改了B进程跑的时候直接就乱套了。所以操作系统从硬件层面就把代码段的内存页设成了「只读可执行」你敢写就直接给你报段错误SIGSEGV程序当场崩给你看。所以不是不想直接改地址是根本改不了。那怎么办指令不能改那就在可写的数据段单独开一块地方存地址——这就是后面要讲的GOT表。-fPIC说白了就是一套「代码只读、地址外放、间接跳转」的规范保证代码本身在哪都能跑要变的地址全扔到可写的数据区里。静态库是编译时就把地址定死了所以不需要-fPIC。2. 动态库的「天坑」运行时找不到编译的时候一切正常一运行就报错用ldd查一下依赖$ ldd a.out libmystdio.so not found # 红通通的找不到 linux-vdso.so.1 /lib64/linux-vdso.so.1这里很多新手会搞混-L明明已经指定了库路径为什么运行还找不到说白了就是两个阶段各管各的-L只管编译链接阶段相当于你点外卖的时候告诉商家「我家在XX小区」商家按地址给你配餐LD_LIBRARY_PATH只管程序运行阶段相当于外卖员上门的时候按这个地址找你家编译的时候链接器只认-L运行的时候动态链接器只认系统库路径和LD_LIBRARY_PATH俩部门不共享信息所以编译过了不代表运行能找到。解决办法有四种按推荐度排序配置系统库路径在/etc/ld.so.conf.d/下加配置文件执行ldconfig刷新缓存环境变量临时方案export LD_LIBRARY_PATH./:$LD_LIBRARY_PATH软链接到系统目录ln -s /你的路径/libmystdio.so /usr/lib64/直接拷进系统库目录不推荐容易搞乱系统四、扒开底裤ELF文件到底是什么不管是.o、.so还是可执行程序本质都是同一种文件格式——ELFExecutable and Linkable Format。它就像一个标准化的集装箱把代码、数据、符号表全按规矩装进去。1. ELF的四种形态可重定位文件.o半成品用来和其他目标文件链接可执行文件能直接跑的程序共享目标文件.so动态库核心转储文件core dump程序挂了之后的「案发现场」2. 两套视图链接看section运行看segmentELF最巧妙的地方就是提供了两个视角一套给链接器看一套给操作系统加载器看链接视图节Section按功能把文件拆成一小块一小块比如.text存代码、.data存初始化的全局变量、.bss存未初始化变量、.symtab存符号表。链接器就像拼乐高把各个.o的同名section拼在一起。执行视图段Segment加载到内存时操作系统不关心细碎的section只关心「这块内存有没有读/写/执行权限」。于是把权限相同的section合并成一个大段segment比如.text和.rodata都是只读的就合并成一个只读段加载。通俗类比section是一个个零散的零件螺丝、芯片、电容segment是组装好的主板、电源模块。工厂生产链接按零件管用户装机加载按模块来。用两条命令就能验证# 查看所有section链接视图 readelf -S a.out # 查看所有segment执行视图 readelf -l a.out很多人学到这儿都停留在“section是给链接器看的segment是给系统看的”但没说透最关键的一点segment里带了权限标记这才是代码段只读的根源。你用readelf -l看segment的时候Flags那一列会标着R E只读、可执行或者RW可读可写。这个标记是链接器生成ELF的时候就写进程序头Program Header里的。等程序运行的时候内核调用mmap把segment映射进进程虚拟地址空间会严格按照这个标记设置页表的硬件权限位。CPU的内存管理单元MMU每次访问内存都会查这个权限位——写只读页直接拦下来触发异常。举个例子.text代码段和.rodata常量段都是只读的链接的时候就会被合并到同一个PT_LOAD类型的segment里权限标成R E.data、.got、.bss这些可写的合并到另一个segment权限标成RW。内存页是硬件管理的最小单位一般是4KB一页。把相同权限的section合并成一个大segment一来减少内存碎片二来一次性设置好整段的权限管理效率高。3. 为什么要合并成segment内存是按「页」来管理的一般4KB一页如果每个小section都单独占一页会产生大量内存碎片。合并成大段之后内存利用率直接拉满同时也方便统一设置内存权限比如代码段只读、数据段可写安全性更高。五、动态链接的灵魂GOT和PLT是怎么演戏的动态链接的核心矛盾是代码段是只读的不能直接修改指令里的地址但动态库加载地址又不固定怎么找到函数大佬们想出了绝妙的方案GOT全局偏移表 PLT过程链接表1. GOT放在数据段的「地址本」既然代码段不能改那就把函数地址存在数据段里数据段可写这张表就是GOTGlobal Offset Table全局偏移表。先掰扯清楚一个最容易混淆的点GOT里存的是什么地址是虚拟地址不是物理地址。用户态的程序和动态链接器根本碰不到物理地址。整个动态链接过程全在虚拟地址空间里玩mmap给动态库分配一段虚拟地址区间共享区拿到库的虚拟基址然后「真实虚拟地址 虚拟基址 库内函数偏移」算出来的还是虚拟地址填进GOT表里。至于虚拟地址对应哪块物理内存那是内核页表管的事CPU的MMU会自动翻译用户态完全不用管。说回为什么必须有GOT代码段里的函数调用不直接写死地址而是写「去GOT表第N项找真实地址」。动态库加载完成后动态链接器把真实函数地址填进GOT后面调用直接查表跳转就行。这样代码段全程只读能被所有进程共享——多个进程各自有自己的GOT表在自己的数据段里但大家的代码段都指向同一份物理内存互不干扰又省内存。这里再补一个灵魂拷问每个进程的GOT不一样那代码段怎么知道GOT在哪很简单用相对寻址。GOT和代码段都在同一个so里它们之间的相对偏移是编译的时候就固定死的。代码里只要写「当前指令偏移多少多少就是GOT」不管库加载到哪个虚拟地址这个相对距离永远不变所以总能找到GOT。这就是PIC的精髓。2. PLT 延迟绑定能偷懒就偷懒按说mmap完库基址就有了所有函数的真实虚拟地址都能算出来为什么不启动的时候一次性全填进GOT两个字效率。一个标准的shturl.里有上千个函数大部分程序可能只用其中十几个。程序一启动就把上千个函数全解析一遍纯纯浪费时间启动速度直接拖慢。于是大佬们搞了个「延迟绑定」Lazy Binding哪个函数第一次被调用才去解析它的地址不用就永远不解析。配合这个机制的就是PLTProcedure Linkage Table过程链接表。你可以把PLT理解成一个个“前台接待员”每个外部函数对应一个PLT条目都放在只读的代码段里指令永远不变。第一次调用函数的完整流程以puts为例代码里调用puts其实是call putsplt直接跳到PLT里对应的接待员位置PLT里第一条指令就是间接跳转去GOT表里拿地址这时候GOT里还没填真实地址默认存的是PLT内部的一个跳板地址所以相当于跳了一圈又跳回PLT的下一条指令把当前函数的编号压栈调用动态链接器的_dl_runtime_resolve函数动态链接器拿到函数编号去符号表里查算出「基址偏移」得到真实虚拟地址把算出来的真实地址写回GOT表对应的位置GOT在数据段可写所以没问题跳转到真实的puts函数执行第二次调用同一个函数还是call putsplt跳到PLTPLT还是去GOT拿地址这时候GOT里已经存了真实地址了直接跳转到真实函数执行全程不碰动态链接器说白了就是第一次打电话问前台前台查完记在通讯录GOT里下次再打前台直接翻通讯录不用再查了。第一次麻烦一次后面全是光速。这里澄清一个常见误区不是启动的时候算不出地址是故意不算。你要是想让它启动的时候全算完也可以设置环境变量LD_BIND_NOW1再运行程序就会关闭延迟绑定启动阶段一次性把所有符号全解析完GOT一上来就全是真实地址。代价就是程序启动变慢好处是第一次调用函数的时候没有额外开销。3. 安全补丁RELRO保护既然GOT是可写的那黑客能不能篡改GOT表里的地址让程序跳去执行恶意代码当然可以这就是经典的「GOT劫持攻击」。所以现代Linux又加了一层保护RELRORelocation Read-Only。简单说就是等所有重定位都做完之后把GOT表所在的内存页改成只读。这样就算黑客拿到了程序的写权限也改不了GOT表没法劫持函数流程。这也解释了为什么不直接把GOT放只读段——一开始必须可写才能填地址填完了再锁成只读两全其美。六、一张表总结动静态库怎么选维度静态库动态库可执行文件体积大代码全拷进去小只存依赖信息运行依赖无删了库也能跑必须有对应库文件内存占用每个程序各存一份浪费多进程共享一份省内存升级维护要重新编译整个程序替换库文件就行不用改程序启动速度快没有加载过程稍慢需要加载和符号解析重定位时机编译链接时一次性完成运行时重定位默认延迟绑定代码修改方式链接时直接修改指令代码段只读修改GOT表间接跳转核心机制目标文件合并静态重定位PIC位置无关代码 GOT/PLT mmap映射适用场景小工具、不想依赖环境的程序大型系统、通用基础库附录1常用命令速查表命令作用ar -rc libxxx.a *.o打包静态库gcc -fPIC -shared -o libxxx.so *.c生成动态库ldd 程序/库查看依赖的动态库readelf -h 文件查看ELF文件头readelf -S 文件查看所有sectionreadelf -l 文件查看所有segmentobjdump -d 文件反汇编代码段file 文件查看文件类型附录2新手常见报错速查表报错信息原因解决办法xxx.h: No such file or directory找不到头文件检查-I后面的路径确认头文件在对应目录里cannot find -lxxx找不到库文件检查-L后面的路径确认目录里有libxxx.a/libxxx.soundefined reference to xxx找不到函数实现检查有没有加-lxxx参数确认库里确实包含这个函数multiple definition of main多个main函数冲突静态库/动态库里别打包带main的.o文件make: xxx is up to date.目标文件已是最新无需重建执行make clean清理旧产物后重新编译Segmentation fault (core dumped)内存访问越界/写只读页检查是否修改了常量、字符串常量或者越界访问数组搞懂这些你再遇到「链接错误」「库找不到」这类问题就不会只会瞎搜命令了——毕竟知道了原理排查问题都是降维打击。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价