深入 Linux 动态链接:so 库构建与运行逻辑(转)

有不少开发者仅仅知道基础的编译命令,但是却不清晰动态链接器到底是如何解析符号、程序在运行的时候究竟是如何加载so文件的。当碰到找不到库、版本冲突、符号未定义这类报错情况的时候,仅仅只能胡乱地去修改环境变量,而无法从根本上去查找问题所在。本文从动态链接的底层逻辑着手,对so库完整的构建流程进行分解,搞清楚编译参数、符号导出、版本控制等关键要点,从而补上只懂得操作却不懂得原理的知识方面的缺陷。

动态链接并非仅仅是简单的文件加载那么简单。在编译的阶段,存在着重定位的处理情况。在运行的时候,ld链接器得去检索库路径、匹配符号表的。整个整套的流程有着好多容易被忽略掉的细节。这篇文章结合着gcc编译的实际操作以及ldd工具依赖的分析,完完整整地梳理了so库生成、部署、调用的全链路。同时针对开发当中高频出现的踩坑点给出相应的解决思路。

一、初识 Linux动态链接与 so库

1.1 动态链接是什么

在Linux系统之中,动态链接乃是程序于运行的时候方才加载以及链接所需要的库文件的一种机制。打个比方来说,将动态链接想象成是一种灵活的合作伙伴关系。当你编写程序的时候,并不需要把所有的功能代码全都一股脑地塞进程序里面。如同组建项目团队一样,不必在一开始的时候把所有有可能会用到的人才都给招揽过来。动态链接可以使得你在程序运行的时候,去邀请所需要的库文件来提供特定的功能。

静态链接那可是大不一样。在程序进行编译的时候,把所有被用到的库代码直接就嵌入到可执行文件里头。这就好比一开始把项目有可能会用到的所有人才都给招募进来一样,不管以后用不用得到。这么做项目在运行的时候就不用再到外面去找人帮忙,可是程序就会变得特别臃肿。而且要是某一个功能需要进行修改的话,整个项目都得重新进行调整,也就是说要重新对程序进行编译。

动态链接具备着不少的优势。其一它可以节省磁盘空间以及内存。众多的程序都依赖着同一个库,比如说很多的C语言程序都依赖于C标准库libc.so(libc.so)。在动态链接的机制状况之下,这些程序在运行的时候能够共享内存当中同一份libc.so(libc.so)的副本,而不是每一个程序都各自携带一份,这样子就大大的节省了内存资源。

其次,动态链接可以使得程序的更新以及维护变得更为简易。倘若库文件发生了更新,只要接口依旧还是老模样,依赖它的程序不需要重新去进行编译就能够运用上新的库的功能。打个比方来说,项目团队当中有一个成员的技能得到了提升,只要他和团队其他成员的协作方式(也就是接口)没有发生改变,整个项目不用兴师动众就能够享受到他新技能所带来的益处。

1.2 so 库是什么?

so库,便是共享对象文件,也就是Shared Object。它乃是Linux系统当中动态链接库的文件格式,它的文件扩展名就是.so。so库是不能去执行的二进制程序文件,这里面有着能够被多个程序所共享的代码以及数据。打个比方说so库就好似一个工具盒,这里面装着各种各样的工具,也就是函数和数据,不同的程序在运行的时候,都能够从这个工具盒里面拿出自己想要用的工具来进行使用。

在开展图形界面程序的开发工作的时候,常常会用到GTK库。此库是以so库这样的形式而存在着的,例如libgtk.so。当程序进行运行的时候,会依照需求来加载libgtk.so库。从该库之中去获取比如绘制窗口、处理用户输入等功能的代码以及数据。如此一来开发者就不需要自己从最初开始去实现繁杂的图形界面功能。

倘若你对于Windows系统是颇为熟悉的,那么就可以把so库类比成Windows里面的.dll文件(也就是动态链接库)。它们所起到的作用大体上是相近的,全都是达成代码共享以及复用的途径。但是在不一样的操作系统之中,它们具备着不一样的文件格式以及管理的方式 。

1.3 静态库与 so 动态库的区别

静态库以及 so 库(也就是动态库)乃是程序开发过程当中两种十分关键的库文件的形式。它们在核心方面的差别主要体现在链接的时间、运行时的依赖状况、文件的大小、内存的占用以及迭代更新的成本这类方面。并且它们所适用的业务场景也全然不同。

