本章内容为导入表和导出表的前置内容
章节引入
当我们需要重复使用一段代码时,我们可以将其定义成一个函数,每当使用这段代码时,直接调用函数即可
但当我们进行多人大型项目时,一个人电脑上有一段代码需要另外很多人的电脑使用时,每次复制粘贴相应的代码到另外很多台电脑是很复杂的,为了使代码复用简单化,我们可以通过下面的三种方式进行实现:
1.静态链接库
2.动态链接库
3.def导出
静态链接库
静态链接库的创建
以VC6为例,创建静态链接:
在VC6中创建项目,选择Win32 Static Library,进入下一步
由于目前还不需要什么支持,所以不用选选项,直接点finish即可
在项目中点击ClassView–右键刚创建的项目–New Class,新增一个类,取一个名字,就生成了对应的源文件和头文件:xxx.h 和 xxx.cpp
如下我们将新增类命名为text
创建完毕以后有如下两个文件夹,一个头文件一个源文件
此时在cpp源文件中写入要给别人复用的函数
如下我们定义了加减乘除的函数
按下F7进行编译,然后在创建的静态链接库的项目文件夹中就会生成一系列的文件。其中有一个**.h的头文件。打开文件夹中的Debug文件夹,有一个**.lib文件。这两个文件就是使用静态连接库需要的两个文件.
静态链接库的使用
通过使用静态链接库,可以让别人轻松的实现对你的代码的复用
方法一
1.将我们需要的两个文件xxx.h 和 xxx.lib复制到需要使用静态链接库的.cpp文件所在的控制台项目文件夹中,也就是我们平时写代码用的项目WorkSpace
注意不要复制在Debug文件夹中!注意和动态链接库区分开
2.在需要使用静态链接库的.cpp文件中导入头文件,用来告诉编译器要复用的函数的地址:
格式:#include "xxx.h"
3.在需要使用静态链接库的.cpp文件中导入Lib文件,该文件告诉编译器导入的函数的二进制代码,即硬编码
格式:#pragma comment(lib, "xxx.lib")
完成以上步骤以后,就意味着导入了头文件和lib文件,之后就可以直接在要编写的cpp文件中使用需要复用的函数了
代码如下:
方法二
1.将xxx.h 和 xxx.lib复制到要使用的项目文件夹中
2.在需要使用静态链接库的文件中写入#include "xxx.h"
3.在文件所在的Project右键–setting–Link–在Library modules中添加需要导出的静态链接库TestLib.lib即可
如上图可以发现我们平时就一直在使用一些静态链接库。比如在我们使用memsize和memcpy这些函数时,我们并没有定义这些函数,但可以直接使用。这是因为我们通过#include包含了函数所需要的头文件,并且在上图的link中也默认包含了这个函数所在的.lib文件,比如我们平时用的printf函数,它所在lib就被默认写到Library modules中了,因此我们可以直接使用这些函数。
此时便可在程序中使用静态链接库的函数了
静态链接库的特点
优点:实现了代码复用
缺点:在编译程序中,直接将静态链接库的代码添加到了源文件中。当我们对静态链接库修改时,需要重新编译源文件
具体解释:
1.静态链接库是一种假的模块化,这是因为当一个程序使用了静态链接库的时候,相当于这个程序中使用了别的文件中的函数,这就有了类似于模块化的感觉。
2.我们前面学过,一个PE文件中包含很多模块(.dll),可以通过OD中的E来查看。此时我们将我们自行添加了静态链接库的文件通过OD打开,但是我们并没有在PE文件模块中发现我们添加的静态链接库。这是因为程序在编译的时候,直接把静态链接库的代码添加到了源程序中。所以静态链接库和模块化是没有什么关系的
下图便是我们在OD中具体解释:
如下是程序的具体解释
下图是我们使用的Plus函数在反汇编中的形式:
可以发现程序使用的函数所在的内存地址为0x00401190,一般来说一个.exe文件的代码块差不多就是从偏移地址为1000多的地方开始的(即ImageBase0x400000 + 偏移0x1000),由此此时这个函数是属于.exe文件中的一部分,而不是使用其他模块.dll中的函数。
此时又可以进一步解释,静态链接库在程序编译时直接加到程序中了
但是如果一个.exe文件包含.dll,使用了.dll中的函数,此时如果修改函数,.exe文件就不再次编译。这就是模块化的好处,也是动态链接库的好处
动态链接库
动态链接库的创建
1.创建DLL
创建一个新的项目–选择Win32 Dynamic-Link Library–起名–选择A empty DLL project(空的)即可
同样点Class View–在创建的项目右键–New Class–输入名字,即可创建好.cpp文件和.h文件
2.在.cpp源文件中:定义函数
3.在.h头文件中声明(与静态链接库不同)
extern:表示这是一个全局函数,可以供各个其他函数调用
“C”:指的是此函数按照C语言的方式进行编译、链接。因为不指定C的话,编译器可能会理解成通过C++的方式导出,但是C++允许函数的重载(可以定义相同名字的函数,但参数不同),而假如此时编译器导出后有相同名字的函数,但是此时有一个C程序要使用,但C不支持重载,那么此程序就不知道用哪个函数。①故如果没有指定"C",编译器会自动在导出.dll的时候把当中定义的所有函数名都改了,保证不出现同名函数,这样任何程序使用此动态链接库中函数时就不会出现同名的情况。②如果加上"C",因为C中不能出现同名函数,所以你在动态链接库中定义的函数名是啥就导出啥,不会改名。
假如现在不加"C",我们看看导出的.dll文件中的函数名是什么样的:
使用VC6自带的工具:depends.exe,查看编译后导出的dllTest.dll(可以查看PE文件依赖于哪些动态链接库以及使用了动态链接库中的哪些接口)
由此可以发现,编译器把我们定义的函数名进行了修改
假如现在按照C语言的方式导出(不加__stdcall),就不会改名字,定义的函数是什么名字导出的.dll中就是什么名字。
如果此时加上"C"和__stdcall,则在原来函数名的基础上,前面加_,后面加@8,8表示函数有两个4字节宽度的参数。
_declspec(dllexport):导出函数的关键字。指告诉编译器此函数为导出函数,可以供别人使用(一定要写的,固定格式)
__stdcall:就是函数的调用约定使用stdcall, stdcall调用约定的函数会使用内平栈,如果不加的话VC默认使用cdcall,外平栈。建议如果在Windows下使用动态链接库最后都使用stdcall的调用约定导出.dll
动态链接库的使用
方法一
隐式链接:.dll提供了函数,.exe使用函数时通过编译器去找函数,跟程序本事没关系
1.将xxx.dll ,xxx.lib 放到需要使用动态链接库的工程目录(创建的WorkSpace下面)下面(前面如果有静态链接库的.lib和.h记得先删掉)
说明:after_day34是写代码的WorkSpace,有些VC版本直接将after_day34.exe放到工程目录下,有些会创建一个Debug文件夹,如果after_day34.exe在Debug中。只要把dll和lib跟after_day34.exe放到一个文件夹下即可。静态链接库的.h和.lib文件要放到工程目录下面,不是Debug下面
此时.lib中不像静态链接库会存放函数的硬编码,对于动态链接库.lib只是告诉编译器要使用的函数在哪里。真正存放代码(函数)的地方是.dll中
2.在要使用动态链接库的源文件中写入 #pragma comment(lib,"xxx.lib"),告诉编译器要使用动态链接库的函数
3.加入函数的声明
extern "C" __declspec(dllimport) __stdcall int Plus (int x,int y);
extern "C" __declspec(dllimport) __stdcall int Sub (int x,int y);
extern "C" __declspec(dllimport) __stdcall int Mul (int x,int y);
extern "C" __declspec(dllimport) __stdcall int Div (int x,int y);
__declspec(dllimport):告诉编译器此函数为导入函数,说明这是使用动态链接库的函数
与上面导出时规定的要一致:导出时有extern "C",导入时也要有
方法二
显示链接:编译器不去链接动态链接库,操作者手动改链接动态链接库
1.定义函数指针
typedef int (__stdcall *lpPlus)(int,int);
typedef int (__stdcall *lpSub)(int,int);
typedef int (__stdcall *lpMul)(int,int);
typedef int (__stdcall *lpDiv)(int,int);
函数在导出时定义了__stdcall,这里定义函数指针时也需要加上__stdcall。导出导入格式要对应
特别说明:
HINSTANCE:在win32下与HMODULE是相同的东西(Win16 遗留问题)
HMODULE:代表应用程序载入的模块(本例中hModule的值就是给定dll的ImageBase,相当于dll在内存中拉伸后的起始地址)
HANDLE:代表系统的内核对象,如文件句柄,线程句柄,进程句柄。
HWND:是窗口句柄
这三个本质上就是一种类型—无符号整型,它们本质上就是一个编号。windows之所以设计成三个不一样的名字,是因为可读性更好,也可以避免在无意中对这个类型的变量进行运算
综上所述,这便是我们要定义不同的函数指针的原因
2.声明函数指针变量
lpPlus myPlus;
lpSub mySub
lpMul myMul
lpDiv myDiy
3.动态加载dll到内存中
#include <Windows.h> //LoadLibrary函数调用需要包含Windows.h头文件
HMODULE hModule = LoadLibrary("xxx.dll"); //你的dll取的什么名字就写什么,作用是将dll加载到内存中
给函数指针赋值,即指向具体函数的地址
4.获取函数地址
lpPlus myPlus = (lpPlus)GetProcAddress(hModule,"_Plus@8"); //这里的函数名要注意,这是加上_stdcall的形式
lpSub mySub = (lpSub)GetProcAddress(hModule,"_Sub@8");
lpMul myMul = (lpMul)GetProcAddress(hModule,"_Mul@8");
lpDiv myDiv = (lpDiv)GetProcAddress(hModule,"_Div@8");
如果导入时格式为"C" __stdcall,那么这里的函数名就是_函数名@参数大小这种格式。如果只有"C",但是没有__stdcall,那么这里的函数名就是在dll中定义的函数名本身。
GetProcAddress()函数说明:
功能:显式链接时使用,用于获取DLL中导出函数的地址
参数:
1.使用该函数前先动态加载dll:HMODULE hModule = LoadLibrary("xxx.dll");,这里就是把你的dll载入后赋给HMODULE类型的变量hModule,那么此时hModule就代表了你的dll载入的模块
2.GetProcAddress()函数第一个参数就表示已经载入的DLL,第二个参数是DLL当中导出函数的名称
返回值:将DLL中导出函数的地址赋给相应的函数指针,接下来就可以通过函数指针来调用函数了
5.调用函数
int a = myPlus(10,2); //10+2
int b = mySub(10,2); //10-2
int c = myMul(10,2); //10*2
int d = myDiv(10,2); //10/2
动态链接库的特点
使用动态链接库的方式,编译的.exePE文件中包含了模块,动态链接库是真正的模块化,.exe文件中包含了.dll(即模块),所以才能使用.dll中的函数。我们使用OD查看一下使用了.dll中的函数的C程序
我们再通过反汇编查看一下这个C程序使用的Plus的函数地址是多少?发现是0x10001060这个地址明显不属于.exePE文件的某个节,而是.exe文件去调用dllTest.dll模块中的函数
正因为.exe中使用的函数不在.exe中,而是在.dll模块中,所以如果对.dll中的函数进行优化改动,就不用让.exe重新编译,直接在.dll中修改完后,.exe文件中使用的函数就是修改后的。
因此模块化的好处就是,哪里有问题就改哪个模块。比如做一个项目时,把整个应用按照不同的功能分成不同的dll文件,即分成不同的模块,有的.dll负责通信,有的.dll负责处理等等,此时想修改哪一个功能,不用动整个项目,而是对.dll文件做修改即可
使用.def导出
导出函数的方式:
1.以名字的方式导出(编译器会默认给函数添加序号)
2.以序号的方式导出(以序号的方式导出,再加上NONAME关键字,就只以序号的方式导出,函数没有名字)
以序号的方式导出dll中的函数,就是.def导出,需要在使用动态链接库的项目WorkSpace中的.h,.cpp,.def文件中进行相应的操作。好处是隐藏了函数名,防止对方通过逆向分析得到函数名字,进而猜到函数的功能,对程序造成破坏。这种方式导出函数更具安全性
1.在.cpp源文件中定义函数:
2.在.h头文件中声明函数:
不再使用dllexport这种形式定义,直接定义成普通函数的样子就可以
3.创建.def文件,命名函数编号:
在动态链接库项目WorkSpace下,点击File–new–选择Text File,取名字,后缀名为.def
文件中写入如下代码:
其中Export表示导出
Plus @12:表示Plus函数的导出序号为12
Sub @15 NONAME:表示Sub函数的导出序号为15。NONAME关键字表示Sub函数只有序号,没有名字,这样做就可以将Sub函数名字隐藏
动态链接库的那种以名字的导出方式也会给函数默认生成一个导出序号;.def导出的方式的序号是自己随便定义的
现在对程序进行编译,使用DEPEND.exe查看一下函数导出后的情况:发现Plus函数前面的序号为12,Mul和Div函数前面的序号分别是13、16,但是序号为15的Sub函数名字并没有显示出来,即函数没有名字,这就起到了在.dll中隐藏函数名的作用
序号和函数
在.def导出中,我们只定义了4个函数导出。但是由于四个函数的序号最小是@12,最大是@16,而系统计算导出函数个数的方式是通过最大序号 - 最小序号 + 1,所以最后会有五个导出函数,多出来一个@14,此函数入口地址用全0表示,因此这是一个虚假的函数,但是其序号位置是预留出来的
另外添加了NONAME关键字导出的序号为@15的函数,其名字也和@14一样为空,但是是有地址的,说明它是一个真实的函数