个人博客地址: https://cxx001.gitee.io
前言
Windows与Linux下面的动态链接库区别
1. 文件后缀不同
Linux动态库的后缀是 .so
文件,而window则是 .dll
文件。
2. 文件格式不同
(a)Linux下是ELF
格式,即Executable and Linkable Format
在ELF之下,共享库中所有的全局函数和变量在默认情况下都可以被其它模块使用,即ELF默认导出所有的全局符号。
(b)Windows下面是PE
格式的文件,即Portable Executable Format
DLL文件和EXE文件实际上是一个概念,都是PE格式的二进制文件。DLL需要显示地“告诉”编译器需要导出某个符号,否则编译器默认所有的符号都不导出。
3. 动态链接库的文件个数不一样
Linux的动态链接库就只有一个 .so 文件,还有与之对应的头文件,而在Windows下面的动态库有两个文件,
一个是引入库(.LIB)文件,
一个是动态库(.DLL)文件,
需要的头文件(.h)文件
(1)LIB引入库文件包含被DLL导出的函数名称和位置,对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
(2)DLL文件包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。
Windows上动态库使用
1. 创建动态库
windows上创建动态库一般有两种方式:
-
使用
_declspec
显示声明要导出的对象 。 -
和平时写程序一样,源码中不需要显示声明导出,导出放在
def
文件中声明。
首先介绍下使用
_declspec
来创建动态库的过程:
(1)新建一个空项目或者是使用DLL模板都可以。(个人习惯用干净的空项目)
(2)修改项目属性输出类型改为dll。
(3)正常添加.h与.cpp文件,.h中要导出的函数前添加_declspec(dllexport)
声明即可。
(4)重新生成,在工程目录即可生成对应的dllapi.lib
与dllapi.dll
文件。
再来看看使用def文件声明导出的方式:
(1)添加def文件
(2)def文件中声明要导出的函数
LIBRARY
EXPORTS
add
(3)重新生成,和第一种显示声明方式一样生成了.lib和.dll2个文件。
2. 使用动态库
windows上使用动态库一般有2种方式:
- 隐式调用(IDE上设置)
- 显示调用
下面分别介绍下详细的使用流程
隐式调用使用流程
(1)创建控制台测试工程,并建立一个依赖目录,将动态库的.h
和.lib
放在这个目录下,同时将.dll
放在exe可执行程序同级目录。
(2)配置
-
项目->属性->配置属性->VC++ 目录-> 在“包含目录”里添加头文件
DllAPI.h
所在的目录 。 -
项目->属性->配置属性->VC++ 目录-> 在“库目录”里添加依赖文件
dllapi.lib
所在的目录。 -
项目->属性->配置属性->链接器->输入-> 在“附加依赖项”里添加
dllapi.lib
。 你也可以在代码中添加一行设置库的链接,#pragma comment(lib, "dllapi.lib")
,这样就不需要2、3配置了。
(3)测试运行
显示调用使用流程
只需要将.dll
放在exe可执行程序同级目录就行了,IDE不需要额外设置。注意要使用这个dll中的方法其创建时必须要_declspec
显示导出,使用时只要这个dll文件就行了,.h和.lib不需要。
#include <iostream>
#include "Windows.h" // 动态库加载、释放等接口头文件
#include "tchar.h" // _T头文件,设置支持Unicode编码
typedef int(*Dllfun)(int, int); // 待使用接口的函数指针
int main()
{
// 加载动态库
HINSTANCE hdll = LoadLibrary(_T("dllapi.dll"));
if (hdll == NULL) {
return -1;
}
// 获取动态库中导出的函数指针
Dllfun funName = (Dllfun)GetProcAddress(hdll, "add");
if (funName == NULL) {
FreeLibrary(hdll);
return -1;
}
// 调用、释放
int ret = funName(1, 2);
FreeLibrary(hdll);
std::cout << "result = " << ret << "\n";
}
Linux上动态库使用
Linux上创建动态库很简单,不需要显示声明导出的函数,它会默认导出。
在Linux上使用动态库也有2种方式:
- 编译器链接
- 库文件加载
1. 编译器链接使用流程
- 编写源文件。
- 将一个或几个源文件编译链接,生成libxxx.so。
- 通过
-L<path> -lxxx
的gcc选项链接生成的libxxx.so。 - 把libxxx.so放入链接库的标准路径,或指定
LD_LIBRARY_PATH
,才能运行链接了libxxx.so的程序。
(1) 编写源文件,生成so共享库
建立一个源文件:add.c,代码如下:
int add(int x, int y)
{
return x + y;
}
编译生成libadd.so:
gcc -fPIC -shared -o libadd.so add.c
我们会得到libadd.so。
实际上上述过程分为编译和链接两步, -fPIC是编译选项,PIC是 Position Independent Code 的缩写,表示要生成位置无关的代码,这是动态库需要的特性; -shared是链接选项,告诉gcc生成动态库而不是可执行文件。
上述的一行命令等同于:
gcc -c -fPIC add.c
gcc -shared -o libadd.so add.o
(2)为动态库编写接口文件
#pragma once
int add(int x, int y);
(3)测试,链接动态库生成可执行文件
建立一个使用add
函数的test.c,代码如下:
#include <stdio.h>
#include "add.h"
int main(int argc, char *argv[])
{
int ret = add(1, 2);
printf("ret= %d.\n", ret);
return 0;
}
gcc test.c -L. -ladd
生成a.out,其中-ladd
表示要链接libadd.so
。
-L.
表示搜索要链接的库文件时包含当前路径。
注意,如果同一目录下同时存在同名的动态库和静态库,比如 libadd.so
和 libadd.a
都在当前路径下,
则gcc会优先链接动态库。
(4) 运行
运行 ./a.out
会得到以下的错误提示。
./a.out: error while loading shared libraries: libadd.so: cannot open shared object file: No such file or directory
找不到libadd.so,原来Linux是通过 /etc/ld.so.cache
文件搜寻要链接的动态库的。
而 /etc/ld.so.cache
是 ldconfig 程序读取 /etc/ld.so.conf
文件生成的。
(注意, /etc/ld.so.conf
中并不必包含 /lib
和 /usr/lib
,ldconfig
程序会自动搜索这两个目录)
如果我们把 libadd.so
所在的路径添加到 /etc/ld.so.conf
中,再以root权限运行 ldconfig
程序,更新 /etc/ld.so.cache
,a.out
运行时,就可以找到 libadd.so
。
因此我们可以为a.out
指定 LD_LIBRARY_PATH
运行,如下:
LD_LIBRARY_PATH=. ./a.out
程序就能正常运行了。LD_LIBRARY_PATH=.
是告诉 a.out
,先在当前路径寻找链接的动态库。
或者修改LD_LIBRARY_PATH
环境变量,指定为当前目录也可以,如下:
export LD_LIBRARY_PATH=${pwd}:${LD_LIBRARY_PATH}
然后直接执行./a.out
也可以运行了。
2. 库文件加载使用流程
像window调用库文件一样,在linux下,也有相应的API因为加载库文件而存在。它们主要是以下几个函数:
使用源码如下:
// test2.c
#include <stdio.h>
#include <dlfcn.h>
int main(int argc, char *argv[]){
void * libm_handle = NULL;
int (*add_method)(int, int);
char *errorInfo;
int result;
// dlopen 函数还会自动解析共享库中的依赖项。这样,如果您打开了一个依赖于其他共享库的对象,它就会自动加载它们。
// 函数返回一个句柄,该句柄用于后续的 API 调用
libm_handle = dlopen("libadd.so", RTLD_LAZY );
// 如果返回 NULL 句柄,表示无法找到对象文件,过程结束。否则的话,将会得到对象的一个句柄,可以进一步询问对象
if (!libm_handle){
// 如果返回 NULL 句柄,通过dlerror方法可以取得无法访问对象的原因
printf("Open Error:%s.\n",dlerror());
return 0;
}
// 使用 dlsym 函数,尝试解析新打开的对象文件中的符号。您将会得到一个有效的指向该符号的指针,或者是得到一个 NULL 并返回一个错误
add_method = dlsym(libm_handle,"add");
errorInfo = dlerror();// 它会在发生前面的错误时返回一个字符串,同时将其从内存中清空; 在没有错误发生时返回 NULL
if (errorInfo != NULL){
printf("Dlsym Error:%s.\n",errorInfo);
return 0;
}
// 执行“cosf”方法
result = (*add_method)(1, 2);
printf("result = %d.\n",result);
// 调用 ELF 对象中的目标函数后,通过调用 dlclose 来关闭对它的访问
dlclose(libm_handle);
return 0;
}
编译,运行./test2
可以看到结果为3。
gcc test2.c -o test2 -ldl