从链接的时机来讲,静态库在程序进行编译链接的时候,就会被完完整整地复制到可执行文件里边去。程序所用到的所有函数的代码,在可执行文件生成的时候就全都固化下来。而so动态库可就不一样,程序编译的阶段仅仅只会记录下对这个库的引用方面的信息,不会把库的代码给嵌入到文件的内部,只有在程序实际运行的时候,才会由操作系统动态地加载对应的so库文件。

在运行依赖方面,二者之间的差异十分明显。经过静态库进行编译从而生成的可执行文件具备较强的独立性。由于库代码完整地融合到程序内部之中,在运行的时候不需要去依赖外部的库文件。但是对于依赖so动态库的程序来讲,运行的环境得要有相对应的so库文件,并且库的路径还得配置正确,要不然程序就会直接启动报错而无法运行。比如说运行依赖libmysql.so的数据库程序,要是系统缺少了这个库文件,程序就会出现运行失败的情况。

在文件体积这一方面来讲,静态链接的程序从整体来看体积是比较大的。这是由于静态库的所有代码都会被打包嵌入到可执行文件里面。哪怕是很简单的C语言程序,经过静态编译之后体积能够达到几十KB甚至更大。那动态链接的情况又是如何的,它仅仅保留库的引用信息,并不包含库的实际代码。这样生成的可执行文件体积就很小,一般就只有几KB。库文件是单独地存放在系统之中的,这样就有效地对程序本体进行了精简。

在内存占用的效率这一方面来讲,so动态库是有着绝对的优势的。当多个进程同时去调用同一个静态库的时候,每一个进程都会在内存里面弄出一份属于自己独立的库的副本,这样子就会浪费掉好多好多的内存资源。而当多个进程使用同一个so动态库的时候,操作系统就只会在内存当中加载一份库的代码段,所有的进程一块儿来共享着调用它,这样就大大的节省了物理内存。我们日常运行的各种各样依赖libc.so的程序,全都是通过动态链接的这种方式来达成内存共享的。

版本进行更新以及迭代的成本这一方面,两种库在维护难度上存在着非常大的差别。倘若说静态库需要去修复bug、添加新的功能,那么所有调用这个静态库的程序,都得重新进行编译链接,才可以去适配新版本的库。而so动态库,只要对外的接口没有发生变化,就不用去改动原有的程序,直接更换一个新的so库文件,所有依赖这个库的程序就会自动去适配新的功能,不需要重新进行编译。常见的软件插件系统大多数都是用so库来进行实现的,就是利用这个特点,仅仅更换插件库文件就能够完成功能的更新,不需要去修改主程序。

从实际的开发场景来进行考量,这两种库都有着各自与之相适配的场景。对于小型的项目,还有那一些对于程序的独立性、稳定性以及启动速度有着比较高要求的程序而言,是更加适合采用静态库的。而对于大型的项目,或者是需要频繁地进行迭代更新的程序,又或者是好几个程序需要去共享同一套代码的场景来说,那么动态库的优势就会显得更加地明显,就成了更为优选的选择。

二、so 库构建流程与要点

2.1 编写共享库源码

假设我们要构建一个简单的数学运算 so 库,先编写 C 源文件和对应的头文件。创建一个名为 math_lib.c 的源文件,实现两个简单的数学运算函数 add 和 multiply:

复制代码
// math_lib.c
#include
 "math_lib.h"
int add(int a, int b) {
    return a + b;
}
int multiply(int a, int b) {
    return a * b;
}

然后创建头文件 math_lib.h,定义函数接口,供其他程序调用:

复制代码
// math_lib.h
#ifndef
 MATH_LIB_H
#define
 MATH_LIB_H
int add(int a, int b);
int multiply(int a, int b);
#endif

在这个头文件里,通过#ifndef、#define 和#endif 预处理指令,防止头文件被重复包含,这在大型项目中是很重要的,可以避免很多重复定义的错误 。

2.2 编译生成共享库

使用 gcc 编译命令将上述源码编译为 so 库文件。首先,要启用-fPIC(Position - Independent Code,生成位置无关代码)选项,这是因为动态库在运行时可能会被加载到内存的不同位置,位置无关代码可以保证库代码在任何加载地址都能正确运行。还要使用-shared 选项来指定生成动态库 。可以分两步进行编译:

第一步,将 math_lib.c 编译为目标文件 math_lib.o,命令如下:

复制代码
gcc -fPIC -c math_lib.c -o math_lib.o

