一、生成动态库(含头文件、不含头文件)
以生成dllTest.dll为例(工程名为dllTest、 头文件名为dllTest.h、 源文件名为dllTest.cpp)
1.1 不含头文件的动态库
我们生成的动态库想要被别人调用,那么一定要将想要被调用的函数导出,使用_declspec(dllexport)进行导出。
//dllTest.cpp
_declspec(dllexport) int add(int a, int b)
{
return a+b;
}
_declspec(dllexport) int sub(int a, int b)
{
return a-b;
}
编译之后,在工程目录的Debug目录下我们可以看到以下的几个文件,分别是
得到上述的文件之后,我们就可以进行调用该动态链接库了。其中,dll文件是包含了函数具体实现的可执行文件;lib文件是导入库文件,主要包含的是函数名和符号名。我们可以用vs提供的dumpbin工具查看生成的动态库中导出的函数以及名字。我的vs安装在C:\Program Files (x86)\Microsoft Visual Studio 10.0,进入该文件夹后点击VC进入该目录,可以看到一个vcvarsall.bat的文件。依次点击Microsoft Visual Studio 2010 -> visual studio tools ->Visual Studio Prompt 2010,如下图所示:
即可打开一个vs的命令框,将vcvarsall.bat文件拖拽进入该命令框中,将得到下图所示的内容:
然后我们将得到的dllTest.dll文件放入到VC文件夹中,输入dumpbin -exports dllTest.dll即可看到该动态库导出的函数以及经过c++编译器修饰后的函数名字。
椭圆所示的即为导出的函数以及其经过修饰后的名字。(因为c++要支持函数重载以及命名空间等,因此需要将函数进行修饰)。
1.2 动态库调用(隐式连接、动态连接)
1.2.1 隐式连接
首先建立一个工程,配置工程属性,具体的步骤如下:
(1)新建一个win32控制台应用程序,工程名dllCall;
(2)配置工程属性,添加对动态库dllTest.lib的引用:
a. 工程项目右键->属性->链接器->gengeral->附加库目录,在其中添加导入库dllTest.lib所在的文件目录,如下图:
b.工程项目右键->属性->链接器->输入->附加依赖项,在其中添加导入库dllTest.lib,如下图:
(3)此时,我们就可以在我们的工程中对该动态库进行调用了。
调用函数dllCal.cppl如下:
<pre name="code" class="cpp">// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
using namespace std;
extern int add(int, int ); //告诉编译器,add函数是在该源文件外部定义的函数
_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
(4)编写好之后,编译、链接都能通过,但是运行的时候会出错,因为调用函数不知道int add(int, int ) 以及int sub(int, int)的可执行文件(dllTest.dll)在哪,因此当调用该函数时,就找不到具体的执行的代码,因而就会报错。此时,我们只需将dllTest.dll放入到调用函数的.exe文件所在的目录中即可,结果如下图:
上述的两种形式都能正常的调用。
1.2.2 动态调用
上述的隐式调用需要我们在工程属性中配置一些动态库导入库(dllTest.lib)的目录以及名称,会很麻烦,有时候我们也会忘记配置这些属性或者当动态库较多的时候有遗漏,都会导致函数链接的时候出现unresolve external symbol的错误。而且,动态调用还有一个优点就是,什么时候需要调用动态库的函数的时候什么时候加载该动态库,这样就不必在程序运行开始时加载所需的所有的动态库,这样也能加快启动的速度。此外,我们仅仅只需要一个dllTest.dll文件即可。
(1)我们首先删掉工程属性中 “附加库目录”以及“附加依赖项”中我们输入的内容。
(2)修改相应的调用代码,如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include<Windows.h>
using namespace std;
//extern int add(int, int ); //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
HINSTANCE hInst = LoadLibraryA("dllTest.dll");
typedef int (*pFun)(int, int);//定义一个函数指针类型pAdd
pFun add = (pFun)GetProcAddress(hInst,"?add@@YAHHH@Z");
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
pFun sub = (pFun)GetProcAddress(hInst,"?sub@@YAHHH@Z");
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
(3)程序运行正常,结果同上面的结果。
2.含头文件的动态库
我们一般用编写一个动态库,同时也会提供一个头文件方便调用者使用,因为调用者一般情况下很难知道我们编写的动态库具体导出的函数。而且,我们不想想上面的函数调用一样,还要自己写_declspec(dllimport) int add(int, int)或者extern int add(int, int);这是我们需要向调用者提供一个头文件完成这项工作,是调用变得更加方便。
头文件:
//dllTest.h
#ifdef DLLTEST_API
#else
#define DLLTEST_API _declspec(dllimport)
#endif
DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);
</pre><pre name="code" class="cpp"><span style="font-family: Arial, Helvetica, sans-serif;"> </span><span style="font-family: Arial, Helvetica, sans-serif;">源文件:</span>
<pre name="code" class="cpp">//dllTest.cpp
#define DLLTEST_API _declspec(dllexport)
#include "dllTest.h"
DLLTEST_API int add(int a, int b)
{
return a+b;
}
DLLTEST_API int sub(int a, int b)
{
return a-b;
}
2.1 函数调用(隐式链接、动态链接)
2.1.1 隐式链接
(1) 新建工程,配置工程属性(添加导入库目录、导入库),具体的参照上面的隐式调用。此外,还要将动态库头文件所在的目录加入到属性中: 在 c\c++ -> gengeral -> additional include Directories 加入dllTest.h所在的目录。 如下图:
(2)修改调用函数,具体的代码如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
using namespace std;
//extern int add(int, int ); //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
(3)编译、链接、运行正常,结果如上面。此外,我们也可以不用在属性->链接器->输入->附加库目录中添加dllTest.lib, 通过在调用函数中添加#pragma comment(lib,"dllTest.lib")即可。具体代码为:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include "dllTest.h"
#pragma comment(lib,"dllTest.lib")
using namespace std;
//extern int add(int, int ); //告诉编译器,add函数是在该源文件外部定义的函数
//_declspec(dllimport) int sub(int, int);//告诉编译器,sub函数是从动态库导入的函数
//这两种方式都可以正常的调用,但是下面的相对来说加载的更快一些
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
2.1.2 动态链接
动态链接同上面的一样。
3 不改变名字的导出库
但是我们发现,通过动态链接时,那个函数的名字经过c++编译器修饰后很复杂,刚开始并不熟悉修饰的规则(后面讲修饰规则)。因此,如果我们需要先用dumpbin工具查询该动态库导出的函数及其名字,才方便调用。那么,怎么才能使导出的函数名字不发生变化呢,同我们定义的函数名一样,这样我们在动态调用时更加方便。具体的方法有两种:一是采用extern "C";二是用模块定义文件.def。
3.1 extern "C"
3.1.1 默认调用方式_cedel
以含头文件的动态库为例进行修改,修改后的头文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else
#define DLLTEST_API extern "C" _declspec(dllimport)
#endif
DLLTEST_API int add(int, int);
DLLTEST_API int sub(int, int);
//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"
DLLTEST_API int add(int a, int b)
{
return a+b;
}
DLLTEST_API int sub(int a, int b)
{
return a-b;
}
进行编译得到相应的.dll、.lib文件,通过dumpbin工具,我们可以查看导出函数的名字,如下:
红色部分为 没有加extern "C" 导出的函数名,它经过了c++编译器的修饰。
绿色部分为添加了extern "C" 导出的函数名,它的意思是告诉编译器,以C的方式导出函数。
3.1.2 更改调用方式 _stdcall
但是如果改变了调用方式,通过这种方式进行导出,函数的名字依旧会改变,我们采用_stdcall调用方式,也就是
WINAPI,后面讲为什么window API 函数都采用该种调用方式:
(1)编写_stdcall调用方式的动态库(以含头文件方式为例)
相应的头文件和源文件如下:
//dllTest.h
#ifdef DLLTEST_API
#else
#define DLLTEST_API extern "C" _declspec(dllimport)
#endif
DLLTEST_API int _stdcall add(int, int);
DLLTEST_API int _stdcall sub(int, int);
//dllTest.cpp
#define DLLTEST_API extern "C" _declspec(dllexport)
#include "dllTest.h"
DLLTEST_API int _stdcall add(int a, int b)
{
return a+b;
}
DLLTEST_API int _stdcall sub(int a, int b)
{
return a-b;
}
编译后利用dumpbin工具查看导出函数的名字如下:
红色的为采用c\c++默认调用方式导出的函数名字;
绿色的为采用_stdcall 调用方式导出的函数名字。至于为什么会是这样的名字在后面的名字修饰规则中进行说明。我们发现他的名字还是改变了。
3.2 模块定义文件.def
3.2.1 默认调用方式(_cedel)动态库
(1)新建一个Win32程序,选择一个动态库程序,勾选空工程。
(2)修改相应的代码(源文件、模块定义文件),具体的如下:
源文件:
//dllTest.cpp
int add(int a, int b)
{
return a+b;
}
int sub(int a, int b)
{
return a-b;
}
模块定义文件:
LIBRARY dllTest
EXPORTS
add
sub
其中,LIBRARY 后面要与生成的动态库的名称相同,EXPORTS下面写需要导出的函数(add、sub),它会自动与你的源文件中的函数进行匹配。也可以用 add1 = add、 sub1 = sub 这样的方式来改变导出函数的名字。
(3)通过dumpbin工具查看动态库的导出函数,如下图(分别是没改名字以及改名字后的):
3.2.2 采用_stdcall,也就是WINAPI调用方式生成动态库
(1) 源文件
//dllTest.cpp
int _stdcall add(int a, int b)
{
return a+b;
}
int _stdcall sub(int a, int b)
{
return a-b;
}
(2)模块定义文件
LIBRARY dllTest
EXPORTS
add
sub
(3)利用dumpbin查询动态库导出的函数
3.3 动态库调用
3.3.1 动态调用
动态调用同上,只是变了一个地方,如下:
<span > </span>pFun add = (pFun)GetProcAddress(hInst,"add");
<span > </span>pFun sub = (pFun)GetProcAddress(hInst,"sub");
也可以使用每个函数前面的编号,如add函数的编号为1 、sub函数的编号为2。只需要将上述函数的第二个参数改为
MAKEINTATOM(1)。
3.3.2 隐式调用
<1> 动态库与函数库都采用_cedel调用方式
(1) 配置工程属性,加入附加库目录;(附加依赖项使用 #pragma comment(lib,"testdll.lib"代替)
(2)调用函数如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;
_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
(3)编译、链接正常,结果正确。
<2>动态库函数采用_stdcall调用方式,调用方采用默认的调用方式(_cedel)
(1) 新建工程,配置工程属性,加入附加库目录; (附加依赖项使用#pragma comment(lib,"testdll.lib"代替)
(2)编写调用函数,如下:
// dllCall.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#pragma comment(lib,"testdll.lib")
using namespace std;
_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int _tmain(int argc, _TCHAR* argv[])
{
int a = 5, b = 3;
cout<<a<<" + "<<b<<" = "<<add(a,b)<<endl;
cout<<a<<" - "<<b<<" = "<<sub(a,b)<<endl;
return 0;
}
(3)编译、链接正常,运行出错。
这是因为_stdcall是被调用的函数自己清理栈空间,而_cedel则是调用者清理栈。上述的调用方式会使栈得到两次清理,使得函数的返回地址、ebp寄存器的值被更改从而导致失败。通过观察其生成的汇编语言就可以看到(下面的主要的,并不是全部):
main函数在调用add函数之后清理的栈,而在add函数中,可以清楚的看到,add函数在运行完之后自己清理的栈。
因此,导致栈数据出现错误。
c\c++编译器修饰规则和函数调用中栈的变化后面有时间在写!!!