动态链接库是一个包含可由多个程序同时使用的代码和数据的库,使进程可以调用不是属于其可执行代码的函数,多个进程调用同一个DLL只需向内存加载一份该DLL。静态链接库lib 指令都直接包含在最终生成的exe文件中,而dll不包含在exe文件中,exe文件可以动态的加载与卸载 dll文件。
动态链接库分 win32动态链接库和MFC动态链接库
Win32动态链接库 代码运行速度快,建立的动态链接库可以被各种语言调用,但不能使用MFC类库。
MFC动态链接库分为:
1、 MFC规则动态链接库,由它生成的动态链接库包含一个继承自CWinApp的类,但无消息循环
2、 MFC扩展动态链接库, 由它生成的动态链接库只能被MFC类库所编写的应用程序调用
DLL的编制与语言和编译器无关,只要遵循约定的DLL接口规范和调用方式,各种语言写的DLL都可以相互调用
函数的调用和链接规范
函数的调用规范决定了函数调用时,实参压栈、退栈及堆栈释放方式,函数名改变的方案(命名规范)。
1、__stdcall调用规范
常用C语言调用规范,成为pascal 调用约定,含义与WINAPI相同。使用它定义的函数从右向左压栈,退栈时函数自己清理堆栈。编译器会在函数名前面加一个下划线,后面跟一个@符号,@符号后紧跟参数所占的字节数。
如 int __stdcallADD(int a, int b)
执行参数传递时,先把b压入堆栈,然后把a压入堆栈,再调用ADD函数求和,调用完成后,函数退出,并清理这8个字节的堆栈。函数编译后,函数名称为_ADD@8
2、__cdecl调用规范
C语言默认的调用方法。从右向左入栈,函数退出时,由函数的调用者清理堆栈,所以函数的参数个数可变。 对不同语言的编译器产生堆栈的方式不同,因此采用该方法有可能不能正确的清除堆栈。 编译后的函数为原函数名前加下划线。
Int __cdecl ADD(int a, int b) 编译后为 _ADD
3、__fastcall
调用速度较快的调用方法。从右向左参数入栈,最左面的两个参数被放到寄存器中,较快调用速度。退出时函数自己清理堆栈。编译后 函数名前加@,函数名后加@和压入堆栈(包含寄存器)的参数的字节数。对于大多数调用都使用少于两个的参数。
Int __fastcall ADD(int a, int b) 编译后 @ADD@8
4、 thiscall 调用规范
C++类成员函数默认的调用约定。如果参数个数确定,this指针通过ecx传递给被调用者,被调用函数清理堆栈;如果参数个数不确定,this指针在所有参数压栈后被压入堆栈,调用者自己清理堆栈。
class A
{
Int ADD(int a, int b);
Int SUM(int a, ….);
}
Int A::ADD(int a , int b)
{
Return a+ b;
}
Int A::SUM(int a, …)
{
Int sum = 0, I = a;
Va_list marker;
Va_start(marker, a);
While(I != -1)
{
Sum+= I;
I = va_arg(marker, int);
}
Va_end(marker);
}
调用ADD函数时, 首先将两个参数b, a按照从右向左的顺序压入堆栈,再把this指针放入ecx寄存器中,调用完成后由ADD函数清理堆栈。
调用SUM函数时,参数从右向左入栈,this指针进入堆栈,调用完成后由调用者清理堆栈。
链接方式
1、 extern “C”链接方式
被extern “C”修饰的变量和函数是按照C语言方式进行编译和链接的。函数被C编译器编译后会根据调用方式不同添加不同的前后缀。 例如 void foot(int a,int b) (__cdecl)调用方式,编译后为_foot
2、 extern “C++” 编译连接方式
没有添加任何修饰符时,表示变量和函数是按照 C++(extern “C++”)方式编译和连接的。由于C++中存在函数重载,故不能使用extern “C”的方式进行编译链接方式,可能会造成函数名称冲突,因而根据调用方式不同C++链接方式会形成不同的函数名。
1)__stdcall 调用约定
A、 以? 标志函数名的开始,后跟函数名
B、函数名后面以@@YG标志参数表的开始
符号名 |
参数类型 |
符号名 |
参数类型 |
X |
void |
D |
char |
E |
unsigned char |
F |
short |
H |
int |
I |
unsigned int |
J |
long |
K |
unsigned long |
M |
float |
N |
double |
__N |
bool |
PA |
指针(*) |
PA 代表指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以“0“代替
C、参数表的第一项为该函数的返回值,其后一次为参数的数据类型,指针标志在其所指数据类型前。
D、参数表后以”@Z” 标志整个名字的结束,如该函数无参数,则以”Z”标志结束。
如void __stdcall foo(int x, int y)被编译成 ?foo@@YGXHH@Z
2)_cdecl调用约定
规则同__stdcall 调用约定,只是参数表的开始标志变为@@YA
Void foo(int a, int b)编译为 ?foo@@YAXHH@Z
3)__fastcall调用约定
规则同__stdcall调用约定,参数表开始标志为@@YI
动态链接库的引出
定义在动态链接库中的函数需要引出才能被其他可执行文件引用。两种引出方法:
1、使用def文件
模块定义文件(def文件)为连接器提供有关被连接程序的导出、属性、以及其他方面的信息。生成DLL时,def文件最有用。
在连接阶段可以使用/def(指定模块定义文件)连接器选项调用 .def文件:
/def:”文件名” 如 /def:”.link.def”
模块定义文件的构成:
1) DESCRIPTION “text”
DESCRIPTION 仅在生成虚拟设备驱动程序(VxD)时有用,用来标识该模块。
2) LIBRARY[library][BASE = address]
LIBRARY 语句通知连接程序创建一个DLL文件。链接程序同时创建导入库。
Library 参数指定DLL的名称,也可以使用/out 链接器选项指定DLL输出名。BASE = address参数设置操作系统用来加载DLL的基址。如果没有该项,则默认的基址为0x10000000
3) STACKSIZEreserve[, commit]
STACKSIZE 分配给该动态链接库的堆栈空间,默认1M。commit指定该动态链接库加载时分配的物理空间。
4) EXPORTS
在该定义文件中的主要作用是引出函数。格式为:
EXPORTS
definitions
definitions 可分为很多行,每一行的格式为:
entryname[=internalname][@ordinal][NONAME][PRIVATE][DATA]
entryname为引出文件的文件名。如果entryname与DLL定义的函数名不同,使用internalname来说明使用DLL文件的哪个函数。
@ordinal 允许指定序号而不是函数名进入DLL的导出表。有助于最小化DLL的大小。LIB文件将包含序号与函数之间的映射。
可选的NONAME关键字允许只按序号导出,并减少结果DLL中导出表的大小。但是,如果要在DLL上使用GetProcAddress,则必须知道序号,因为名称将无效。
可选的PRIVATE 关键字禁止将entryname放到由LINK 生成的导入库中。它对同样是由LINK 生成的图像中的导出无效。
可选的DATA 关键字指定导出的是数据,而不是代码
5) SECTIONSdefinitions
SECTIONS 语句引入了一个由一个或多个definitions组成的节。每个定义必须在单独一行上。.def文件可以包含一个或多个SECTIONS语句。
Definitions的格式为:
.section__name specifier
其中specifier可以为:EXECUTE READ SHARED WRITE
2、__declspec关键字方式
函数的引出可以使用 __declspec(extended-attribute) declaratory
__declspec用于指定所给类型的实例与Microsoft相关的存储方式。
extended-attribute参数如下,可同时出现,中间由空格隔开:
allocate(“segname”)
dllimport
dllexport
naked
noreturn
nothrow
novtable
thread
扩展属性allocate 是用来分配一个数据段。Segname 表示由#pargma data_seg,#paragma code_seg, #paragma init_seg, #paragma const_seg定义的数据段名。
扩展属性 dllimportdllexport用来从dll导出函数、数据或对象以及从dll中导入函数、数据或对象。相当于定义了dll的接口,为它的客户exe或dll定义可使用的函数、数据或对象。如果想从dll文件中引出void func()函数,表达式为:
__declspec(dllexport) void func();
如果一个函数被__declspec(noreturn)修饰,则告诉编译器,这个函数不会返回,其结果是让编译器知道被修饰为__delspec(noreturn) 的函数之后的代码不可能达到。编译器发现一个函数有无返回值的分支,则会发出C4715警告后C2202错误信息。用该__delspec(noreturn)修饰则可以避免警告或错误。
扩展属性 nothrow告诉编译器被声明的函数以及函数内部调用的其他函数都不会抛出异常。
扩展属性novtable表示该类不会被实例化
扩展属性thread格式:__declspec(thread) declator
其中declator为线程局部变量并具有线程存储时限,以便编译器在创建线程时自动分配的存储。
如: Win32Dll.dll文件中定义函数void SayHello()
#include "stdafx.h"
#include <iostream>
extern "C" __declspec(dllexport)void SayHello()
{
MessageBox(NULL, L"Hello",L"fangyukuan", MB_OK);
::MessageBoxW(NULL, L"Hello", L"fangyukuan",MB_OK);
printf("what thefuck!!\n");
}
在另一个程序中调用 Win32Dll.dl中的SayHello()函数
typedef void (SAYHELLO)(); // 定义一个这种类型的函数指针
int _tmain(int argc, _TCHAR*argv[])
{
HINSTANCE hDllInst;
// 这里为dll的路径,因为现在exe和dll在同一个目录下,所以只写名字即可
hDllInst=LoadLibrary(L"Win32Dll.dll");
if(NULL ==hDllInst)
{
cout << "加载dll失败" << endl;
}
else
{
//获得DLL文件中的 SayHello函数的指针
SAYHELLO * lpproc =(SAYHELLO*)GetProcAddress(hDllInst,"SayHello");
if(NULL!= lpproc)
(*lpproc)();
FreeLibrary(hDllInst);
}
system("pause");
return 0;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////编写DLL时的函数与一般的函数方法基本一样。但要对库中的函数进行必要的声明,以说明哪些函数是可以导出的,哪些函数是不可以导出的。
把DLL中的函数声明为导出函数的方法有两种:
一是使用关键字_declspec(dllexport)来声明。
二是在.def文件中声明。
一、使用关键字_declspec(dllexport)来声明导出函数
声明函数SayHello为导出函数语句为:int _declspec(dllexport) SayHello ();
为了使一个用C++语言编写的DLL函数可以在C语言编写的应用程序中使用,在关键字_declspec(dllexport) 之前要附加另一个关键字:extern “C”,以通知编译器采用C链接方式。
例子:(用vs2008写的例子)
新建一个工程。
选择Win32 Project,工程名字为1_DLLDemo。确定。
选择DLL其它默认。
在文件1_DLLDemo.cpp编写代码如下:
extern "C" __declspec(dllexport) void SayHello()
{
::MessageBoxW(NULL, L"Hello", L"fangyukuan", MB_OK);
}
按F7编译即可。
再新建一个测试工程。这里简单一点,新建一个控制台工程就可以了。
选择Win32 consoleApplication,其它全部默认。
编写如下代码:
#include "stdafx.h"
#include "stdlib.h" // for system("pause");
#include "windows.h"
#include <iostream>
using namespace std;
typedef void (SAYHELLO)(); // 定义一个这种类型的函数指针
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hDllInst;
// 这里为dll的路径,因为现在exe和dll在同一个目录下,所以只写名字即可
hDllInst=LoadLibrary(L"1_DLLDemo.dll");
if(NULL == hDllInst)
{
cout << "加载dll失败" << endl;
}
else
{
SAYHELLO * lpproc =(SAYHELLO *)GetProcAddress(hDllInst,"SayHello");
if(NULL != lpproc)
(*lpproc)();
FreeLibrary(hDllInst);
}
system("pause");
return 0;
}
按F7编译。再按F5运行。结果如下。
二、使用def文件声明导出函数
def文件又叫做模块定义文件,这是一个用于描述DLL属性的文本文件,每个def文件一般要包括以下模块定义语句:
A) LIBRARY语句,指出DLL的名字,链接器将把这个名字放到DLL库中。
B) EXPORTS语句,列出库中导出函数的名称及导出函数的序号(可选)。
C) DISCRIPION语句,该语句用来描述DLL的用途等说明。
可以在def文件分号“;”后面书写注释语句。
在创建DLL时,编译链接器将要使用def文件创建两个文件:一个导出文件(.EXP)和一个导入库文件(.LIB),然后使用导出文件再创建DLL文件。
外部应用程序使用的文件是导入库文件和DLL文件。由于在导入库文件中存放了外部应用程序可导入的DLL导出函数名称列表,因此外部应用程序需要把它连接在应用程序中,才能以它为索引到DLL中去找到要调用的导出函数。也就是说,导入库文件相当于是DLL可提供的服务项目表。
例子:(用vs2008写的例子)
方法同上面一样新建一个DLL工程。
在文件2_DLLDemo.cpp编写代码如下:
void SayHello()
{
::MessageBoxW(NULL, L"2_DLLDemo::Hello", L"fangyukuan", MB_OK);
}
然后再新建一个def文件。
在工程右键->add->NewItem…
在def文件编写如下代码:
LIBRARY "2_DLLDemo"
EXPORTS
SayHello
按F7编译完成。
测试方法同上面例子一样。
不同的参数调用方式:
DLL头文件中:
#ifndef LINK81_H
#define LINK81_H
extern "C"
{
int __stdcallAdd(int a, intb);
int __stdcallSubstract(int a, intb);
void __stdcallsetData(int data);
int __stdcallgetData();
};
#endif
DLL 实现文件中:
#include<Windows.h>
#pragma data_seg("Shared")
int dwID = 6;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.Shared,RWS")
BOOL WINAPIDllMain(HANDLE hMoudle, DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
extern "C" int __stdcall Add(int a, int b)
{
return a + b;
}
extern "C" int __stdcallSubstract(int a, intb)
{
return a - b;
}
extern "C"void __stdcallsetData(int data)
{
dwID = data;
}
extern "C" int __stdcallgetData()
{
return dwID;
}
.def文件中:
LIBRARY LINK81 或者 LIBRARY “windll”
EXPORTS
Add @ 1
Substract @ 2
setData @ 3
getData @ 4
或者不适用def文件而使用:
.h文件中:
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b);
cpp文件中:
extern "C" __declspec(dllexport)int __stdcallAdd(int a, intb)
{
return a + b;
}
在调用文件中使用
Add * lpproc =(Add*)GetProcAddress(hDllInst,(LPCSTR)MAKEINTRESOURCE(1));
//Add函数在DLL文件中的序号为1
而不能使用
Add * lpproc =(Add*)GetProcAddress(hDllInst,"Add");
应该用:
Add * lpproc =(Add *)GetProcAddress(hDllInst,"_Add@8");
GetProcAddress中的第二个参数函数名应该使用编译后的函数名
typedef int (__stdcall Add)(int, int); // 定义一个这种类型的函数指针
………
……….
Add * lpproc =(Add*)GetProcAddress(hDllInst,"Add");
if(NULL!= lpproc){
intresult = (*lpproc)(3, 2);
printf("the answer is : %d\n", result);
}
DLL 和LIB
dll和.lib都是程序集合,便于代码重用。都是二进制的文件。
.dll也叫动态链接库,与程序链接的方式为运行时链接(run-timelinked),为PE(portable executable)格式,也就是程完整的程序。.exe、.dll、.fon、.mod、.drv、.ocx等等都是动态链接库。如.exe为系统调用的函数集合。.dll不存在同名引用,且有导出表,与导入表。
1、与DLL无关的lib库【纯粹的静态链接库】
.lib也叫静态链接库,在编译时与程序链接(link-timelinked),将“嵌入”到程序中。会有冗余(程序文件代码的冗余和运行时内存存储的冗余),当两个lib相链接时地址会重新建立同。在使用.lib之前,要在程序源代码中引用lib对应的头文件.h,这些头文件告诉编译器.lib中有什么。
别的工程要使用静态链接库lib有两种方式:
1 在工程选项-〉link-〉Object/Library
Module中加入hello.lib
2 可以在源代码中加入一行指令
#pragma comment(lib, "xxxx.lib")
2、动态链接库DLL
在生成.dll时,通常会生成一个.lib。这个.lib将被编译到程序文件中,在程序运行的时候,告诉操作系统将要加载的.dll。这个.lib包括对应.dll的文件名、顺序表(ordinal table包含.dll暴露出的函数的进入点),在程序运行的时候,通过顺序表实现函数的跳转。
2.1 DLL中的LIB
在另一个程序中调用DLL时,加载lib来只是程序如何调用DLL中提供的函数
#include “dll的名称.h”
#pragma comment(lib, “dll的名称”) //使用DLL带的lib
2.2 直接在程序中动态加载DLL
如果不想使用或者找不到该.lib,可以用LoadLibrary () Win32 API和GetLibrary ()
根据调用方式的不同,对动态库的调用可分为静态调用方式和动态调用方式。
(1)静态调用,也称为隐式调用,由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(Windows系统负责对DLL调用次数的计数),调用方式简单,能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须在源文件中声明一下。 LIB文件包含了每一个DLL导出函数的符号名和可选择的标识号以及DLL文件名,不含有实际的代码。Lib文件包含的信息进入到生成的应用程序中,被调用的DLL文件会在应用程序加载时同时加载在到内存中。
(2)动态调用,即显式调用方式,是由编程者用API函数加载和卸载DLL来达到调用DLL的目的,比较复杂,但能更加有效地使用内存,是编制大型应用程序时的重要方式。在Windows系统中,与动态库调用有关的函数包括:
①LoadLibrary(或MFC 的AfxLoadLibrary),装载动态库。
②GetProcAddress,获取要引入的函数,把符号名或标识号转换为DLL内部地址。
① FreeLibrary(或MFC的AfxFreeLibrary),释放动态链接库。
静态链接库LIB 的调用
例如:
创建了一个静态库myfun.lib,其中有个函数int myfun(int a, int b); 静态库工程的头文件为myfun.h: #ifndef _MYFUN_H_ #define _MYFUN_H_ #pragma comment(lib, "myfun.lib") /*强制调用链接库*/ int myfun(int, int); /*声明函数原型*/ #endif 在调用静态库的程序中引用: #include "myfun.h"