这里边的 -c 这个选项所代表的意思就是仅仅去进行编译以及汇编的操作,而不开展链接的操作。所生成的 math_lib.o 乃是一个目标文件,在这个目标文件里边存在着机器代码,可是还不可以直接就拿去运行。

第二步,将目标文件 math_lib.o 链接成共享库文件 libmath_lib.so,命令如下:

复制代码
gcc -shared -o libmath_lib.so math_lib.o

-o 选项用于指定输出文件名为 libmath_lib.so 。

也可以把这两步合并成一条命令:

复制代码
gcc -fPIC -shared -o libmath_lib.so math_lib.c

直接从源文件来生成共享库文件,如此会更加简洁且方便。所生成的libmath_lib.so就是我们构建完成好的动态链接库,它可以被其他的程序进行共享使用。在Linux系统当中,动态库的文件名一般是从lib开头,以.so结尾,中间的部分是库的名称,这里就是math_lib。

2.3 编译主程序并链接共享库

接下来编写一个主程序 main.c 来调用 libmath_lib.so 库中的函数:

复制代码
// main.c
#include <stdio.h>
#include "math_lib.h"
int main() {
    printf("5 + 3 = %d\n", add(5, 3));
    printf("5 * 3 = %d\n", add(5, 3));
    return 0;
}

gcc main.c -I. -L. -lmath_lib -o main

这条命令会生成一个名为 main 的可执行文件,它链接了 libmath_lib.so 库,能够调用其中的 add 和 multiply 函数 。

2.4 运行程序与库路径配置

当我们尝试运行生成的可执行文件 main 时,可能会遇到这样的错误:

复制代码
./main: error while loading shared libraries: 
libmath_lib.so: cannot open shared object 
file: No such file or directory

这是因为动态链接器在默认路径中找不到 libmath_lib.so 库文件 。

下面介绍几种解决方法:

1.将库复制到系统目录:可以将 libmath_lib.so 复制到系统默认搜索的库目录,比如/usr/lib/或/usr/local/lib/,命令如下:

复制代码
sudo cp libmath_lib.so /usr/lib/

或者

复制代码
sudo cp libmath_lib.so /usr/local/lib/

这种方法虽然简单直接,但不推荐经常使用,因为这样会污染系统目录,而且可能会导致权限问题,同时也不利于库的版本管理 。

  1. 设置 LD_LIBRARY_PATH 环境变量:可以通过设置 LD_LIBRARY_PATH 环境变量来指定动态链接器搜索库文件的路径。比如,假设 libmath_lib.so 在当前目录,执行以下命令:

    export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH

其中,:是路径分隔符,. 表示当前目录,$LD_LIBRARY_PATH 表示原来的环境变量值,这样就把当前目录添加到了搜索路径中 。这种方法的优点是简单灵活,不需要修改系统配置,但缺点是只对当前终端会话有效,关闭终端后设置就失效了,而且如果在不同的环境中运行程序,可能需要频繁修改这个环境变量 。

  1. 使用 ldconfig 配置系统库路径:这是一种更推荐的长期使用的方法 。首先,将 libmath_lib.so 复制到一个自定义的目录,比如/usr/local/mylibs/(需要提前创建这个目录):

    sudo mkdir -p /usr/local/mylibs/
    sudo cp libmath_lib.so /usr/local/mylibs/

然后,编辑/etc/ld.so.conf 文件(需要管理员权限),在文件中添加一行:

复制代码
/usr/local/mylibs/

保存文件后,执行 ldconfig 命令,它会在默认搜寻目录/lib 和/usr/lib 以及/etc/ld.so.conf 内所列的目录下,搜索出可共享的动态链接库,进而创建出动态装入程序 ld.so 所需的连接和缓存文件 。执行命令如下:

复制代码
sudo ldconfig

ldconfig通常在系统启动的时候运行。倘若用户安装了新的动态库,手动去运行它可以使得系统识别新的库。这种方法的好处是一劳永逸,系统会记住这个库的路径,所有的用户都能够使用,而且对于程序的运行环境没有额外的限制 。

在完成了这些相关的配置之后,再去运行 main 这个程序,在这个时候就能够正常地去调用 libmath_lib.so 这个库里面的函数,然后就可以输出正确的结果 。

三、so 库运行逻辑深度剖析

3.1 编译时:记录依赖信息

