ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

《从零入门Linux系统篇(三十五):文件篇·八——静态库与动态库详解:从制作链接到动态库加载》

《从零入门Linux系统篇(三十五):文件篇·八——静态库与动态库详解:从制作链接到动态库加载》 这篇文章我们要把Linux环境下动静态库的那点事儿从头到尾彻底讲透。先搞清楚最底层的原理库不过是一堆.o目标文件的归档集合。怎么把这些散装目标文件打包成库ar命令怎么用一个能对外交付的库目录该怎么组织编译时-I、-L、-l这些选项到底在指挥什么这些都是制作阶段绕不开的基本功。再做共享库就必须迈过-fPIC这道坎。位置无关代码为什么要单独编译-shared又是怎么把一堆位置无关的.o打包成.so的这背后是链接器和加载器之间的默契。然后是最让人抓狂的环节做好了一运行却报错找不到.so。为什么编译时明明通过了运行时却翻车根子在于“编译器”和“操作系统加载器”不是一家人。针对这个顽疾我们会给出四种解法系统路径拷贝、软链接、LD_LIBRARY_PATH环境变量以及/etc/ld.so.conf.d/配置。临时的、永久的各取所需。目录一、基础概念什么是库1.1 库的本质——为什么需要库1.2 静态库与动态库的命名规范二、静态库的制作与使用2.1 静态库的制作与打包2.2 静态库的发布与交付2.3 静态库的链接与使用2.4 实战案例从库的生产者到使用者2.4.1 开发者视角Creater_Mr.Li2.4.2 使用者视角User_Mr.Zc三、动态库的制作与使用3.1 动态库的编译与打包3.2 动态库运行时加载失败3.2.1 为什么静态库不会遇到同样的问题3.2.2 问题根源编译器 ≠ 操作系统四、动态库加载失败的四种解决方案4.1 方案一复制到系统标准库路径——完成“安装”4.2 方案二在系统标准路径建立软链接4.3 方案三配置LD_LIBRARY_PATH环境变量4.4 方案四配置/etc/ld.so.conf.d/五、动静态库混合链接与优先级5.1 gcc默认的链接行为5.2 强制进行静态链接一、基础概念什么是库1.1 库的本质——为什么需要库在Linux环境下不管静态库还是动态库剥开皮看都是一回事一堆.o目标文件的集合。你写的每个.c文件编译完都会变成对应的.o文件。这些.o文件散落在项目里自己用没问题但想给别人复用就太零碎了。库的诞生就是把这些.o文件打包装箱再配上对外的头文件.h别人拿去就能用既方便复用代码又保护了你的源码不被直接窥探。说到底库就是“编译好的半成品”的仓库头文件是说明书.o集合才是真正干活的零件。1.2 静态库与动态库的命名规范Linux里库的命名有着严格的规矩编译器也是靠这层“皮”来辨认谁是静态库、谁是动态库静态库前缀必须是lib后缀是.a。比如libmyc.a。动态库前缀同样是lib后缀是.so。比如libmyc.so。前缀lib不是装饰品它是链接器默认查找的“暗号”。你写-lmyc链接器就自动去搜libmyc.so或libmyc.a。顺带对比一下Windows的命名静态库通常以.lib结尾。动态库以.dllDynamic Link Library结尾。所以换平台时别把后缀搞混了。Linux的.a是静态、.so是动态Windows的.lib是静态、.dll是动态。名字不同思路相同都用前缀和后缀把库的类型标得明明白白。二、静态库的制作与使用2.1 静态库的制作与打包静态库的本质就是一个归档文件。把一堆.o目标文件用ar命令打个包后缀写个.a它就成库了。步骤就两步简单到没朋友。先把所有源文件编译成目标文件gcc -c mystdio.c mystring.c这行跑完mystdio.o和mystring.o就躺在那了。接着用ar把它们归拢成静态库ar -rc libmyc.a mystdio.o mystring.o两个参数拆开看-rreplace如果库里有同名目标文件直接替换掉旧的。-ccreate创建一个全新的归档文件。合起来-rc就是“给我造个新库有重名的就换掉”。一条命令散装的.o就变成了一块整砖libmyc.a。静态库里能不能塞main函数绝对不能。这是红线碰都不能碰。库是给别人的项目拿来调用的功能集合它自己不该有“入口”。如果库里面藏了个main等使用者链接的时候他项目里那个main跟你库里的main就会在符号表里撞个满怀。两个同名main 一碰面链接器直接懵掉报符号冲突整个链接过程就崩了。所以记住库是“工具箱”不是“可执行程序”。 工具只管提供函数main这种“总开关”只有使用者的程序才有资格拥有。2.2 静态库的发布与交付库做好了总不能让人家自己跑去你项目里东翻西找一个.a文件。得把它组织成一套像模像样的“安装包”别人才愿意用、才用得顺。最标准的发布姿势是搭一个规范的目录结构。比如这样lib/ ├── include/ │ ├── mystdio.h │ └── mystring.h └── mylib/ └── libmyc.ainclude/下面放头文件mylib/下面放库文件。这种“头文件归头文件库文件归库文件”的布局就是给使用者最清晰的界面想包含什么去include翻想链接什么去mylib拿。一目了然互不干扰。目录搭好了再用tar打个包一个简易的库“安装包”就诞生了tar czf lib.tgz lib这里c是创建归档z是顺手压缩一把f指定输出文件。一条命令把整个lib目录裹成一个lib.tgz。别人拿到这个压缩包解压开来就能按图索骥地使用了。如果想让交付更体面你甚至可以在里面塞上一对安装脚本和卸载脚本。一个负责把文件拷到系统目录一个负责清干净。当然这属于加分项小项目不写也完全没问题。但只要写了整个库的“专业感”瞬间就上来了。2.3 静态库的链接与使用使用者拿到库之后满心欢喜地敲下编译命令结果往往被一盆冷水浇醒编译报错头文件找不到库也找不到。为什么因为gcc默认只去系统标准路径里溜达它才不会屈尊到你的当前目录下翻找这些“来路不明”的头文件和库文件。所以编译用户的源文件时必须自己动手把路径指给编译器看。标准姿势是这样一条命令gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc别小看这三个选项它们就是“指路三件套”缺一不可-I大写i告诉编译器“头文件去哪个目录找”。这里就是lib/include/。-L大写l告诉链接器“库文件去哪个目录找”。这里就是lib/mylib/。-l小写L指定你要链接哪个库。注意-lmyc其实是在找libmyc.a或libmyc.so。它把前面的lib和后面的.a/.so都省掉了只留下中间那个“小名”。那么问题来了为什么我们写C语言时从来不用这些选项printf、malloc照样用得好好的因为标准库是“自己人”。gcc/g编译器天生就认识它们知道它们住在哪头文件在/usr/include库文件在/lib64这些路径早就刻在编译器的默认搜索列表里了。所以标准库的-I、-L、-l编译器都替你默默加好了。但只要你用到的库不在这个“默认白名单”里不管是第三方库还是我们自己写的库那就得用这三个选项把路指得明明白白。库是外来的路得自己问。2.4 实战案例从库的生产者到使用者为了把静态库的制作流程彻底看透我们直接上实战案例。整个项目拆成两个角色开发者Creater_Mr.Li负责写代码、打包交付使用者User_Mr.Zc负责拿到库后编译自己的程序。2.4.1 开发者视角Creater_Mr.Li作为库的创作者Mr.Li的任务是写好核心代码编译成目标文件打成静态库再整理成规范的交付目录最后打包交给用户。他的工作目录长这样Creater_Mr.Li ├── libmyc.h # 核心功能头文件 ├── libmyc.cpp # 核心功能实现源文件 ├── MyString.h # 字符串工具头文件 ├── MyString.cpp # 字符串工具源文件 ├── ObjectOfStatic/ # 静态库专用目标文件目录 │ ├── libmyc.o │ └── MyString.o ├── lib/ # 准备交付的规范目录 │ ├── include/ # 所有对外公开的头文件 (libmyc.h, MyString.h) │ └── mylib/ # 打包生成的静态库文件 (libmyc.a) └── lib.tgz # 最终交付给用户的压缩包步骤复现在ObjectOfStatic目录下Mr.Li执行编译命令把两个源文件变成普通目标文件g -c ../libmyc.cpp ../MyString.cpp这行跑完libmyc.o和MyString.o就躺在ObjectOfStatic里了。接着用ar把所有.o文件归档成静态库ar -rc libmyc.a *.o-r是替换-c是创建*.o通配符一把抓两个目标文件被整整齐齐塞进libmyc.a。然后进入交付整理阶段把libmyc.a放进lib/mylib/把libmyc.h和MyString.h放进lib/include/。最后整个lib目录打包压缩tar czf lib.tgz lib搞定。一份“拿着就能用”的静态库安装包就此出炉。从零散的源代码到规范的目标文件再到归档成库最后整理成标准目录打包装箱开发者视角的完整流程就这样闭环了。接下来我们把镜头切到使用者 Mr.Zc 那边看看他拿到 lib.tgz 后要干些什么。2.4.2 使用者视角User_Mr.Zc​镜头切到使用者这边。Mr.Zc从Mr.Li手里接过lib.tgz解压、写业务、编译一条龙走起。他的工作目录长这样User_Mr.Zc ├── lib.tgz # 从 Mr.Li 处获取的静态库压缩包 ├── lib/ # 解压出来的库资源 │ ├── include/ # 引入的头文件 (libmyc.h, MyString.h) │ └── mylib/ # 引入的静态库 (libmyc.a) ├── usercode.cpp # Mr.Zc 自己的业务代码 (包含 #include libmyc.h) ├── Makefile # 自动化编译脚本 └── proc # 最终链接生成的独立可执行程序编译运行实操Mr.Zc在Makefile里封装好了链接规则执行编译时底层真正跑的命令是g -o proc usercode.cpp -I lib/include -L lib/mylib -lmyc这行命令指路三件套齐全-I告诉编译器头文件在哪-L告诉链接器库文件在哪-l点名要链libmyc.a。因为走的是静态链接编译成功后proc这个可执行程序已经把库里的libmyc.o和MyString.o的二进制代码结结实实拷贝进了自己身体里。所以接下来见证奇迹的时刻到了哪怕Mr.Zc一挥手把整个解压出来的lib/目录删得干干净净proc 照样跑得欢。它已经不需要任何外部库文件了代码就在它自己肚子里走到哪都能独立运行。这就是静态库最实在的优点交付时带着库运行时就忘了库。一旦链接完成库的历史使命就结束了程序变成一个完全自给自足的整体。当然代价就是体积大了一圈但这会儿Mr.Zc可不会在乎他正忙着欣赏那个删了目录还能跑的程序呢。三、动态库的制作与使用3.1 动态库的编译与打包动态库跟静态库最大的不同在于它“不认死理”。它必须能够在内存的任意位置被加载、被多个进程共享。这就要求它内部的代码不能写死地址得学会“随遇而安”。这种代码有个专业名词叫位置无关码Position Independent Code。所以制作动态库的第一步就是编译出位置无关的目标文件gcc -fPIC -c mystdio.c mystring.c这里那个-fPIC选项就是告诉编译器“别把地址写死给我生成可以随便搬家的代码。”少了它后面打包出来的动态库一加载就可能崩。第二步用-shared把这些位置无关的.o打包成.sogcc -shared -o libmyc.so mystdio.o mystring.o-shared的意思很直白我要生成的是一个共享对象不是可执行程序。-o libmyc.so指定输出文件名前前后后的命名规范也得跟上lib前缀.so后缀。打完包想验证一下是不是真动态库用file命令探一探file User_Mr.Zc/lib/mylib/libmyc.so输出会告诉你它是个ELF 64-bit LSB shared object属性是dynamically linked。简而言之它身份明确就是一个等着被多个进程共享的动态库。跟静态库那种“闷头打包”的性格完全不同动态库天生就带着“共享”的基因。[abc]$ file User_Mr.Zc/lib/mylib/libmyc.so User_Mr.Zc/lib/mylib/libmyc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]fb30dcc0b85625aa7f5b8999530a56954efff93a, not stripped3.2 动态库运行时加载失败在使用上动态库的编译链接指令和静态库一模一样gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc然而当编译成功并尝试执行./usercode时系统却会抛出以下经典错误./usercode: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory3.2.1 为什么静态库不会遇到同样的问题因为静态链接的玩法完全不同。在链接阶段编译器就把库里的代码结结实实地拷贝进了最终的可执行程序里。程序一旦生成它就把所有需要的东西都打包背在自己身上了代码、数据、函数实现一样不少。所以运行的时候它根本不需要回头去找那个.a文件。库文件删了也好路径变了也好都跟它没关系。它天生就“自给自足”走到哪儿都能独立跑起来。而动态链接呢编译时它可没这么慷慨。它只是往可执行程序里塞了一张“取货单”记录下“这个程序运行时需要libmyc.so”这样的符号信息。等到程序真正跑起来的那一刻操作系统才揣着这张单子去系统指定的那些路径里找库、加载。找到了万事大吉找不到就是那行刺眼的not found。一句话收束静态库是把行李打包带走动态库是把行李寄存到机场到地方再取。 取的时候你得知道寄存处还在不在那儿。3.2.2 问题根源编译器 ≠ 操作系统为什么编译通过一运行却找不到库先看一眼案发现场。用ldd usercode查一下程序依赖的动态库屏幕上会冷冷地蹦出这么一行libmyc.so not found你明明在编译时用-L指了路库也确实在那儿。可一运行系统却像失忆了一样说它找不到。为什么简单直接地说根本原因就一句话编译阶段和运行阶段是两拨完全不同的人在负责。gcc只负责“相亲”。编译阶段你敲下-L选项这是在跟gcc说“去这个路径帮我看看动态库在不在。在的话就放行编译。”gcc很听话跑去一看库确实在。于是它给你的可执行程序盖了个戳上面写着“这个程序运行时需要libmyc.so。”盖完戳gcc就拍拍屁股下班了。它的活儿到此为止。操作系统才负责“过日子”。等你真正敲下./proc运行程序冲上来的是操作系统内核和动态链接器ld-linux.so。这会儿gcc早下班了你指望它来带路门都没有。操作系统的任务是把libmyc.so找到并加载进内存。可它压根不知道你编译时用 -L 指过的路径是什么。你跟gcc说的话操作系统一个字都没听见。所以gcc ≠ 系统。你告诉了gcc库在哪里并不等于告诉了操作系统库在哪里。操作系统有它自己固定的“寻库朋友圈”比如/lib64、LD_LIBRARY_PATH这些。你的自定义路径要是没挤进这个圈子操作系统找不着就只会两手一摊甩你一句not found。相亲时见过面结婚时找不到人不是库没了是介绍人下班了。四、动态库加载失败的四种解决方案要让操作系统在运行时能顺利找到并加载我们的动态库有四种主流方案。它们有的简单粗暴有的优雅规范各有各的适用场景。4.1 方案一复制到系统标准库路径——完成“安装”Linux的动态链接器有个习惯运行程序时它会自动去系统标准路径下翻找动态库。所以最直接的办法就是把我们自己的库文件塞进这些默认路径。这一步本质上就是完成了一次“安装”。# 把库文件拷贝到系统标准的库搜索路径 sudo cp lib/mylib/libmyc.so /lib64/ # 如果头文件也需要系统级引用可以顺手放过去 sudo cp lib/include/* /usr/include/往/lib64一丢动态链接器一找一个准问题当场解决。不过这里有个更讲究的做法。对于用户自己编译、安装的第三方库标准推荐的位置其实不是/lib64而是/usr/local/include和/usr/local/lib或 /usr/local/lib64。为什么因为/lib64是系统自带库的地盘归包管理器比如yum、apt管辖。你手动塞进去的东西一没登记在案二容易跟系统基础库打架。等哪次系统升级或包管理操作一打扫你的库很可能就被误删或覆盖。放在/usr/local 下就清清爽爽跟系统的“亲儿子”们井水不犯河水。所以方案一虽然叫“安装”但装在哪还是有讲究的。图省事放/lib64能用但更规范的是放/usr/local。这就像租房直接睡桥洞也行但正经人还是租个公寓住。4.2 方案二在系统标准路径建立软链接不想动系统目录也不想搞复制粘贴那就在标准路径下挂个“快捷方式”。软链接这招干净又灵活。sudo ln -s /home/abc/code/lib/mylib/libmyc.so /lib64/libmyc.so这条命令干的事就是在/lib64里放了一个指向我们真实库文件的“指路牌”。操作系统扫描标准目录时撞见这个软链接就会顺着它的路径一路追到你用户目录下那个货真价实的.so文件。库还在老地方系统却能在标准路径里找到它。完美避开“复制文件”的笨重也绕开了“污染系统目录”的烦恼。4.3 方案三配置LD_LIBRARY_PATH环境变量操作系统在运行动态链接的程序时会专门去翻一个叫LD_LIBRARY_PATH的环境变量。我们要做的就是把动态库所在的路径追加到这个变量后面export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/home/abc/code/lib/mylib/注意那个$LD_LIBRARY_PATH这是把原来已有的值原样保留再往后面拼一段新路径。直接覆盖掉的话可能会把别的库路径弄丢别犯这个傻。不过这种export出来的配置是内存级的。终端一关或者新开一个窗口这段配置就烟消云散了。所以它适合临时调试、快速验证不适合当长久之计。4.4 方案四配置/etc/ld.so.conf.d/想一劳永逸这是系统级的永久方案也是工程里最正经的做法。Linux 允许我们在/etc/ld.so.conf.d/目录下自己创建一个专属的配置文件把动态库的搜索路径写进去。# 1. 在配置目录下建一个自己的配置文件 sudo vim /etc/ld.so.conf.d/my_test_lib.conf # 2. 把动态库所在的绝对路径填进去保存退出 /home/whb/code/lib/mylib/ # 3. 执行 ldconfig让系统重新加载动态库缓存配置 sudo ldconfig三步走完配置永久生效。之后再用ldd usercode一看路径已经稳稳匹配上了libmyc.so /home/zbc/Code/blog_Linux_file_4/User_Mr.Zc/lib/mylib/libmyc.so (0x00007f857122f000) libstdc.so.6 /lib64/libstdc.so.6 (0x00007f8570e00000) libm.so.6 /lib64/libm.so.6 (0x00007f857114b000) libgcc_s.so.1 /lib64/libgcc_s.so.1 (0x00007f857112b000) libc.so.6 /lib64/libc.so.6 (0x00007f8570c0c000) /lib64/ld-linux-x86-64.so.2 (0x00007f857123c000)看到那行libmyc.so 后面跟着我们自己的路径就说明动态链接器已经知道该去哪儿找它了。这个方案虽然比前三个多敲几条命令但胜在“一次配置终身受益”是真正适合生产环境的做法。四种方案从临时到永久从简单到规范拷贝最直接软链接最灵活环境变量最快速配置文件最持久。选哪个看你的场景调试时用方案三交付时上方案四准没错。五、动静态库混合链接与优先级在Linux世界里有个不成文的规矩系统里装的库大多数都优先提供动态版本。这不仅是为了省内存更是为了更新方便。5.1 gcc默认的链接行为gcc/g骨子里是个“动态库爱好者”。默认情况下它一门心思想链动态库。假设同一个路径下同时躺着同名的静态库和动态库libmyc.so和libmyc.a并排坐。你敲下编译命令不带任何特殊选项gcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc编完之后用file usercode看一眼你会毫不意外地发现生成的可执行程序是dynamically linked它选了动态库。这就是gcc的默认品味有动态就绝不碰静态。5.2 强制进行静态链接但如果你非要跟gcc拧着来就想把库死死焊进程序里也有办法。在编译命令的末尾加上一个 -staticgcc -o usercode usercode.c -I lib/include/ -L lib/mylib/ -lmyc -static这行命令的意思很明确把所有库都用静态版本链进去。不是光链libmyc而是程序依赖的每一个库C标准库也好数学库也好全都得是静态的。但-static有个很现实的约束既然你要“全静态”那系统中就必须存在每一个依赖库对应的.a版本。只要缺了一个比如C标准库的静态版没装编译立马报错一点情面不讲。那什么情况下gcc会“被迫”走静态链接呢只有当系统里只存在静态库、压根没有动态库的时候。没得选它才不情不愿地链静态版本。否则只要动态库还在它永远优先翻动态库的牌子。所以混合场景下的优先级再清晰不过默认动态强行-static才静态没动态可挑时才无奈静态。记住这条以后编译报错时你至少能先判断出是不是-static在跟缺失的静态库较劲。从ar打包归档到-fPIC 与 -shared的位置无关魔法从 -I、-L、-l的指路三件套到“编译过了、运行却 not found”的经典翻车从四种动态库加载方案到gcc骨子里对动态链接的偏爱这一篇我们把Linux动静态库从制作到交付再到排错完整走了一遍。几个关键认知值得在翻页前再钉一遍库的本质就是 .o 合集。静态库是.a归档动态库是.so共享对象。一个把代码打包进程序一个在运行时才把代码接进来。-L是给gcc看的不是给操作系统看的。编译时指的路运行时系统根本不认。想让系统找到库得让它进系统的“寻库朋友圈”拷路径、软链接、配环境变量、写配置文件四招任选。默认动态强静态才静态。gcc的天性是链动态库除非你祭出-static而且还得保证所有依赖库都有静态版本。不然缺一个编译就崩。静态库删了目录还能跑动态库删了库就挂。前者把行李打包带走后者把行李寄存机场。灵活和轻便总得选一样。搞懂了动静态库你手里就多了一把在真实项目里跟编译、链接、部署打交道的利器。以后不管是自己造轮子发给别人用还是接手别人甩过来的库心里都有底了。如果这篇文章对你有帮助别忘了点个赞、点个收藏、点个关注。你的每一次反馈都是我继续肝下一篇的最大动力。我们下篇见。
返回列表