动态链接库

Posted swust_wjy

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态链接库相关的知识,希望对你有一定的参考价值。

动态链接库

建立动态链接库的方法

新建项目–>win32控制台应用程序–>Dll
建立动态链接库

注意:画蓝色线部分是静态链接库

动态链接库的使用方法

隐式链接方式

隐式连接方法使用动态链接库需要用到动态链接库程序生成的.lib文件(.lib文件说明了导出了哪些函数)。
在使用动态链接库中的函数之前需要先使用extern 声明函数,将.lib文件和.dll文件放到代码所在目录下,在项目链接器的附加依赖项那里填入lib文件名或者使用pragma comment方法加载lib文件。注意:只有一个.lib文件没有.dll文件是不行的,因为.lib文件没有导出函数的具体实现,具体实现在.dll文件中,同样的只有.dll文件没有.lib文件也是不行的,因为没有导出函数的声明.

简单的函数

范例:

//Dll1.cpp文件
_declspec(dllexport) int add(int a, int b)
{
    return a + b;
}

调用dumpbin 命令来查看dll1中导出的函数
dumpbinDll1
可以看到导出了add函数
将Dll1.lib和Dll.dll文件都放到测试程序代码目录下

// 测试程序
#include <stdio.h>
#pragma comment(lib,"Dll1.lib")
int _tmain(int argc, _TCHAR* argv[])
{   
    extern int add(int a, int b);
    printf("4 + 5 = %d \\n", add(4, 5));
    return 0;
}
//输出
4 + 5 = 9

此处要声明extern int add(int a,int b);
或者使用_declspec(dllimport) int add(int a, int b);来替代上面的一句声明
那么这两种声明方法的区别是什么的?
第二种声明方法表明这两个函数是从.lib文件导入的,效率更高,我们在使用的时候应尽量使用这种方法。

dumpbin 的使用方法:

直接在Dll的debug目录下执行dumpbin命令的方法
在VS2013的如下目录下Microsoft Visual Studio 12.0\\VC\\bin有一个vcvars32.bat文件,将这个文件直接拖放到cmd窗口中执行,然后我们就可以在DLL程序的Debug目录下直接执行dumpbin命令而不必进入VS2013的bin目录下去执行dumpbin命令了。(但是有一个缺点是,当你关闭当前cmd窗口下次再使用时就要重新执行一遍那个vcvars32.bat批处理文件)
- 查看动态链接库导出了哪些函数的方法:在DLL程序的debug目录下执行 dumpbin -exports 文件名.dll
- 查看应用程序导入了哪些动态链接库函数的方法:在debug目录下执行命令 dumpbin -imports test.exe
注意,一定是imports,不能是import

test.exe是我的使用dll的程序的可执行文件

改进

为了更好的使用隐式连接方法调用动态链接库,我们在编写动态链接库的时候一般提供一个头文件,这个头文件中包含了我们所导出的函数的声明注释等。注意:我们的动态链接库头文件是提供给调用者使用的。

//Dll1.h文件
_declspec(dllimport) int add(int a, int b);

我们可以把这个头文件放到要调用该动态链接库的项目下,然后包含这个头文件就可以使用动态链接库中的函数了。而不再需要使用上面的方法来声明函数了。但是.lib文件是必不可少的。

范例:
//Dll1.cpp文件
_declspec(dllexport)  int add(int a, int b)
{
    return a + b;
}
//Test.cpp文件
#include "stdafx.h"
#include "Dll1.h"
#pragma comment(lib,"Dll1.lib")
int _tmain(int argc, _TCHAR* argv[])
{
    printf("%d\\n", add(5, 6));
    return 0;
}
//输出结果为11
进一步优化,为了使Dll头文件发挥更大的作用,我们可以这样做
范例:
//Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL3_API int add(int a, int b);
//Dll1.cpp文件
#define DLL1_API _declspec(dllexport)
#include"Dll1.h"

int add(int a, int b)
{
    return a + b;
}

在编译一个程序的时候头文件不参与编译,源文件单独编译。在编译源文件的时候首先我们定义了一个宏DLL1_API,然后我们包含了一个头文件,这个头文件需要展开,展开后进入头文件,头文件首先判断是否定义了DLL1_API这个宏,定义了不做处理,这里我们在cpp文件里面已经定义了DLL1_API这个宏,所以它什么也不做,然后DLL1_API int add(int a, int b);这句代码中的DLL1_API实际上是_declspec(dllexport),在动态链接库使用的时候呢就表明我们要导出add()这个函数。当这个头文件给我们的调用者使用的时候,只要调用者没有定义DLL1_API那么这个时候DLL1_API int add(int a,int b);这段代码就相当于导入函数声明,这样就达到了一个dll的头文件同时为调用者和Dll源文件使用的目的。
注意:隐式链接方式加载动态链接库时,.dll文件可以放到debug目录或者程序代码目录下,但是.lib文件和.h文件必须放到代码目录下。