在对程序进行编译并且使用动态库的时候,编译器并不会把动态库实际的代码给嵌入到可执行文件当中。就比如说之前的那个libmath_lib.so库以及调用它的main.c程序,当用gcc -o main main.c -L. -lmath_lib这个命令来编译main.c的时候,编译器就会在生成的可执行文件main里面记录下对libmath_lib.so库的依赖方面的信息。而这些信息主要就是有库名还有可能的路径等等,就好像在可执行文件里面留下一些线索一样,让程序在运行的时候能够知道到哪里去寻找这个库 。

复制代码
# 1. 编写动态库头文件 math_lib.h
cat > math_lib.h << EOF
#ifndef MATH_LIB_H
#define MATH_LIB_H
int add(int a, int b);
int multiply(int a, int b);
#endif
EOF
# 2. 编写动态库实现 math_lib.c
cat > math_lib.c << EOF
#include "math_lib.h"
int add(int a, int b)
{
    return a + b;
}
int multiply(int a, int b)
{
    return a * b;
}
EOF
# 3. 编译生成位置无关代码的动态库 libmath_lib.so
gcc -fPIC -shared -o libmath_lib.so math_lib.c
# 4. 编写主程序 main.c
cat > main.c << EOF
#include <stdio.h>
#include "math_lib.h"
int main()
{
    printf("相加结果:%d\n", add(2, 8));
    printf("相乘结果:%d\n", multiply(3, 6));
    return 0;
}
EOF
# 5. 链接可执行文件,仅记录动态库依赖,不拷贝库代码
gcc -o main main.c -L. -lmath_lib
# 6. 查看可执行文件内记录的动态库依赖线索
readelf -d main

运用readelf -d命令就可以去查看可执行文件所记录的依赖信息。去执行readelf -d main这个操作,输出的结果里面会有比如NEEDED Shared library: libmath_lib.so这样的条目,这样就明明白白地显示出main程序是依赖libmath_lib.so库的。

编译器这样去做是存在一定缘由的。其目的乃是为了使得可执行文件能够变得简洁一些,同时达成代码共享的这样一种情况。要是每一个使用动态库的程序都把库代码给包含进去的话,那么可执行文件的体积就会大幅度地变大,而且还会违背动态库共享代码的原本意图。

3.2 运行时:动态链接器登场

程序开启运行之际,动态链接器便开始发挥起至关重要的作用。在Linux系统当中,动态链接器通常就是ld.so或者ld-linux.so。它如同一个后勤工作人员似的,专门在程序运行的时候去寻觅并且准备好所需要的动态库。

动态链接器会根据可执行文件中记录的依赖信息,在系统指定的路径或者用户设定的路径中查找相应的动态库文件。比如对于 main 程序,动态链接器会去寻找 libmath_lib.so 库。如果直接执行./main,未配置库路径时会报错:

复制代码
./main
# 报错信息:error while loading shared libraries: 
libmath_lib.so: cannot open shared object 
file: No such file or directory

如果在当前目录(通过 LD_LIBRARY_PATH 环境变量设置或者其他方式指定)或者系统默认的库搜索路径中找到了 libmath_lib.so,动态链接器就会把这个库加载到内存中 。

复制代码
# 配置临时库路径后正常运行
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main

在被加载到内存之后,动态链接器得去做一个重要的事情,那就是把动态库当中的符号(像是函数、变量相关的)和可执行文件里边的符号引用给绑定到一块儿。举个例子来说吧,main程序调用了libmath_lib.so里边的add和multiply函数,这时候动态链接器就得去找到libmath_lib.so中这两个函数实际的内存地址,然后把main程序里对这两个函数的调用指令和那个内存地址给关联起来。这么一来main程序在执行到调用函数的语句的时候,就能够精准地去定位并且执行动态库里对应的函数,从而让程序能够正常地运行起来。

3.3 动态库的搜索路径顺序

动态库的搜索路径遵循一定的顺序,这对于程序能否正确找到所需的动态库至关重要 。

(1)LD_LIBRARY_PATH 环境变量:

这乃是用户可以进行设置的一个环境变量。其是用于指定动态库的搜索路径。比如说之前我们设置export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH,就是将当前目录添加到搜索路径里头。程序在运行的时候,动态链接器会先在LD_LIBRARY_PATH所指定的路径当中去寻找动态库。

复制代码
# 同时指定多个自定义库目录,冒号分隔
export LD_LIBRARY_PATH=/usr/mylib:./:$LD_LIBRARY_PATH
./main

这个环境变量的优点是灵活,可以根据不同的运行场景快速指定库的搜索路径,但它只对当前终端会话有效,关闭终端后设置就失效了 。

(2)/etc/ld.so.cache 缓存文件:

