在开发中,动态链接库(DLL)和共享对象(Shared Object).so文件的使用成为提升程序灵活性和重用性的关键手段。如下相关工具,GNU Libtool: 一种用于创建可移植共享库的工具。Dynamic Linker: 关于动态链接器的更多信息。CMake: 一个跨平台的构建系统,简化了共享库的管理和构建。无论是提升程序的模块化程度,还是优化资源使用,动态链接都是一种强大的技术手段。
库概念的由来
比如项目合作时,你不可能直接把源代码给别人,那样别人就可以自己开发,因为源代码就是你的核心技术。理论上不应该卖给别人源代码,而应该是程序,这样你可以根据别人有什么需求,进行改进或添加什么功能模块等,即改一次就可以收费一次,达成一个长期合作。
问题是给到客户的程序,应该是什么呢? 首先当然可以是可执行程序,不过这不是库讨论的范围
对于库,即两种文件:(1)生成的库 (2)头文件
这样把生成的库和头文件给客户也能够使用,只是他不知道里面具体怎么实现的。这样二者才能维持一个长期的合作。头文件对应的.c文件都被打包到了静态库和动态库里面了。
动态库
一种可执行代码模块,允许多个程序共享相同的库文件,从而减少内存使用和磁盘空间占用。在Linux系统中,这类文件通常以.so为后缀名,而在Windows系统中则为.dll。使用动态库可以(1)节省内存和磁盘空间:多个程序可以同时使用同一个动态库实例。(2)简化更新:编译更新动态库后,所有使用该库的程序都自动获得更新后的功能。(3)动态加载:程序可以在运行时选择性地加载,从而实现模块化设计和延迟加载,提升启动性能。
动态库创建
假设要创建一个简单的库,包含一个打印“Hello, World!”的函数。
编写一个动态库的源代码 mylib.cpp
#include <iostream>
/*
使用 extern "C" 指定函数采用C语言的调用约定
以避免C++函数名修饰带来的问题
*/
extern "C" {
void hello() {
std::cout << "Hello from shared library!" << std::endl;
}
}
编译动态库
g++ -fPIC -shared -o libmylib.so mylib.cpp
(-fPIC:生成与位置无关的代码(Position-Independent Code)创建动态库的必需选项。
-shared:指示编译器生成动态库(共享库)。 -o:指定输出文件名为 libmylib.so。)
动态库调用
编写一个主程序 main.cpp
#include <iostream>
#include <dlfcn.h>
typedef void (*HelloFunc)();
int main() {
// 动态加载共享库
void* handle = dlopen("./libmylib.so", RTLD_LAZY);
if (!handle) {
std::cerr << "Cannot open library: " << dlerror() << std::endl;
return 1;
}
// 清除之前的错误
dlerror();
// 获取函数指针
HelloFunc hello = (HelloFunc) dlsym(handle, "hello");
const char* dlsym_error = dlerror();
if (dlsym_error) {
std::cerr << "Cannot load symbol 'hello': " << dlsym_error << std::endl;
dlclose(handle);
return 1;
}
// 调用函数
hello();
// 关闭共享库
dlclose(handle);
return 0;
}
编译
g++ -o main main.cpp -ldl
(-ldl:链接动态加载库(libdl),以使用 dlopen、dlsym 和 dlclose 函数。)
运行程序确保共享库 libmylib.so 位于程序的可访问路径下,然后运行编译后的可执行文件:
./main
//如果一切正常,输出 Hello from shared library!。
dlopen 函数用于动态加载共享库。第一个参数是共享库的路径,第二个参数指定加载模式。常用的加载模式有:RTLD_LAZY:延迟解析未定义的符号,直到真正需要时再解析。RTLD_NOW:立即解析所有未定义的符号。如果无法解析所有符号,则 dlopen 失败。dlsym 函数用于从共享库中获取符号(函数或变量)的地址。该函数返回一个 void* 指针,需要进行强制类型转换以调用实际的函数。
dlclose 函数用于关闭共享库,释放相关资源。程序退出时系统会自动关闭所有打开的共享库,但手动调用 dlclose 有助于及时释放资源,减少内存占用。
找不到动态库的问题
如果在运行程序时出现错误 Cannot open library: ...,需要确保共享库路径正确,并且库文件具有可执行权限。使用以下命令检查库文件权限:
ls -l libmylib.so
权限不足的话,增加执行权限
chmod +x libmylib.so
确保共享库路径包含在环境变量 LD_LIBRARY_PATH 中
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/your/library
使用经验:(符号解析失败:如果 dlsym 失败,通常是因为函数名错误或符号不可见。检查共享库代码,确保函数名正确,并使用 extern "C" 指定C语言调用约定。)
在工程中,分步骤制作动态库 (gcc)
.
├── include
│ └── head.h
├── lib
├── main.c
└── src
├── a.c
├── b.c
├── c.c
└── d.c
编译和位置无关的目标文件
gcc -fPIC -c *c -I ../include
编译打包动态库,并移动到lib目录下
gcc -shared -o libMylibs.so *o -Iinclude
mv libMylibs.so ../lib
应用程序编译使用动态库的方法
gcc main.c lib/libMylibs.so -o myapp -Iinclude
gcc main.c -L lib -l Mylibs -o myapp -Iinclude
开发经验:
动态链接器搜索某一个动态库时,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
1.可执行文件内部的 DT_RPATH 段
2.系统的环境变量 LD_LIBRARY_PATH
3.系统动态库的缓存文件 /etc/ld.so.cache
4.存储动态库 / 静态库的系统目录 /lib/, /usr/lib 等
按照以上四个顺序,依次搜索,找到之后结束遍历,最终还是没找到,动态连接器就会提示动态库找不到的错误信息。
解决方案:
Linux 静态库和动态库 | 爱编程的大丙
总结:
分步骤编译有几个优势:
增量编译:如果有多个源文件,单独编译可以减少重复编译的时间。修改一个文件后,只需重新编译这个文件,而不需要重新编译所有文件。
模块化:分步骤编译可以帮助组织代码,使项目更加模块化和易于管理。
调试:生成对象文件后,可以更容易进行分析和调试。
依赖管理:在大型项目中,使用 Makefile 或其他构建系统可以更好地管理依赖关系。
(对于简单项目,一步到位是可以的;但在复杂项目中,分步骤更灵活和高效)
32位系统的内存分配如下:
关于静态库
在工程中,分步骤制作
.
├── include
│ └── head.h
├── lib
├── main.c
└── src
├── a.c
├── b.c
├── c.c
└── d.c
首先编译目标文件
gcc *.c -c -I ../include
因为静态库是和位置有关的,打包成静态文件(.a文件),并移动到lib目录下
ar rcs libMylibs.a *.o
mv libMylibs.a ../lib
静态库调用的两种方法
//方式一
gcc main.c -Iinclude -L lib -l Mylibs -o myapp
//方式二
gcc main.c -I ./include lib/libMylibs.a -o myapp
-I参数: 指定头文所在的文件夹名,文件夹名可以和参数贴着写在一起
-L参数:指定静态库的文件夹名
-l参数: 指定静态库的名字,但名字要掐头去尾,原静态库名字为libMylibs.a,在指定-l参数值的时候为:-l Mylibs
-o参数:输出编译之后可执行文件的名字