定义类

范例:
//Dll3.h
#ifdef DLL3_API
#else
#define DLL3_API _declspec(dllimport)
#endif
DLL3_API int add(int a, int b);
class DLL3_API Rectangle
{
public:
    float calcArea(float x, float y);
};
//Dll3.cpp
#define DLL3_API _declspec(dllexport)
#include"Dll3.h"

int add(int a, int b)  //这里不需要再加上DLL3_API因为头文件里面已经声明了要导出这个函数。
{
    return a + b;
}
float Rectangle::calcArea(float x, float y)
{
    return x*y;
}
//测试程序Test.cpp
#include "Dll3.h"
#include <stdio.h>
#pragma comment(lib,"Dll3.lib")
int _tmain(int argc, _TCHAR* argv[])
{
    printf("%d\\n", add(5, 6));
    Rectangle rt;
    printf("面积是:%f\\n", rt.calcArea(4, 5));
    return 0;
}
//输出
11
20.000

动态加载方式

在了解动态加载方式之前先要了解一下为什么要使用动态加载方式。

预备知识:函数调用约定

转载自:作者:星轨(oRbIt)
E_Mail:inte2000@163.com
原文地址

函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用函数负责清除栈中的参数,还原堆栈。函数调用约定有很多方式,除了常见的__cdecl,__fastcall和__stdcall之外,C++的编译器还支持thiscall方式,不少C/C++编译器还支持naked call方式。这么多函数调用约定常常令许多程序员很迷惑,到底它们是怎么回事,都是在什么情况下使用呢?下面就分别介绍这几种函数调用约定。
1. _cdecl约定

编译器的命令行参数是/Gd。__cdecl方式是C/C++编译器默认的函数调用约定,所有非C++成员函数和那些没有用__stdcall或__fastcall声明的函数都默认是__cdecl方式,它使用C函数调用方式,函数参数按照从右向左的顺序入栈,函数调用者负责清除栈中的参数,由于每次函数调用都要由编译器产生清除(还原)堆栈的代码,所以使用__cdecl方式编译的程序比使用__stdcall方式编译的程序要大很多,但是__cdecl调用方式是由函数调用者负责清除栈中的函数参数,所以这种方式支持可变参数,比如printf和windows的API wsprintf就是__cdecl调用方式。对于C函数,__cdecl方式的名字修饰约定是在函数名称前添加一个下划线;对于C++函数,除非特别使用extern “C”,C++函数使用不同的名字修饰方式。
2. _stdcall

编译器的命令行参数是/Gz,__stdcall是Pascal程序的缺省调用方式,大多数Windows的API也是__stdcall调用约定。__stdcall函数调用约定将函数参数从右向左入栈,除非使用指针或引用类型的参数,所有参数采用传值方式传递,由被调用函数负责清除栈中的参数。对于C函数,__stdcall的名称修饰方式是在函数名字前添加下划线,在函数名字后添加@和函数参数的大小,例如:_functionname@number

  1. thiscall

thiscall只用在C++成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的this指针通过ECX寄存器传递。需要注意的是thiscall不是C++的关键字,不能使用thiscall声明函数,它只能由编译器使用。
4. _fastcall

编译器的命令行参数是/Gr。__fastcall函数调用约定在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈,被调用函数在返回之前负责清除栈中的参数。编译器使用两个@修饰函数名字,后跟十进制数表示的函数参数列表大小,例如:@function_name@number。需要注意的是__fastcall函数调用约定在不同的编译器上可能有不同的实现,比如16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突。
5. naked call

采用前面几种函数调用约定的函数,编译器会在必要的时候自动在函数开始添加保存ESI,EDI,EBX,EBP寄存器的代码,在退出函数时恢复这些寄存器的内容,使用naked call方式声明的函数不会添加这样的代码,这也就是为什么称其为naked的原因吧。naked call不是类型修饰符,故必须和_declspec共同使用。
VC的编译环境默认是使用__cdecl调用约定,也可以在编译环境的Project Setting…菜单-》C/C++ =》Code Generation项选择设置函数调用约定。也可以直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等单独确定函数的调用方式。在Windows系统上开发软件常用到WINAPI宏,它可以根据编译设置翻译成适当的函数调用约定,在WIN32中,它被定义为__stdcall。

预备知识:编译器的改名规则:

对于C语言编译器:

对于__stdcall调用约定,编译器和链接器会在输出函数名前加上一个下划线前缀,函数名后面加上一个“@”符号和其参数的字节数,例如_functionname@number。__cdecl调用约定仅在输出函数名前加上一个下划线前缀,例如_functionname。__fastcall调用约定在输出函数名前加上一个“@”符号,后面也是一个“@”符号和其参数的字节数,例如@functionname@number