这是一个由ldconfig命令所生成的缓存文件。在这个缓存文件之中存放着系统里所有能够使用的共享库的路径以及名字这类信息。当动态链接器在LD_LIBRARY_PATH里没有找寻到所需要的动态库的时候,就会去到这个缓存文件里边去进行查找。

ldconfig命令会按照一定的周期去扫描系统当中指定路径下的共享库文件,很多路径一般是在/etc/ld.so.conf以及/etc/ld.so.conf.d/目录下的配置文件里面所指定的。之后它会对/etc/ld.so.cache文件进行更新。这么做可以使得动态库的查找速度变得更快。举例来说要是我们安装了一个新的动态库,还想要系统能够迅速地找到它,那么就可以运行sudo ldconfig命令来对缓存进行更新。

复制代码
# 1. 创建自定义库配置文件
sudo echo "/home/test/so_dir" > /etc/ld.so.conf.d/math_lib.conf
# 2. 更新系统库缓存
sudo ldconfig
# 3. 检索缓存中是否存在目标库
ldconfig -p | grep libmath_lib.so

**(3)默认路径:**要是在前面的那两个地方都寻觅不到动态库的话,动态链接器就会前往默认的路径去进行查找。常见的默认路径有/lib以及/usr/lib。这些路径是系统所默认设置的,在里面存储着不少系统级别的以及常用的动态库。比如说C标准库shturl一般就在/lib或者/usr/lib这个目录里面。

要去知道动态库搜索路径的先后顺序情况。这可以助力我们在开展开发以及进行程序部署的过程之中,精准地对库的路径进行配置。如此一来便能够解决有可能出现的找不到动态库这类的错误状况。

四、so 库手动加载原理与实战

4.1 动态加载库的函数

在 Linux 系统中,实现运行时动态加载 so 库主要依靠 dlopen、dlsym 和 dlclose 这几个函数,它们都定义在 <dlfcn.h> 头文件中 。

(1)dlopen 函数------用于打开动态库文件,其函数的原型乃是 void *dlopen(const char *filename, int flags)。filename这个参数是动态库的路径,能够是绝对路径,例如/usr/lib/libm.so这般。也能够是相对路径,比方说./libmath_lib.so此等情况。flags参数属于加载标志,常见的有着RTLD_LAZY以及RTLD_NOW。

RTLD_LAZY乃是延迟绑定之情况,也就是程序在实际调用动态库当中的函数的时候才去解析符号地址,如此这般能够让程序的启动速度得以加快。RTLD_NOW则是立即绑定,在调用dlopen的时候就对动态库里所有未定义的符号地址进行解析。要是dlopen成功地打开了动态库,就会返回一个指向该库的句柄,此句柄为void*类型,后续的操作会用到这个句柄。要是失败了就返回NULL,能够通过dlerror函数来获取错误信息 。

复制代码
// dlopen 使用示例
void *handle = dlopen("./libmath_lib.so", RTLD_LAZY);
if (!handle) {
    printf("dlopen failed: %s\n", dlerror());
    return -1;
}

(2)dlsym 函数------它的作用是获取动态库中某个符号(函数或变量)的地址,函数的原型乃是 void *dlsym(void *handle, const char *symbol)。handle为dlopen所返回的动态库的句柄也。symbol则是要获取的符号名称也,且是以字符串形式来表示的。

倘若我们想要获取前面所构建的 libmath_lib.so 库当中的 add 函数的地址,那么可以运用 dlsym 函数。dlsym 函数会返回一个指向该符号的指针,此指针是 void * 类型的。在使用的时候需要依据实际的符号类型来进行强制类型转换。要是获取符号出现失败的情况,dlsym 会返回 NULL,也可以通过 dlerror 来获取错误的缘由 。

复制代码
// dlsym 使用示例
typedef int (*add_t)(int, int);
add_t add = (add_t)dlsym(handle, "add");
const char *err = dlerror();
if (err) {
    printf("dlsym failed: %s\n", err);
}

(3)dlclose 函数------用于关闭动态库,减少动态库的引用计数,函数的原型乃是int dlclose(void *handle)。handle是之前经过dlopen所返回的动态库的句柄。当某一个动态库的引用计数变成0的时候,系统就会将它从内存之中给卸载掉。

dlclose要是执行成功的话就返回0,要是失败的话就返回非零的值。当程序不再需要使用某个动态库的时候,及时去调用dlclose是良好的编程习惯。及时调用dlclose能够释放内存资源,能够避免像内存泄漏这类问题的发生。

