尧图精选

Linux动态库与静态库封装实践:从编译到发布完整指南

🕒 发布时间:2026/10/1 3:32:50 📁 来源:尧图网络
1. 为什么我建议每个Linux开发者都亲手做一次库的封装先讲个真实经历。有次我负责的项目要交付一个算法SDK给合作方对方环境是CentOS 7.9我们开发机是Ubuntu 22.04编译器版本、glibc版本全都不一样。第一次交付我图省事直接把源码包发给对方让他们自己编结果对方编译环境缺了一堆依赖来回折腾了两天。第二次我学乖了在开发机上编好动态库和一个简单的demo连同头文件一起发过去对方拿到手一条命令跑通当天就集成完了。这个对比特别直观地说明了动态库和静态库存在的意义别人不关心你怎么实现的只关心怎么用起来最省事。很多入门Linux的朋友学到这里总是把动态库和静态库当成两个需要背诵的概念今天能区分明天就忘。这很正常因为如果只是为了应付面试你确实没必要深挖。但如果你真正写过一些稍微复杂点的C/C工程或者参与过那种需要给别人交付库文件的协作项目你就必须把这一块彻底搞明白。这篇文章不是把man手册翻译一遍而是按照我自己从能用到用得明白再到能给别人讲清楚的过程来写的。内容包括动态库和静态库在编译、链接、运行阶段的行为差异以及这些差异背后的真实原因为什么静态库链接时顺序错了会报undefined reference问题出在哪动态库的SONAME机制、rpath优先级、环境变量LD_LIBRARY_PATH到底怎么配合我实际踩过的坑符号冲突、依赖链断裂、搬服务器后跑不起来以及对应的排查思路一个完整的封装、发布、验证流程照着做就能交付给别人正常使用这篇文章适合这几类人准备把C/C项目发给别人编译的、自己在Linux上做过动态库但偶尔会遇到加载失败问题的、以及想系统理解编译链路的同学。只要你能跑通gcc hello.c -o hello后面的内容你都能跟上。2. 先搞清楚库的本质它不过是一堆编译后的函数在动手之前我们先把库这个词掰开揉碎。库的本质非常简单把一些编译好的二进制代码打包存放起来供其他程序在需要时调用。和我们自己写程序时的多个源文件没有本质区别只是这些源文件被提前编译好了不需要每次都用源码重新编译。假设你写了一个工具函数库包含add.c、sub.c、mul.c三个源文件。你自己用的时候可以直接把它们和主程序一起编译gcc main.c add.c sub.c mul.c -o app这时候如果你想把add.c、sub.c、mul.c分享给别人用你有两个选择把三个.c文件发过去让他们自己编译。这就是源码交付最透明但对方能看到你所有实现细节而且对方的编译环境必须能编译你的代码。自己先把这三个文件编译好打成一个包发过去对方直接链接这个包。这就是库交付对方不需要看到源码也不需要再编译其他文件。库交付做出来的东西就是静态库和动态库。这两种形式对应了两种不同的打包和使用策略对比维度静态库.a动态库.so文件后缀.aarchive.soshared object打包内容一堆.o目标文件的归档包位置无关的编译产物加符号表链接时行为直接把需要的代码复制进最终可执行文件只记录依赖关系运行时加载运行时可执行文件大小包含库代码较大不包含库代码较小部署环境要求单个可执行文件即能运行需要系统能找到对应的.so文件升级库代码必须重新链接生成新的可执行文件替换.so文件即可接口兼容前提下内存占用每个进程各有一份代码副本多个进程共享同一份代码段这张表只是一个粗印象真正的差异需要在编译、链接、运行三个阶段中分别体会。2.1 静态链接的完整过程静态库本质上是ar命令打包的一个归档文件。你可以把它理解成一个存放了多个.o文件的盒子。编译时链接器从盒子里挑选出确实被用到的目标文件把里面的代码真的复制进最终的可执行程序然后就不需要这个盒子了。打个比方静态链接就像你把朋友请到家里住。朋友住进来之后你家里多了个人但你不需要再依赖外面的任何东西。缺点是每请一个朋友你家里就多住一个人如果多个程序都静态链接了同一个库每个程序内部都有一份完整的库代码副本内存和磁盘空间都会重复占用。实操阶段我们用一个小例子来验证。先创建三个文件// add.c int add(int a, int b) { return a b; }// sub.c int sub(int a, int b) { return a - b; }// main.c #include stdio.h int add(int a, int b); int sub(int a, int b); int main() { printf(10 3 %d\n, add(10, 3)); printf(10 - 3 %d\n, sub(10, 3)); return 0; }编译并打包成静态库gcc -c add.c sub.c # 生成 add.o sub.o ar rcs libmymath.a add.o sub.o这里ar rcs中的r表示把文件插入归档c表示创建归档文件s表示生成符号索引这个符号索引在链接时很重要。然后用静态库链接主程序gcc main.c -L. -lmymath -o app_static-L.表示在当前目录寻找库文件-lmymath表示链接名为libmymath.a的文件——链接器搜索静态库时约定的命名规则就是libXXX.a所以-lmymath会自动去找libmymath.a。注意链接时用了-static选项可以强制所有库都采用静态方式但通常我们不需要这么做。上面的命令并未加-static链接器只在-L指定的路径中找到了libmymath.a于是用了这个静态库而系统自带的libc等库仍然选择了动态版本如果环境里有.so的话。如果你想完全静态链接才需要额外加-static。2.2 动态链接的完整过程动态库在编译阶段要加两个关键选项-fPIC和-shared。gcc -c -fPIC add.c sub.c gcc -shared -o libmymath.so add.o sub.o-fPIC生成位置无关代码Position Independent Code它保证了库中代码可以被加载到内存中的任意地址运行。这一点是动态库能否被多个进程共享的基础。如果没有这个选项加载器在装载库的时候就麻烦得多甚至无法在多个进程中共享同一份代码段。然后编译主程序gcc main.c -L. -lmymath -o app_dynamic这时app_dynamic可执行文件里并没有包含add和sub的代码它只是记录了一个我需要用到libmymath.so里面要有add和sub这两个符号的信息。运行的时候系统里的动态链接器ld-linux-x86-64.so.2运行时主要负责查找、加载动态库会去固定路径和LD_LIBRARY_PATH等路径中查找libmymath.so并加载。你可能会问既然app_dynamic链接时知道在-L.里找到了这个库为什么运行时不直接去.里找因为运行时的工作由动态链接器负责它默认只会搜索系统缓存、系统库目录和环境变量指定的目录而不会自动搜索当前目录。这就引出了运行时报错找不到.so的问题。3. 静态库与动态库的三大实操差异用一组实验证实理论的差异光靠背表格是不够的亲手做一组对照实验体会会完全不一样。3.1 实验一可执行文件体积与依赖关系用上面生成的app_static和app_dynamic做对比ls -lh app_static app_dynamic file app_static app_dynamic ldd app_dynamicfile命令会告诉你这是ELF格式的可执行文件但对静态链接的版本会明确标出statically linked动态版本会标出dynamically linked一眼就能分辨。看体积会发现app_static比app_dynamic大不少因为库代码被复制进去了。ldd app_dynamic会列出它依赖的动态库列表而ldd app_static则显示not a dynamic executable。这个实验能直观解释一个经典问题**为什么有时候我把自己编好的静态程序拷到别的机器上还能跑**答案很简单因为静态链接的可执行文件不需要依赖外部库如果完全没有动态链接所以部署时单文件拷贝即可。但代价是体积大而且如果库有安全补丁更新你也无法享受到必须重新编译。3.2 实验二修改静态库源码后老程序不会受影响我们做个假设add.c实现有bug比如整数溢出没做保护你修正了add.c并重新编译生成了新的libmymath.a。但是app_static在链接时已经把旧的add函数代码复制进去了所以不重新链接的话app_static跑的还是旧代码。这就是静态库的冻结特性。反过来看动态库如果你只重新编译libmymath.so然后把新的.so替换到原来的位置下次运行app_dynamic时它加载的就是新代码不需要重新链接主程序。这就是为什么动态库非常适合热升级。当然这个便利有个前提新库必须保持接口兼容。如果你改了函数签名或者改了结构体布局老程序在运行时调用就会出问题。动态库的接口兼容性是一条不可逾越的红线我们后面细说。3.3 实验三多个进程是否共享同一份代码动态库最核心的卖点是共享。开两个终端同时运行app_dynamic然后观察内存ps aux | grep app_dynamic cat /proc/PID/maps | grep mymath你会看到两个进程的地址空间中libmymath.so映射的起始地址不同但代码段对应的文件是同一个/path/to/libmymath.so。Linux内核会把同一文件映射到多个进程的物理内存中同一份页帧这就是代码共享的实际体现。如果这个库的体积很大比如1GB你跑10个进程不共享的话内存占用就是10GB共享之后可能只有1GB的物理内存被大家映射着用。这个差距在生产环境里是非常明显的也是服务器上尽量把公共功能做成动态库的根本原因。不过要注意这里的共享节省的是物理内存中的代码段每个进程仍然有自己的数据段全局变量、堆、栈这些是不共享的。3.4 静态库链接时的顺序坑也是面试常客静态库其实只是一个.o文件的归档集合链接器在解析符号时是从左到右扫描输入文件的。如果链接命令里先把静态库写在前面主程序写在后面就会因为符号尚未被引用而导致链接器认为库中的add、sub都是无用的最终出现undefined reference to add之类的报错。正确的顺序是主程序或目标文件在前静态库在后。而且如果libA.a依赖libB.a那么-lA必须写在-lB前面。我见过不少新手在Makefile里被这个问题折磨到怀疑人生实际上调整一下链接参数的顺序就好了。动态库没有这个顺序问题因为动态库的符号在最终生成可执行文件时以符号表未解析但允许延迟解析的方式处理。但你会遇到另一个问题运行时找不到或者找到的是错误路径下的同名库。4. 动态库的加载机制与Soname这是你必须理解的系统级知识这部分是很多人学了库的概念但依然会踩坑的地方。因为编译链接和运行时加载是两套有联系但不同的策略理解动态加载机制有助于你真正掌握动态库。4.1 动态链接器的搜索顺序当系统启动一个动态链接的可执行文件时由ld-linux.so负责加载器的工作。它查找依赖库的顺序大致可以这样概括可执行文件内记录的DT_RPATH如果存在且没有禁用环境变量LD_LIBRARY_PATH可执行文件内记录的DT_RUNPATH/etc/ld.so.cache缓存文件由ldconfig生成默认的系统库目录如/lib、/usr/lib这个顺序在不同发行版上会有些许区别比如有些系统不再推荐使用RPATH而使用RUNPATH但整体思想是一致的。其中LD_LIBRARY_PATH是最容易临时生效的办法也是很多开发机上的常用手段export LD_LIBRARY_PATH/home/user/project/lib:$LD_LIBRARY_PATH ./app_dynamic但生产环境不建议长期依赖环境变量因为环境变量是全局性的很容易造成我明明在这个项目里已经设置了路径为什么跑到那个项目里就加载了另一个路径的同名库这种混乱。4.2 SONAME机制为什么升级库时程序还能跑动态库文件名和库的真实名称并不一定是同一个。当一个程序链接某个动态库时链接器会把这个库的SONAMEShared Object Name记录下来而不一定记录全文件名。SONAME是在编译动态库时通过-Wl,-soname,libmymath.so.1设置的。这个机制的存在意义在于它允许你平滑地升级库文件程序运行时需要的是libmymath.so.1而你只是在这个名字背后替换不同的小版本.so文件。比如库文件的实际名为libmymath.so.1.0.0和libmymath.so.1.0.1但它们的SONAME都是libmymath.so.1那么程序无需重新编译就能使用新版库。实际操作中你会看到一个动态库目录下经常同时存在多个带版本号的软链接ls -l /usr/lib/libmymath.so*典型结构是libmymath.so - libmymath.so.1.0.0 libmymath.so.1 - libmymath.so.1.0.0链接时用-lmymath找到的是libmymath.so这个软链接而运行时系统根据SONAME找的是libmymath.so.1。如果我们直接修改或删除软链接就会破坏程序运行时的依赖解析。提示项目发布动态库时建议遵循实际文件名带完整版本号 不带版本号的软链接给编译用 带主版本号的软链接给运行时用这个惯例。这套约定不是可有可无的它决定了别人能不能顺手用你的库。4.3ldd、objdump、readelf三个排查命令的组合用法真正遇到动态库加载问题时我的排查流程一般是这样的ldd ./app_dynamicldd会列出所有依赖以及解析结果。如果某项显示not found说明动态链接器在搜索路径里没找到目标库。这时我会接着看readelf -d ./app_dynamic | grep -E NEEDED|RPATH|RUNPATHreadelf -d能直接看到可执行文件的动态段信息里面包括NEEDED条目依赖的SONAME、RPATH和RUNPATH这能从根本上告诉我链接时记录了什么、运行时打算去哪找。如果想进一步看某个动态库的导出符号用nm -D或objdump -Tnm -D ./libmymath.so左侧的T表示文本段导出符号U表示未定义符号W表示弱符号。排查符号找不到或符号版本冲突的时候这些信息非常关键。这套组合拳我基本每次遇到动态库问题都会走一遍已经成了肌肉记忆。5. 从编译到发布一次完整的动态库交付流程这一节我们把所有知识点串起来实际走一个写代码 - 编译 - 链接 - 发布 - 验证的完整流程。我会特意加入一些容易忽略但很重要的细节。5.1 目录结构规划我习惯按下面的结构组织一个库项目libmymath/ ├── include/ │ └── mymath.h ├── src/ │ ├── add.c │ ├── sub.c │ └── mul.c ├── build/ ├── dist/ └── test/ └── main.c目录分工include/放对外发布的头文件这是使用方的接口契约src/放实现代码不需要对外暴露build/是编译产物目录不干净时可以直接删掉重新构建dist/是最终交付目录包含lib和include两个子目录拿出去给别的项目用test/放测试程序用真实的调用场景来验证库的功能明确目录结构后后续的Makefile和发布动作会清晰很多。5.2 头文件的接口设计决定了库的兼容性库的头文件就是API的说明书。使用方即使不看源码只要看到头文件就能知道这个库提供了什么。接口设计要遵循几个原则函数命名尽量带前缀比如mm_add避免add这样容易和第三方库冲突的通用名对外暴露的结构体尽量向前向后兼容最好在末尾保留reserved字段不要轻易改变函数签名也不要改变结构体字段的顺序头文件最好加上extern C保护方便C程序直接使用// mymath.h #ifndef MYMATH_H #define MYMATH_H #ifdef __cplusplus extern C { #endif int mm_add(int a, int b); int mm_sub(int a, int b); #ifdef __cplusplus } #endif #endifextern C的作用很多新手不懂这里简单说一下C编译器会对函数名做name mangling名字修饰如果我头文件里不加这个保护C使用者调用mm_add时去库里找的符号就会是修饰后的名字和C编译出的_mm_add对不上。加了extern C之后C编译器会按C的规则来处理这个块里的函数名两者就能对上了。5.3 用Makefile管理编译、链接、安装下面这份Makefile我用了很多年精简后可直接套用CC gcc CFLAGS -Wall -Wextra -fPIC -Iinclude LDFLAGS -shared VERSION 1.0.0 SONAME libmymath.so.1 TARGET libmymath.so.$(VERSION) SRCS src/add.c src/sub.c src/mul.c OBJS $(SRCS:.c.o) all: $(TARGET) symlink $(TARGET): $(OBJS) $(CC) $(LDFLAGS) -Wl,-soname,$(SONAME) -o $ $^ symlink: ln -sf $(TARGET) $(SONAME) ln -sf $(TARGET) libmymath.so %.o: %.c $(CC) $(CFLAGS) -c $ -o $ test: all $(CC) test/main.c -Iinclude -L. -lmymath -o test/app_test LD_LIBRARY_PATH. ./test/app_test install: mkdir -p $(PREFIX)/lib $(PREFIX)/include cp $(TARGET) $(PREFIX)/lib/ ln -sf $(TARGET) $(PREFIX)/lib/$(SONAME) ln -sf $(TARGET) $(PREFIX)/lib/libmymath.so cp include/mymath.h $(PREFIX)/include/ clean: rm -f $(OBJS) $(TARGET) $(SONAME) libmymath.so test/app_test .PHONY: all test install clean几个关键点-fPIC必须放在编译目标文件的阶段不放在链接阶段-Wl,-soname,$(SONAME)是链接动态库时设置SONAME的选项这一步不能省先创建libmymath.so.1.0.0然后生成libmymath.so.1软链接和libmymath.so软链接test目标特意把LD_LIBRARY_PATH设成当前目录是为了模拟运行时查找过程提示LD_LIBRARY_PATH.的写法只在当前终端有效不会污染系统配置。测试时临时设置是安全的。5.4 生成.deb或.rpm包与直接拷贝的取舍对于正式的对外交付拷贝目录只适合内部试用。如果要给别人安装更专业的做法是打包成deb或rpm这样能处理头文件路径、ldconfig缓存、版本冲突等一系列问题。最小可行的deb包流程大概是mkdir -p dist/DEBIAN dist/usr/local/lib dist/usr/local/include cp libmymath.so.1.0.0 dist/usr/local/lib/ ln -s libmymath.so.1.0.0 dist/usr/local/lib/libmymath.so.1 ln -s libmymath.so.1.0.0 dist/usr/local/lib/libmymath.so cp include/mymath.h dist/usr/local/include/然后写一个dist/DEBIAN/control文件Package: libmymath Version: 1.0.0 Section: libs Priority: optional Architecture: amd64 Depends: libc6 Maintainer: yourname youexample.com Description: A simple math library for demonstration最后执行dpkg-deb --build --root-owner-group dist libmymath_1.0.0_amd64.deb装包后运行ldconfig更新缓存库就能被系统正常识别。rpm包的工具是rpmbuild步骤类似但语法不同这里不展开用到时可以按rpmbuild SPECS来写。5.5 对外交付时头文件、lib目录、文档三者缺一不可真正的库交付至少要包含三部分头文件使用方编译代码时引用这是接口契约二进制库文件可能是libmymath.so.1.0.0这份真实文件以及必要的软链接提供静态版就再加一个libmymath.a文档/README写清楚依赖环境、支持的系统版本、编译选项、API示例如果只给.so不给头文件对方就只能靠猜来调用你的函数这显然不行。如果给了头文件却不注明最低支持的glibc版本对方在较老的系统上编译时可能看到极其隐蔽的兼容性问题排查起来非常痛苦。6. 编译链接和使用过程中的高频踩坑与排查思路讲完正向流程这一节集中说出了问题怎么办。以下每个坑都是我或身边同事真实遇到过并花了不少时间排查的。6.1 链接时明明有库却报cannot find -lmymath这个报错发生在链接阶段意思是链接器搜索了-L指定的所有路径都没有找到libmymath.a或libmymath.so。可能原因按出现频率排序-L路径写错了或者库文件不在那个路径下库文件名不符合libXXX.so或libXXX.a的命名规则比如直接叫mymath.so就找不到库文件权限不对链接器无法读取静态库的符号索引损坏需要用ar s libmymath.a修复我见过有人为了让库被找到直接改了系统/usr/lib下的文件结果弄得系统环境一团糟。正确的做法是在自己项目的构建目录中维护好-L路径不要轻易动系统目录。6.2 编译链接成功但运行时提示cannot open shared object file这个报错发生在运行时动态链接器阶段原因可能有.so文件不在程序所记录的SONAME对应的路径中你用-L指定了路径但-L只对链接期生效对运行期无效设置了LD_LIBRARY_PATH但没export或者当前shell已经缓存了旧环境变量快速临时解法export LD_LIBRARY_PATH/path/to/your/lib:$LD_LIBRARY_PATH ./app_dynamic长期解法是在库安装后运行ldconfig或者用rpath将路径写入可执行文件。后者的编译方式是gcc main.c -L. -lmymath -Wl,-rpath,$ORIGIN/libs -o app_dynamic$ORIGIN在运行时会被动态链接器替换为可执行文件所在的目录这样库放在可执行文件旁边的libs/下就能稳定找到。注意$ORIGIN如果在Makefile里写可能需要写成$$ORIGIN才能让Shell不把它当变量展开。这是一个特别容易踩的小坑。6.3 符号冲突两个动态库都导出了同名函数这是动态库最隐蔽的坑之一。假设程序链接了libA.so和libB.so这两个库内部都定义了一个全局函数helper()。那么程序运行时调用helper()的代码可能解析到libA里的实现也可能解析到libB里的实现具体取决于符号解析顺序甚至可能产生难以复现的诡异行为。排查手段用nm -D libA.so | grep T helper确认两侧都有同名导出符号用LD_DEBUGbindings运行程序查看符号绑定到哪个库必要时在链接参数中使用-Wl,--exclude-libs,ALL或者让第三方库的内部符号变成局部符号从源头避免暴露如果是你自己写的库有个非常好的习惯是所有非对外函数都加static修饰或者给它们起一个足够独特的前缀避免和第三方库的同名函数撞车。6.4undefined reference to到底是哪一步的问题很多人一看到undefined reference to xxx就认为是库没找到但实际上是符号没有找到和库文件没找到是两个不同层次的问题。如果是链接时报undefined reference说明链接器扫描过的所有库和目标文件里都没有找到该符号的定义。可能该函数确实没实现也可能链接参数顺序有问题也可能库文件里函数的名称被C name mangling过即你用nm看到的符号名并不是你源码里的函数名如果是运行时才报symbol lookup error说明链接期找到了符号所在库但加载时那个库的版本不匹配或符号被遮蔽了排查顺序建议先用nm确认库中是否有该符号如果符号存在确认链接顺序如果顺序正确确认库文件里符号的命名是否和你调用的名字完全一致尤其注意大小写和下划线如果以上都正常把完整编译命令行贴出来逐项检查-I、-L、-l的作用范围6.5libc.so.6: version GLIBC_XX not found这个报错最常见于把高版本系统上编译的动态库拷贝到低版本系统运行。因为动态库在编译时记录了对glibc版本的最低要求目标机器上glibc版本太低就无法加载。这个问题的本质是动态编译的产物天然带有对运行环境库版本的要求。想解决通常有两条路在更老的环境下重新编译尤其用老版本glibc的发行版或容器环境来构建尽量静态链接那些对版本敏感的基础库或者干脆用容器、虚拟机来保证运行环境一致这其实也是为什么很多开源项目坚持用manylinux这类镜像来构建发布版就是为了保证编译出的.so能在尽可能老的glibc环境下运行。6.6ldconfig的缓存改了库路径后没生效如果你把新编译的动态库放到了/usr/local/lib下然后运行程序还是加载到旧的版本多半是因为系统缓存里还保留着旧库的信息或者/etc/ld.so.conf根本没包含/usr/local/lib。正确做法# 先看配置里有没有包含路径 cat /etc/ld.so.conf # 在 /etc/ld.so.conf.d/ 下新建一个 .conf 文件 echo /usr/local/lib | sudo tee /etc/ld.so.conf.d/local-lib.conf sudo ldconfig ldconfig -p | grep mymathldconfig -p可以看到当前缓存里有哪些库和它们的实际路径这个命令在调试运行时为什么加载旧库时非常有用。7. 进阶设计动态库的隐藏符号、依赖裁剪与冲突规避到了最后一个部分聊一点进阶策略。这些内容不一定天天用但一旦你需要做底层基础库或者给别人做SDK就是决定成败的细节。7.1 用-fvisibilityhidden控制导出符号默认情况下动态库中的所有非static全局符号都会被导出。这个默认全导出看似方便其实带来了两个问题外部程序可以误用你不想公开的符号多个库之间出现同名符号冲突的概率大大增加解决方案是编译时加上-fvisibilityhidden然后只有在头文件中显式标注了__attribute__((visibility(default)))的函数才会被导出__attribute__((visibility(default))) int mm_add(int a, int b);也可以定义一个宏来简化#define API __attribute__((visibility(default))) API int mm_add(int a, int b);编译时gcc -c -fPIC -fvisibilityhidden add.c sub.c gcc -shared -o libmymath.so add.o sub.o然后对比nm -D的导出符号数量你会看到只有显式标记的符号被导出。这在构建高稳定的SDK时几乎是必须的做法维护成本很低收益却很大。7.2 动态库的依赖链条也会引发搬运后失效动态库本身就依赖其他动态库比如libmymath.so可能依赖libssl.so。当你想把libmymath.so拷到别的机器上运行时别忘了它的依赖链也必须存在。验证依赖链的标准命令ldd libmymath.so输出的每一行都是一个直接依赖。如果某一项显示not found那这个库在目标机器上就不完整。这种问题常发生在从开发机直接scp库到生产机的场景中所以交付文档里必须明确写出依赖清单。7.3dlopen与运行时插件化加载除了编译时链接动态库Linux还支持在程序运行中通过dlopen、dlsym手动加载库并获取函数地址。这种模式非常适合做插件系统。#include dlfcn.h #include stdio.h int main() { void *handle dlopen(./libmymath.so, RTLD_LAZY); if (!handle) { fprintf(stderr, dlopen error: %s\n, dlerror()); return 1; } int (*mm_add)(int, int) (int (*)(int, int))dlsym(handle, mm_add); if (!mm_add) { fprintf(stderr, dlsym error: %s\n, dlerror()); return 1; } printf(result %d\n, mm_add(2, 3)); dlclose(handle); return 0; }编译时加-ldlgcc plugin_test.c -ldl -o plugin_test这个模式的好处是程序启动时不需要解析所有依赖某个插件加载失败也不影响主程序运行插件甚至可以独立升级替换非常适合框架模块的架构。但代价是必须处理好符号查找时的错误检查和资源释放否则内存泄漏和段错误都可能找上门。7.4 静态库和动态库混用时的注意点一个工程完全可以同时使用静态库和动态库我自己也经常这么干。比如把核心算法编译成静态库嵌入主程序把可热插拔的协议组件编译成动态库插件。混用时最容易忽略的是静态链接的代码不会自动带上它自己依赖的动态库。举个例子如果你的libmymath.a内部依赖了libssl.a那么主程序链接时如果只写了-lmymath而不写-lssl就会因为libmymath.a内部的符号无法解析而失败。所以静态库在使用时它依赖的其他库也必须被显式列出且顺序要符合依赖关系。另一个需要注意的细节是同一个符号如果可以同时从静态库和动态库中解析通常最终选中的是动态库。这种规则在处理一些细节时会让人困惑建议在项目文档里写清楚依赖关系避免靠猜测和玄学调参。8. 我在实际项目中的几条取舍标准文章最后直接分享几条我自己的经验判断。这些不是标准答案但都是我经过几次教训后沉淀下来的原则。什么时候优先用静态库程序需要作为单个二进制分发不想管目标机器上的库依赖运行环境不可控目标机器可能缺失动态库或版本不匹配对启动性能有要求或为了调试时符号信息更完整什么时候优先用动态库多个程序共享同一份公共代码内存占用要控制需要在不重新编译主程序的前提下更新库的实现团队开发时模块边界清晰核心算法和外壳程序独立演进**安全提醒**如果你从不可信的来源下载了预编译的动态库先不要急着ldd运行。用readelf -d和nm -D看看它导出的符号再检查它依赖了哪些库是否存在可疑的路径。动态库里是可以执行任意代码的加载未知来源的.so和运行未知来源的程序风险是一样的。**关于测试**写完库之后至少要测试两种使用方式一种是链接时直接使用-l方式另一种是运行时dlopen方式。这两种方式因为解析路径和时机不同常常暴露出不同的bug。我建议每个库项目都留一个小的测试程序覆盖至少这两个场景会省很多后患。说实话动态库和静态库的知识点不算多但它在实际工程里出现的问题往往很绕因为它横跨编译期、链接期、加载期三个阶段任何一个环节的概念理解不到位都会觉得系统在抽风。但只要把三个阶段各自负责什么、谁决定找哪个文件、符号是怎么连接起来的这几个核心问题想明白了后面所有报错都有清晰的排查路径。按这个思路过一遍自己的项目你也会对Linux程序是怎么跑起来的有一个更完整的认知。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →