动态链接库的使用:加载和链接

在部分SDK的对接中,有些平台除了DLL外并没有提供导入库来供我们使用,那就只能使用代码中加载DLL的办法来调用DLL内的函数,本文来记录一下两种用法,再分析一下优劣。

我们可以先生成一个测试的DLL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// max_dll.h
#ifndef MAX_DLL_H_
#define MAX_DLL_H_
extern "C" int max(int,int);
typedef int (*dll_max)(int,int);
#endif
// max_dll.cpp
#include "max_dll.h"
extern "C" int max(int lhs, int rhs)
{
return lhs>rhs?lhs:rhs;
}

之所以使用extern "C"的原因是在于C++的name manglingimplementation-define的,简单来说C++的ABI不稳定,使用C的链接可以避免不同实现name mangling规则不一导致符号不一致的问题。
生成DLL(更具体的可以在我之前的文章——C/C++编译和链接模型分析中查看:

1
2
3
$ g++ max_dll.cpp -shared -o max_dll.dll
$ ls
max_dll.cpp max_dll.dll* max_dll.h

运行时加载DLL

然后我们考虑在外面的代码中怎么才能调用这个DLL中的max函数呢?
因为编译和链接的缘故,因为该DLL没有导入库,所以不能在连接时指定导入库从而直接使用DLL中的函数,但是我们可以动态地加载DLL文件,在Windows上可以使用LoadLibraryGetProcAddress组合实现。
其中LoadLibrary的作用是加载DLL文件,GetProcAddress的作用是从DLL内获取符号的函数指针,FreeLibrary则是释放DLL文件的句柄(handler):
其原型如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <Windows.h>
// Loads the specified module into the address space of the calling process. The specified module may cause other modules to be loaded.
// Return Value
// If the function succeeds, the return value is a handle to the module.
// If the function fails, the return value is NULL. To get extended error information, call GetLastError.
HMODULE LoadLibraryA(
LPCSTR lpLibFileName
);
// Retrieves the address of an exported function or variable from the specified dynamic-link library (DLL).
// Return Value
// If the function succeeds, the return value is the address of the exported function or variable.
// If the function fails, the return value is NULL. To get extended error information, call GetLastError.
FARPROC GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
// Frees the loaded dynamic-link library (DLL) module and, if necessary, decrements its reference count. When the reference count reaches zero, the module is unloaded from the address space of the calling process and the handle is no longer valid.
BOOL FreeLibrary(
HMODULE hLibModule
);

加载DLL实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "max_dll.h"
#include <cstdio>
#include <tchar.h>
#include <Windows.h>
#define DLL_FILE _T("max_dll.dll")
int main()
{
HINSTANCE hdll = LoadLibrary(DLL_FILE);
if (hdll!=NULL)
{
dll_max max_func=(dll_max)GetProcAddress(hdll, "max");
if(max_func != NULL)
{
std::printf("%d\n",max_func(123,456));
FreeLibrary(hdll);
}else
{
std::printf("From DLL Get function Address is falid.\n");
}
}else
{
std::printf("LoadLibrary is faild.\n");
}
return 0;
}

注意:如果上面的max_dll.cpp不是使用extern "C"来链接的,那么上面的代码在这一行:

1
dll_max max_func=(dll_max)GetProcAddress(hdll, "max");

会获取函数指针失败,因为按照C++的name mangling规则,函数max在dll内的符号名字不是max而是类似下面这样的(之所以说"类似"是因为没有标准规定,依赖实现):
我们可以通过nm来查看目标文件中的符号信息(不使用extern "C"的dll版本),已隐藏其他的符号:

1
2
3
# 非extern "C"版本
$ nm max_dll.dll
00000000660814b0 T _Z3maxii

把上面出错的代码改为下面,则可以获取函数指针成功(但是这样做也太麻烦了):

1
dll_max max_func=(dll_max)GetProcAddress(hdll, "_Z3maxii");

而使用extern "C"的dll版本内的符号信息为(因为使用C的链接方式则没有任何符号改编):

1
2
3
# extern "C"版本
$ nm max_dll.dll
00000000660814b0 T max

注:Linux上与LoadLibrary/GetProcAddress/FreeLibrary分别对应(Linux动态链接库为.so)的是dlopen/dlsym/dlclose,其原型分别如下:

1
2
3
4
5
6
7
8
#include <dlfcn.h>
// dlopen,returns library handle on success, or NULL on error
void *dlopen(const char *libfilename.,int flags);
// dlsym,returns Address of symbol,or NULL if symbol is not found.
void *dlsym(void *handler,char *symbol);
// dlclose, returns 0 on success,or -1 on error
int dlclose(void *handler);

使用DLL的导入库

而如果具有导入库,我们就可以用类似于静态链接的办法,而不用在运行时加载DLL文件再获取函数指针:

1
2
# 创建DLL并创建一个导入库lib
g++ max_dll.cpp -shared -o max_dll.dll -Wl,--out-implib,imp_max_dll.lib

-Wl为传递给链接器的参数,--out-implib为生成导入库。
然后我们就可以像使用静态链接一样来链接DLL中的符号:

1
2
3
4
5
6
7
8
9
// call_max.cpp
#include <cstdio>
#include "max_dll.h"
int main(void)
{
std::printf("%d\n",max(123, 456));
return 0;
}

然后执行编译,但不链接-c,只生成目标文件call_max.o:

1
$ g++ -c call_max.cpp -o call_max.o

然后把imp_max_dll.lib链接进来:

1
$ g++ call_max.o imp_max_dll.lib -o call_max.exe

没有符号未定义错误。

然后运行:

1
2
$ ./call_max.exe
456

我们查看一下call_max.exe依赖的动态链接库,使用ldd命令:

1
2
3
4
5
6
$ ldd call_max.exe
ntdll.dll => /c/WINDOWS/SYSTEM32/ntdll.dll (0x7ffa14b80000)
KERNEL32.DLL => /c/WINDOWS/System32/KERNEL32.DLL (0x7ffa12b00000)
KERNELBASE.dll => /c/WINDOWS/System32/KERNELBASE.dll (0x7ffa11d90000)
msvcrt.dll => /c/WINDOWS/System32/msvcrt.dll (0x7ffa128a0000)
max_dll.dll => /c/Users/visionsmile/Desktop/code/max_dll.dll (0x66080000)

可以看到,这里call_max.exe依赖的是当前目录下的max_dll.dll
如果把max_dll.dll删除呢?我们删除之后再试一下:

1
2
$ ./call_max.exe
call_max.exe: error while loading shared libraries: max_dll.dll: cannot open shared object file: No such file or directory

会提示找不到文件的错误。

运行时动态加载DLL和使用DLL的导入库各有优缺点,未完待续,有时间再继续分析。

扩展阅读

全文完,若有不足之处请评论指正。

扫描二维码,分享此文章

本文标题:动态链接库的使用:加载和链接
文章作者:ZhaLiPeng
发布时间:2018年10月15日 20时50分
本文字数:本文一共有1,295字
更新历史: Blame, History  文本模式: .md Raw
原始链接:https://imzlp.me/posts/18949/
许可协议: CC BY-NC-SA 4.0
转载请保留原文链接及作者信息,谢谢!
您的捐赠将鼓励我继续创作!