复制代码
// dlclose 使用示例
int ret = dlclose(handle);
if (ret != 0) {
    printf("dlclose failed: %s\n", dlerror());
}

(4)dlerror 函数------这是一个用于获取错误信息的辅助函数

函数的原型乃是char *dlerror(void)。当在调用dlopen、dlsym或者dlclose等这类函数之后要是操作出现了失败的情况,那么就可以去调用dlerror来获取具体的错误描述的信息。dlerror会返回一个字符串,这个字符串所描述的是最近一次动态链接相关操作的错误情况。

当dlopen无法找到指定的动态库文件的时候,dlerror返回的字符串有可能是"没有这样的文件或目录"。每一次调用dlerror之后,错误的信息就会被清除掉。所以要是想要保存错误信息的话,就必须得及时去处理返回的那一个字符串。

复制代码
// dlerror 通用捕获示例
char *err_info = dlerror();
if (err_info != NULL) {
    printf("link error: %s\n", err_info);
}

4.2 动态加载 so 库 代码实战演示

下面通过一段具体的代码示例,来展示如何使用这些函数实现动态加载 so 库 。假设我们还是使用之前构建的 libmath_lib.so 库,编写一个新的主程序 dynamic_load.c:

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
intmain() {
    void *handle;
    int (*add_func)(int, int);
    int (*multiply_func)(int, int);
    char *error;
    // 打开动态库
    handle = dlopen("./libmath_lib.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen error: %s\n", dlerror());
        return 1;
    }
    // 清除之前的错误信息
    dlerror();
    // 获取 add 函数地址
    add_func = (int (*)(int, int))dlsym(handle, "add");
    if ((error = dlerror()) != NULL) {
        fprintf(stderr, "dlsym error for add: %s\n", error);
        dlclose(handle);
        return 1;
    }
    // 获取 multiply 函数地址
    multiply_func = (int (*)(int, int))dlsym(handle, "multiply");
    if ((error = dlerror()) != NULL) {
        fprintf(stderr, "dlsym error for multiply: %s\n", error);
        dlclose(handle);
        return 1;
    }
    // 调用 add 函数
    int result_add = add_func(5, 3);
    printf("5 + 3 = %d\n", result_add);
    // 调用 multiply 函数
    int result_multiply = multiply_func(5, 3);
    printf("5 * 3 = %d\n", result_multiply);
    // 关闭动态库
    if (dlclose(handle) != 0) {
        fprintf(stderr, "dlclose error: %s\n", dlerror());
        return 1;
    }
    return 0;
}
  1. 打开动态库:handle = dlopen("./libmath_lib.so", RTLD_LAZY);使用 dlopen 函数尝试打开当前目录下的 libmath_lib.so 库,采用延迟绑定(RTLD_LAZY)方式 。如果打开失败,通过 dlerror 获取错误信息并打印,然后程序返回 。

  2. 获取函数地址:add_func = (int (*)(int, int))dlsym(handle, "add");这行代码使用 dlsym 函数从动态库句柄 handle 中获取名为 add 的函数地址,并将返回的指针强制转换为 int (*)(int, int)类型的函数指针 。同样的方式获取 multiply 函数地址 。每次调用 dlsym 后,都检查是否有错误发生,如果有,打印错误信息并关闭动态库,程序返回 。

  3. 调用函数:获取到函数地址后,就可以像调用普通函数一样调用动态库中的函数,int result_add = add_func(5, 3);和 int result_multiply = multiply_func(5, 3);分别调用 add 和 multiply 函数,并将结果打印出来 。

  4. 关闭动态库:if (dlclose(handle) != 0) {... }使用 dlclose 函数关闭动态库,如果关闭过程中出现错误,同样通过 dlerror 获取错误信息并打印 。

编译这段代码时,需要链接 libdl 库,命令如下:

复制代码
gcc dynamic_load.c -ldl -o dynamic_load

通过这个示例代码,我们可以清晰地看到动态加载 so 库的完整流程,以及 dlopendlsym dlclose 等函数在其中的具体应用 。

五、解决.so 动态库调用中的常见问题

5.1 动态库路径设置

在 Linux 系统中,程序运行时加载动态库,是按照一定的路径查找顺序来进行的。理解这个顺序,对于正确设置动态库路径至关重要。具体的查找顺序如下:

