在学习导入表、导出表之前,由于导入导出表需要用到静态链接库或者动态链接库,所以先来学习一下
一、代码复用的实现
- 我们学过如果一个功能的代码需要被我们自己反复使用,即复用,那么可以定义成函数
- 现在如果要多人实现代码的复用,比如我写的一段代码要给别人用,不可能直接把代码复制一份发给别人,这样如果很多人要使用,就不方便,所以可以通过下面的三种方式实现:
- 静态链接库
- 动态链接库
- def导出
二、静态链接库
1.创建静态链接库
-
在VC6中创建项目:
Win32 Static Library
;由于目前还不需要什么支持,所以不用选选项,直接点finish即可 -
在项目中创建两个文件:xxx.h 和 xxx.cpp(点击
ClassView
–右键刚创建的项目–New Class
,取一个名字test,则现在就生成了对应的源文件和头文件) -
在cpp文件中写要给别人复用的代码:(定义)
int Plus(int x, int y){ return x+y; } int Sub(int x, int y){ return x-y; } int Mul(int x, int y){ return x*y; } int Div(int x, int y){ return x/y; }
-
在.h头文件中声明:(声明)
int Plus(int x, int y); int Sub(int x, int y); int Mul(int x, int y); int Div(int x, int y);
-
最后按F7编译,那么在创建的项目文件夹中,有一个**.h的头文件**;再打开当中的Debug文件夹,就会发现有一个**.lib文件**。这两个文件就是使用静态连接库需要的两个文件
2.使用静态链接库
-
通过使用静态链接库,实现代码复用:
-
方法一:
-
将xxx.h 和 xxx.lib复制到需要使用静态链接库的.cpp文件所在的控制台项目文件夹中(就是我们平时写代码用的项目WorkSpace)
注意:不是Debug中!注意和动态链接库区分开
-
在需要使用静态链接库的.cpp文件中包含头文件:
#include "xxx.h"
先包含.lib的.h头文件,告诉编译器一会要使用的函数在哪
-
在需要使用的.cpp文件中包含Lib文件:
#pragma comment(lib, "xxx.lib")
再告诉编译器要使用的函数是什么,即告诉编译器此函数的硬编码,因为静态链接库函数的硬编码就在.lib文件中,上面的.h中只是申明、告诉编译器函数在哪
-
接着就可以使用.lib中写好的函数了
-
-
方法二:
-
将xxx.h 和 xxx.lib复制到要使用的项目文件夹中
-
在需要使用的文件中包含:
#include "xxx.h"
-
在文件所在的Project右键–setting–Link–在Library modules中添加导出的静态链接库TestLib.lib即可
我们可以发现我们平时就一直在使用静态链接库:memsize和memcopy这些函数,为什么这些函数我们并没有定义过,只是#include 包含了头文件,就可以直接使用了呢?因为include头文件中只是申明了这些函数,而真正告诉编译器这个函数具体是什么的,是上面的link中已经包含了这个函数所在的.lib文件;包括我们平时用的printf函数,它所在lib也已经默认写到Library modules中了
-
导入头文件和lib文件后就可以直接使用当中的函数了
-
3.静态链接库的特点
-
我们发现静态链接库是一种假的模块化,我们前面学过,一个PE中可以包含很多模块(.dll),可以通过OD中的
E
来查看。但是此时使用静态链接库,虽然一个程序中使用了别的文件中的函数,有点类似于模块化的感觉,但是我们却发现其实这个程序是没有使用其他模块的。所以静态链接库和模块化是没有什么关系的 -
那么为什么使用静态链接库的程序,就可以使用库中的函数了呢?
-
我们找一下我们使用的Plus函数在哪里?
-
可以发现使用的函数所在的内存地址为0x00401190,我们前面学过PE文件了,一般一个.exe文件的代码块差不多就是从偏移地址为1000多的地方开始的(即ImageBase0x400000 + 偏移0x1000),所以此时这个函数就属于.exe文件中的一部分了,而不是使用其他模块.dll中的函数
-
-
总结:静态链接库的特点就是使用静态链接库的.exe程序直接将要用到的函数在编译的时候就直接放到.exe文件中代码块中去了
-
这恰恰也是静态链接库的缺点,比如如果我们现在修改了.lib中的函数,那么此时使用此函数的.exe文件必须要重新编译一次,将修改过的函数重写编译到.exe文件中才能使用修改过后的函数
但是如果一个.exe文件包含.dll,使用了.dll中的函数,此时如果修改函数,.exe文件就不用动,即不用再重新编译一次。这就是模块化的好处!!
三、动态链接库
1.创建DLL
-
创建一个新的项目–选择
Win32 Dynamic-Link Library
–起名–选择A empty DLL project
(空的)即可 -
同样点
Class View
–在创建的项目右键–New Class
–输入名字,即可创建好.cpp文件和.h文件 -
在.cpp源文件中:定义函数
int __stdcall Plus(int x,int y){ return x+y; } int __stdcall Sub(int x,int y){ return x-y; } int __stdcall Mul(int x,int y){ return x*y; } int __stdcall Div(int x,int y){ return x/y; }
-
在.h头文件中声明(与静态链接库不同)
extern "C" _declspec(dllexport) __stdcall int Plus (int x,int y); extern "C" _declspec(dllexport) __stdcall int Sub (int x,int y); extern "C" _declspec(dllexport) __stdcall int Mul (int x,int y); extern "C" _declspec(dllexport) __stdcall int Div (int x,int y);
-
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都加
"C" __stdcall
,则在原来函数名的基础上,前面加_
,后面加@8
,8表示函数有两个4字节宽度的参数。
-
-
_declspec(dllexport):指告诉编译器此函数为导出函数,可以供别人使用(一定要写的,固定格式)
-
__stdcall:就是函数的调用约定使用stdcall,我们前面学过,stdcall调用约定的函数会使用内平栈,如果不加VC默认使用cdcall,外平栈。建议如果在Windows下使用动态链接库最后都使用stdcall的调用约定导出.dll
-
2.使用DLL
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中
-
将
#pragma comment(lib,"xxx.lib")
添加到调用文件中(告诉编译器要使用动态链接库的函数) -
加入函数的声明
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"
,导入时也要有 -
为什么叫隐式链接:.dll提供了函数,.exe使用函数,隐式链接即.exe找函数是通过编译器去找,跟程序本事没关系
2)显示链接
-
定义函数指针
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
。导出导入格式要对应 -
动态加载dll到内存中
#include <Windows.h> //LoadLibrary函数调用需要包含Windows.h头文件 HMODULE hModule = LoadLibrary("xxx.dll"); //你的dll取的什么名字就写什么
特别说明:
- HINSTANCE:在win32下与HMODULE是相同的东西(Win16 遗留)
- HMODULE:代表应用程序载入的模块(本例中hModule的值就是给定dll的ImageBase,相当于dll在内存中拉伸后的起始地址)
- HANDLE:代表系统的内核对象,如文件句柄,线程句柄,进程句柄
- HWND:是窗口句柄
这三个本质上就是一种类型—无符号整型;windows之所以设计成三个不一样的名字,是因为可读性更好,也可以避免在无意中对这个类型的变量进行运算
-
给函数指针赋值,即指向具体函数的地址
lpPlus myPlus = (lpPlus)GetProcAddress(hModule,"_Plus@8"); //这里的函数名要注意! 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中导出函数的地址
- 参数:①先动态加载dll–
HINSTANCE hModule = LoadLibrary("xxx.dll");
,这里就是把你的dll载入后赋给HINSTANCE类型的变量hModule,那么此时hModule就代表了你的dll载入的模块;②所以GetProcAddress()函数第一个参数就表示已经载入的DLL;第二个参数是DLL当中导出函数的名称 - 返回值:将DLL中导出函数的地址赋给相应的函数指针,接下来就可以通过函数指针来调用函数了
- ==GetProcAddress()==函数说明:
-
调用函数
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
3.动态链接库的特点
-
我们发现如果使用动态链接库的方式,编译的.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导出
以序号的方式导出dll中的函数;一定要好好看,和导出表的内容联系起来
1.为什么使用.def导出
- 我们如果通过上面的方法,将我们写的代码导出,对方可以通过逆向分析得到函数名字,就可以猜到函数的功能,为了不让别人分析我们的.dll,可以通过.def导出的方法,即通过序列号代替函数名的方式,可以达到隐藏的目的
2…def导出方法
需要在动态链接库的项目WorkSpace中的.h,.cpp,.def中操作!
![]()
-
在.h头文件中:
int Plus(int x,int y); int Sub(int x,int y); int Mul(int x,int y); int Div(int x,int y);
不用dllexport这种形式定义了,直接定义成普通函数的样子就可以
-
在.cpp源文件中:
int Plus(int x,int y){ return x+y; } int Sub(int x,int y){ return x-y; } int Mul(int x,int y){ return x*y; } int Div(int x,int y){ return x/y; }
-
创建.def文件,内容如下:
EXPORTS Plus @12 Sub @15 NONAME Mul @13 Div @16
-
Export表示导出
-
Plus @12:表示Plus函数的导出序号为12
-
Sub @15 NONAME:表示Sub函数的导出序号为15;NONAME关键字表示Sub函数只有序号,没有名字,这样做就可以将Sub函数名字隐藏(学完导出表后,就可以知道怎么调用这种没有名字的函数)
-
动态链接库的那种以名字的导出方式也会给函数默认生成一个导出序号;.def导出的方式的序号是自己随便定义的
-
在动态链接库项目WorkSpace下,点击
File
–new
–选择Text File
,取名字,后缀名为.def -
接着在创建的.def中添加代码
-
现在编译一下,使用DEPEND.exe查看一下函数导出后的情况:发现Plus函数前面的序号为12,Mul和Div函数前面的序号分别是13、16,但是序号为15的Sub函数名字并没有显示出来,即函数没有名字!就起到了在dll中隐藏函数名的作用
-
3.序号和函数的关系
-
我们只定义了4个函数导出,但是由于四个函数的序号最小是@12,最大是@16,而系统计算导出函数个数的方式是通过最大序号 - 最小序号 + 1,所以会发现有五个导出函数,那多出来的@14怎么办?就用全0表示即可,但是序号位置是预留出来的
day36学导出表时,注意与AddressOfFunctions表、AddressOfNameOrdinals表和Base字段联系!!!
-
另外添加了NONAME关键字导出的序号为@15的函数,会发现名字也和@14一样为空,但是是有地址的
4.导出函数的方式
记住两种方式,导出表的时候会用到
-
以名字的方式导出(编译器会默认给函数添加序号)
-
以序号的方式导出(以序号的方式导出,再加上NONAME关键字,就只以序号的方式导出,函数没有名字)