对于C++编译器

函数修饰都是以一个“?”开始,后面紧跟函数的名字,再后面是参数表的开始标识和按照参数类型代号拼出的参数表。对于__stdcall方式,参数表的开始标识是“@@YG”,对于__cdecl方式则是“@@YA”,对于__fastcall方式则是“@@YI”。
例如:我编写了如下一个程序,显示指明采用_cdecl调用约定

int _cdecl add(int a, int b)
{
    return a + b;
}
int _cdecl subtract(int a, int b)
{
    return a - b;
}

然后我用dumpin去查看编译器导出函数的情况如下
显式指明C语言调用约定.png
然后我再改变一下函数调用约定为_stdcall,再次查看

int _stdcall add(int a, int b)
{
    return a + b;
}
int _stdcall subtract(int a, int b)
{
    return a - b;
}

显式指明标准调用约定

参数表的拼写代号如下所示:
X–void
D–char
E–unsigned char
F–short
H–int
I–unsigned int
J–long
K–unsigned long(DWORD)
M–float
N–double
_N–bool
U–struct
对于C++的类成员函数(其调用方式是thiscall),函数的名字修饰与非成员的C++函数稍有不同,首先就是在函数名字和参数表之间插入以“@”字符引导的类名;其次是参数表的开始标识不同,公有(public)成员函数的标识是“@@QAE”,保护(protected)成员函数的标识是“@@IAE”,私有(private)成员函数的标识是“@@AAE”,如果函数声明使用了const关键字,则相应的标识应分别为“@@QBE”,“@@IBE”和“@@ABE”。如果参数类型是类实例的引用,则使用“AAV1”,对于const类型的引用,则使用“ABV1”。

使用动态链接方式加载动态链接库的原因

首先分析隐式链接方式使用动态链接库的缺点,举个例子:


//Dll2.h
#ifdef DLL2_API
#else
#define DLL2_API _declspec(dllimport)
#endif
DLL2_API int add(int a, int b);
//Dll2.cpp
#define DLL2_API _declspec(dllexport)
#include "Dll2.h"
int add(int a, int b)
{
    return a + b;
}

使用dumpbin查看导出函数
Dll2导出函数改名

可以看到此时dll2导出的add函数名字发生了改编,我们使用测试程序来做测试

//测试代码
#include "stdio.h"
#include "Dll2.h"
#pragma comment (lib,"Dll2.lib")
int _tmain(int argc, _TCHAR* argv[])
{   
    printf("4 + 5 = %d\\n", add(4, 5));
    return 0;
}
//输出 4 + 5 = 9

当然这种情况下并没有发生错误,因为我们是在同一个编译器下进行的开发,Dll和测试程序都是使用的默认函数调用方式(我使用的是VS2013,默认的函数调用方式是C语言调用约定),所以不会发生错误,现在我们来做出一些改变,我将Dll2中中的函数调用方式改为标准调用约定,

//Dll2.h
#ifdef DLL2_API
#else
#define DLL2_API _declspec(dllimport)
#endif
DLL2_API int _stdcall add(int a, int b);
//Dll2.cpp
#define DLL2_API _declspec(dllexport)
#include "Dll2.h"
int _stdcall add(int a, int b)
{
    return a + b;
}

使用dumpbin 查看dll2中的导出函数
Dll2标准调用约定
此时,我们去使用测试程序来做测试,结果依然是正确的。因为我们的测试程序包含了Dll2.h头文件,add函数在头文件中已经声明了采用标准调用约定,故不会发生错误,感觉说了半天没说到重点。其实,个人觉得使用动态加载方式的主要目的是为了在防止动态链接库在不同的编译器上能够正确加载,因为不同的编译器对函数的改编规则不同,至于函数调用约定一定要相同,如果你的函数调用约定不同,即使采用动态加载方式那么你也无法正确加载执行动态链接库中的函数。看完下文应该有所收获。现在,回到最开始的地方:

//Dll1.cpp文件
_declspec(dllexport) int _stdcall add(int a, int b)
{
    return a + b;
}
> 我改变了add函数的调用约定
// 测试程序
#include <stdio.h>
#pragma comment(lib,"Dll1.lib")
int _tmain(int argc, _TCHAR* argv[])
{   
    extern int add(int a, int b);
    printf("4 + 5 = %d \\n", add(4, 5));
    return 0;
}
> 编译时报错: unresolved external symbol "int __cdecl add(int,int)" (?add@@YAHHH@Z) referenced in function _wmain,程序无法执行,如果我将    extern int add(int a, int b);改为     extern int _stdcall add(int a, int b);程序可以正常执行

使用动态链接方式加载动态链接库

这种方式就是为了解决编译器改名带来的问题。主要用到了模块定义文件(.def文件)
1. 新建一个.def文件,将之加载到DLL工程目录下,并且在项目工程的属性里要进行设置
设置def
2. 范例:

.def文件
LIBRARY Dll3   //Dll3是动态链接库的名字

EXPORTS         //导出标识
add                  //导出的函数
//dll源文件
int add(int a, int b)
{
    return a + b;
}
//测试程序
//测试程序
#include "stdafx.h"
#include<Windows.h>
int _tmain(int argc, _TCHAR* argv[])
{
    HINSTANCE hInst;
    hInst = LoadLibrary(_T("Dll3.dll"));
    typedef int(*ADDPROC)(int a, int b);
    ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add");
    if (!Add)
    {
        printf("获取函数地址失败\\n");
        return 0;
    }
    printf("%d\\n",Add(5, 6));
    return 0;
}
//输出结果11

解析:typedef int(*ADDPROC)(int a, int b);
将(*ADDPROC)(int a, int b)这一部分看做一个整体相当于定义了一个新的整型的类型。现在将这一部分分开看(int a, int b),这一部分相当于一个函数,函数内部有两个int类型的参数,返回值是int类型。(*ADDPROC)这相当于定义了一个函数指针类型,它所表示的函数有两整型个参数,函数的返回类型也是整型。我们定义这个函数指针类型的目的是在需要的时候产生一个函数指针变量。用来接收GetProcAddress的返回值。

HINSTANCE LoadLibrary( LPCTSTR lpLibFileName // address of filename of executable module);
作用:映射指定的可执行模块到一个调用地址空间,不光可以加载一个动态链接库还可以加载一个可执行程序,返回的是一个实例句柄(它和HMODULE可以通用)
FARPROC GetProcAddress(
HMODULE hModule, // handle to DLL module
LPCSTR lpProcName // name of function
);
获取指定的导出动态链接库的函数地址,第一个参数是动态链接库的模块句柄,第二个是函数名

解释:为什么使用模块定义文件就可以避免编译器改名带来的问题呢?

当链接器在链接的时候会分析模块定义文件,当它发现模块定义文件中EXPORTS下面有add符号的名字和我们在源文件中定义的函数名字是一样的,它就会使用我们在模块定义文件中所写的名字导出我们的add函数。使用dumpbin 查看我们使用模块定义文件导出的函数的名字,可以看到编译器并没有对它发生改编
函数名未改编1

EXPORTS的用法
如果我们想自己命名导出的函数的名字,可以这样做,在.def文件中如下写法

LIBRARY Dll3
EXPORTS
add1 = add

EXPORTS用法

可以看到导出的函数名字变为了add1,这个时候我们在测试程序中使用该函数时也应该使用add1来调用该函数

现在我来对动态链接库的函数调用约定进行更改,而不改变测试程序中的函数调用约定看看会发生什么。
只改变Dll3.cpp文件中的代码

int _stdcall add(int a, int b)
{
    return a + b;
}

查看Dll3的导出函数
Dll3的导出函数
可以看到函数的名字并没有发生改编,所不同的是括号里面的内容发生了变化,仔细观察。再次运行测试程序结果发现不能运行,因为在VS2013中默认的调用约定是C语言调用约定,我使用typedef定义函数指针时没有改变调用约定,导致无法获取函数地址,只需要对测试程序做一些改动即可正常运行
typedef int(_stdcall *ADDPROC)(int a, int b);//改变调用约定为标准调用约定
这些问题一定要注意。

小结:使用模块定义文件的目的主要是为了让我们的动态链接库在其它语言中、在其它的编译器当中能够使用因为这样做能够使动态链接库所输出的函数的符号名不发生改变。

DllMain

DllMain是Dll的入口函数,
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved // reserved
);

MFC下的动态链接库的三种类型

  1. 常规的动态链接库使用MFC的静态链接
    发布的时候只需要提供动态链接库就行了
  2. 常规的动态链接库使用共享的MFC动态链接库
    发布的时候要确保用户的机器上存在MFC的动态链接库,如果MFC的动态链接库在用户的机器上不存在,那么我们发布的动态链接库就不能被加载。
  3. MFC的扩展动态链接库
    它也是使用的MFC的共享的动态链接库,它和常规的动态链接库的区别就是它可以到处MFC的类,常规动态链接库不能导出MFC的类,只能导出普通的C++的类。
    MFC的动态链接库对MFC提供了很好的支持。

以上是关于动态链接库的主要内容,如果未能解决你的问题,请参考以下文章

汇编 ? cdecl 函数调用约定,stdcall 函数调用约定

DLL-动态链接库(导入导出符/调用约定)

_STDCALL&_CDECL 调用约定

调用约定_stdcall _cdecl _fastcall的区别

为啥 Cdecl 调用在“标准”P/Invoke 约定中经常不匹配?

为啥 Cdecl 调用在“标准”P/Invoke 约定中经常不匹配?