**(1)编译时指定的运行路径(RPATH):**在对可执行文件进行编译操作的时候,可以运用 -Wl,-rpath 这样的链接参数来指定运行时动态库的搜索路径。要是这么做的话,所指定的路径就会被嵌入到可执行文件的动态段里头。当程序运行起来的时候,就会优先从这个所在的地方去查找动态库。

编译的命令乃是 gcc -o main main.c -L/path/to/lib -lmath -Wl,-rpath,/path/to/lib,在这当中,-Wl,-rpath,/path/to/lib这一部分就是明确指定了在运行的时候要去到/path/to/lib这个目录当中去查找动态库 。

复制代码
# RPATH 编译示例(把库路径写死到可执行文件里,优先级最高)
gcc -o main main.c -L./ -lmath_lib -Wl,-rpath,./
(2)环境变量 LD_LIBRARY_PATH 指定的路径:
LD_LIBRARY_PA

TH乃是用于指定动态库搜索路径的环境变量。它是由冒号分隔开来的目录所构成的列表。倘若设置了这个环境变量,那么程序在运行的时候,就会依照列表里目录的顺序去寻觅动态库。比如说export LD_LIBRARY_PATH=/home/user/lib:/usr/local/lib,这就意味着会在/home/user/lib以及/usr/local/lib这两个目录当中去查找动态库 。

复制代码
# 设置临时动态库路径
export LD_LIBRARY_PATH=/home/user/lib:/usr/local/lib:$LD_LIBRARY_PATH

(2)/etc/ld.so.cache 缓存文件中记录的路径:/etc/ld.so.cache乃是一个缓存文件也。它具备着系统所已知的动态库路径信息。此缓存文件是由ldconfig命令来予以更新的。ldconfig会去读取/etc/ld.so.conf以及/etc/ld.so.conf.d目录当中的所有配置文件。之后便把很多配置文件所指定的目录以及默认的/lib、/usr/lib等目录里的动态库信息更新到/etc/ld.so.cache里面去。

复制代码
# 查看系统缓存里有没有你的库
ldconfig -p | grep libmath_lib
# 刷新系统缓存
sudo ldconfig

(4)默认路径 /lib 和 /usr/lib:要是前面好几个办法都没有寻找到所需要的动态库,那么系统就会前往默认的路径 /lib 以及 /usr/lib 当中去进行查找,/lib 和 /usr/lib 乃是系统自身所自带的动态库存放的所在之处 。

复制代码
# 查看系统默认库路径
ls /lib
ls /usr/lib

了解了查找顺序后,我们来看看设置 LD_LIBRARY_PATH 环境变量的方法。

①临时设置:若要在当前终端会话当中临时性地设置 LD_LIBRARY_PATH,运用 export 命令便可以。假定动态库所处的目录是 /home/user/mylib,在终端里输入 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/mylib,那么当前终端运行的程序就会前往 /home/user/mylib 这个目录去寻找动态库。可是这类设置仅仅在当前终端会话期间有效,把终端关掉之后设置就不存在了 。

复制代码
# 临时生效(当前终端)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/user/mylib
./main

**②永久设置:**要是想要每次登录系统的时候就自动去设置 LD_LIBRARY_PATH,是可以通过去修改配置文件来进行操作的。要是针对单个用户的情况,那就去编辑用户主目录当中的.bashrc 文件(要是使用的是 bash shell 的话),在这个文件的末尾添加上 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib,然后接着去执行 source ~/.bashrc 来让设置马上就生效。要是要做系统全局的设置,那就去编辑 /etc/profile 文件,添加上同样的内容,不过这种办法得谨慎小心地去操作,因为它会对所有用户产生影响 。

通常情况下并不太建议永久性地去设置 LD_LIBRARY_PATH。这是因为这么做有可能会引发安全层面上的风险以及管理方面的繁杂状况。更为妥当的办法是把库的路径添加到 /etc/ld.so.conf.d/ 目录当中的自定义配置文件里面,之后运行 ldconfig 命令来对缓存进行更新。

复制代码
# 用户永久生效
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/lib' >> ~/.bashrc
source ~/.bashrc
# 系统级推荐方案(推荐)
sudo echo "/path/to/lib" > /etc/ld.so.conf.d/mylib.conf
sudo ldconfig

5.2 加载失败的排查与解决

在调用动态库的时候,有时候会遭遇到加载失败的情形。常见的错误提示例如 error while loading shared libraries: xxx.so: cannot open shared object file: No such file or directory,这个情况所表达的含义就是系统没办法找到所指定的动态库文件。针对这类问题,接下来给出一些进行排查的思路以及解决的办法。

(1)检查库路径------首先要确认动态库是否在系统能够找到的路径中。

你能够运用 ldd 命令去查看可执行文件所依赖的动态库以及它们的路径。举个例子来讲,针对可执行文件 main ,执行 ldd main 操作,输出的结果将会把每一个依赖的动态库以及实际加载的路径给显示出来。要是某一个动态库显示为 not found ,那便得去检查一下 LD_LIBRARY_PATH 环境变量设置得对不对,又或者库文件是不是真的在指定的路径里头。

要是库文件处于非标准的路径,那么就存在几种办法。其一就是设置LD_LIBRARY_PATH环境变量。其二是在编译的时候使用-Wl,-rpath参数来指定路径。其三是把库的路径添加到/etc/ld.so.conf.d/目录下的配置文件当中,然后运行ldconfig命令来更新缓存,如此一来系统就可以找到库文件了。

复制代码
# 查看程序所有依赖库,定位缺失 so
ldd main
# 临时添加库路径快速修复
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
./main
# 编译时固化 rpath,一劳永逸
gcc -o main main.c -L./ -lmath_lib -Wl,-rpath,./
# 系统全局注册库路径
sudo echo "/home/user/mylib" > /etc/ld.so.conf.d/mathlib.conf
sudo ldconfig

(2)检查权限------确保动态库文件和其所在目录具有正确的权限,程序需要具备读取动态库文件的权限才可以对其进行加载。使用ls -l命令来查看文件以及目录的权限情况,例如ls -l /path/to/lib/libmath.so。要是权限不满足要求的话,运用chmod命令来更改权限。举例来说要给所有用户添加读取权限,就执行chmod a+r /path/to/lib/libmath.so。

复制代码
# 查看库文件权限
ls -l ./libmath_lib.so
# 增加全局读权限
chmod a+r ./libmath_lib.so
# 若目录无进入权限同步修复
chmod +x ./

(3)检查库文件完整性------动态库文件可能因为损坏或下载不完整等原因导致无法加载。你可以尝试重新去获取动态库文件。举例来说从那靠谱的软件源重新把动态库文件下载下来,或者重新进行编译以此来生成动态库文件 。

复制代码
# 重新编译生成干净 so
gcc -fPIC -shared -o libmath_lib.so math_lib.c

(4)检查依赖关系------有些动态库可能依赖其他的库文件,要是这些所依赖的库没有被正确地进行安装或者无法被找到的话,那么当前动态库的加载就会出现失败的情况。使用 ldd 命令可以去查看当前动态库的加载路径,并且还能够看到它所依赖的其他库文件。要是发现存在有依赖库没有被找到的情况,那就必须去进行安装或者对这些依赖库进行正确的配置才行。

倘若一个动态库需要依赖libz.so,而系统当中找不到这个库的话,那么就需要去安装zlib库。在Ubuntu系统里面,就可以使用sudo apt-get install zlib1g-dev这个命令来进行安装。

复制代码
# 直接查看 so 自身的依赖库
ldd ./libmath_lib.so
# Ubuntu 缺失 zlib 依赖安装示例
sudo apt update
sudo apt install zlib1g-dev

学习**.so 动态库** 后,建议吃透核心编译参数原理,整理报错排查经验。按需选用动态库与静态库,重点掌握运行时显式加载,规范库路径配置,规避环境问题,提升代码工程稳定性。

相关推荐
好评1241 小时前
【Linux】应用层自定义协议与序列化
linux·运维·网络
赴生-1 小时前
Lunix 操作系统 基本指令(一)
linux
oushaojun22 小时前
大厂 C++ 后端面试:深度拆解 Linux页表 机制(转)
linux
励志不掉头发的内向程序员2 小时前
【LibreCAD 2D架构】从鼠标点击到屏幕像素:LibreCAD绘图架构全链路解析之Action与命令系统
linux·开发语言·c++·qt·学习·系统架构
fengyehongWorld2 小时前
Linux caddy的安装与配置
linux·运维·服务器
晚风叙码2 小时前
Linux(三)apt/yum 软件安装与 vim 编辑器入门指南
linux·编辑器·vim
洪流之源2 小时前
Windows 远程 Ubuntu 24.04 桌面
linux·windows·ubuntu
2401_862880822 小时前
Linux应用层开发 --- HTTP
linux·运维·http
s_w.h2 小时前
【 linux 】线程互斥与同步
java·linux·服务器·开发语言