动态链接库DLL

13 篇文章 0 订阅

【概述】

动态链接库通常不能直接运行,也不能接受消息。他们是一些独立的文件,其中包括可被执行程序或者其他DLL调用来完成某项工作的函数。

WINAPI提供的3个重要DLL:

Kernel32.dll:包含管理内存,进程和线程的函数

User32.dll:包含那些用于执行用户界面的任务的函数,如窗口的创建和消息的发送

GDI32.dll:包含那些用于画图和显示文本的函数


【静态库与动态库的区别】

静态库:函数和数据被编译进一个二进制文件(*.LIB),使用静态库的情况下,在编译可执行文件时,链接器会从库中复制这些函数和数据并把他们和应用程序的其他文件组合起来创建最终的可执行文件(*.EXE)。

动态库:使用动态库要提供两个文件,引入库文件(*.lib)和DLL文件(*.dll)。引入库文件包含dll导出的函数和变量的符号名,dll包含实际的函数和数据,就如同引入库只是论文的摘要,而dll是论文真正内容。使用动态库的时候,编译链接可执行文件时,只需要链接该dll的引入库文件,并不复制函数和数据到可执行文件,程序运行时才会加载dll,然后映射到进程的地址空间,然后访问dll中的函数。

区别:

1)发布产品:静态库不需要提供额外的dll,只需要发布程序;而动态库要发布程序和依赖的动态库DLL

2)不足就是:静态库通常包所有函数和数据都链接到可执行文件,所以体积较大,不易于后期维护;动态库:由于在加载的时候才调用dll,所以可以单独发布,体积较小,但是加载dll也需要一定时间的消耗


【动态库的优点】

1)可以采用多种编程语言编写

2)增强产品的功能

3)提供二次开发的平台

4)简化项目管理

5)可以节省磁盘空间

6)有助于资源的共享

7)有助于实现程序的本地化


【动态库的加载方式】

隐式链接和显式链接


【DLL的创建和使用】

首先在vs2012新建一个Win32的DLL空项目,工程取名为Win32Dll1,如下图所示


如下实例:

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

int sub(int a, int b)
{
	return a - b;
}
一般程序想要访问dll中的add和sub函数,那么还函数必须有被导出的函数,vs2012安装后有一个命令工具“VS2012 开发人员命令提示”我们可以直接打开它,然后使用dumpbin命令来查看导出的函数,如下


可以看出没有导出任何函数,那么就必须将函数导出,就必须在函数的前面加上标识符,让别人认识你是dll了:_declspec(dllexport),如下,

这时看到目录多生出了两个文件.lib 和.exp:

.lib:引入库文件,导向作用,保存的是dll导出的函数和变量的符号名

.exp:输出库文件,该文件并不重要

再次使用dumpbin命令查看一下当前的导出符号

导出的符号有一些输出信息:

ordinal列是导出函数的序号

hint列列出的数字是提示码,该信息不重要

RVA列列出的的地址值是导出函数在dll模块中的位置

name列是导出函数的名字 名字很奇怪,是由于c++支持重载,函数名相同很正常,为了区分编译器会按照自己的习惯更改函数的名字


【隐式链接加载DLL】

如何测试DLL??

在一个mfc程序中设置了两个按钮测试这个dll,第一种引用方式

//1)第一种引用方式
extern int add(int a, int b);
extern int sub(int a, int b);
编译时会弹出以下问题,如何解决呢??
1>------ 已启动生成: 项目: MFCHello, 配置: Debug Win32 ------
1>MFCHelloDlg.obj : error LNK2019: 无法解析的外部符号 "int __cdecl add(int,int)" (?add@@YAHHH@Z),该符号在函数 "public: void __thiscall CMFCHelloDlg::OnBnClickedBtAdd(void)" (?OnBnClickedBtAdd@CMFCHelloDlg@@QAEXXZ) 中被引用
1>MFCHelloDlg.obj : error LNK2019: 无法解析的外部符号 "int __cdecl sub(int,int)" (?sub@@YAHHH@Z),该符号在函数 "public: void __thiscall CMFCHelloDlg::OnBnClickedBtSub(void)" (?OnBnClickedBtSub@CMFCHelloDlg@@QAEXXZ) 中被引用
1>E:\Project\mfc_proj\MFCHello\Debug\MFCHello.exe : fatal error LNK1120: 2 个无法解析的外部命令
========== 生成: 成功 0 个,失败 1 个,最新 0 个,跳过 0 个 ==========
有以下几种方式加载,首先你必须指定库的目录,除非你提前将库放到你当前工程的目录下,如下

VS2012下项目-》xx属性-》配置属性-》链接器-》常规-》附加库目录,如下我的设置


注意:这里目录一般在工程开发的时候设置为相对目录,这里为了测试不在修改

另外一个需要设置的地方就是引用哪个库,上图中的常规下的输入dll的库名字


这样设置完以后,就可以了,但是有没有发现这种方式特别麻烦有没有更好的方式(测试前最后先抹掉上面的)

先不抹掉目录,在代码中加入以下一句

#pragma comment(lib, "Win32Dll1.lib")
编译就行了,是不是很方便,连目录都抹掉的话,你就乖乖的写完整路径或者相对路径了
#pragma comment(lib, "E:\\Project\\mfc_proj\\Win32Dll1\\Debug\\Win32Dll1.lib")

编译以下也是ok的,更多#pragma的介绍请查看#pramga的介绍;扯远了,能不能运行呢??

我们需要知道当前测试程序引入的库,由于命令符有时直接覆盖了前面的信息,我将其导入到文本中查看,如下:

E:\Project\mfc_proj\MFCHello\Debug>dumpbin /imports MFCHello.exe >dll.txt

    Win32Dll1.dll
                42BB90 Import Address Table
                42B3E8 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                    0 ?add@@YAHHH@Z
                    1 ?sub@@YAHHH@Z
导出来的有这些信息,需要这个dll,同时看到还需要其他的dll:mfc110d.dll MSVCR110D.dll KERNEL32.dll USER32.dll COMCTL32.dll OLEAUT32.dll ADVAPI32.dll
直接运行会弹出提示:

---------------------------
MFCHello.exe - 系统错误
---------------------------
无法启动此程序,因为计算机中丢失 Win32Dll1.dll。尝试重新安装该程序以解决此问题。 
---------------------------
确定   
---------------------------
由于DLL还未放入当前目录,当然也可以在工程属性设置或者放入搜索的dll目录下,放入后就可以了,测试代码如下:

void CMFCHelloDlg::OnBnClickedBtAdd()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str;
	int a = 100, b = 101;
	str.Format(_T("%d + %d = %d\n"), a, b, add(a, b));
	AfxMessageBox(str);
}

void CMFCHelloDlg::OnBnClickedBtSub()
{
	// TODO: 在此添加控件通知处理程序代码
	CString str;
	int a = 123, b = 101;
	str.Format(_T("%d - %d = %d\n"), a, b, sub(a, b));
	AfxMessageBox(str);
}
运行后的效果:

可以看到程序已测试ok,上面在查看程序依赖的的dll文件还可以使用下面的命令,如下

dumpbin /dependents MFCHello.exe

在声明函数的时候,我们使用的是extern ,还可以使用专门的标识符 _declspe(dllimport)来表明函数是从动态库引入的,将刚才的测试程序修改为一下:

//2)第二种引用方式,与extern等价,但引用动态库时效率更高
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int sub(int a, int b);
编译后,运行后效果是一样的,而且据说效率还高些 我没测试过呵呵,但起码看着舒服知道是引入的函数

【完善程序】

这样的程序只够给自己使用,如何交给客户使用,客户如何知道使用的是什么函数,怎么使用呢??

给dll1添加头文件,如下

_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int sub(int a, int b);
在测试程序的头部使用包含文件

#include "../../Win32Dll1/Win32Dll1/Win32Dll1.h"

这样刚才测试的那两句就不需要了,运行一样ok

测试了上面的程序还是过于麻烦,客户其实就像用你的头文件,程序员也希望自己可以使用,再次改造一下,如下头文件代码

#ifndef DLL1_API
#define DLL1_API  extern "C" _declspec(dllimport)  //如果未声明,那么就是引入函数声明了
#endif

DLL1_API int add(int a, int b);
DLL1_API int sub(int a, int b);

cpp文件如下

//cpp文件也可以使用头文件了,变成了导出dll函数的声明了
#define DLL1_API extern "C" _declspec(dllexport)

#include "Win32Dll1.h"
#include "windows.h"
#include "stdio.h"

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

DLL1_API int sub(int a, int b)
{
	return a - b;
}
将新的dll再次放入,当然还要再次编译一次,运行后也是ok,cpp文件也可以不加DLL1_API编译也是正常的

【解决名字改变问题】

其实上面的cpp和h文件应该是去掉"C"的,加上这个函数名导出来就是自己了,如下

    Win32Dll1.dll
                42BB90 Import Address Table
                42B3E8 Import Name Table
                     0 time date stamp
                     0 Index of first forwarder reference

                    0 add
                    1 sub
利用限定符extern "C"可以解决C++和C之间相互调用时函数命名的问题,但是这有一个缺陷就是不能用于导出一个类的成员函数,只能用于导出全局函数

但是如果改变调用约定,函数的名字仍会发生改变,如下代码:

头文件

#ifndef DLL1_API
#define DLL1_API  extern "C" _declspec(dllimport)  //如果未声明,那么就是引入函数声明了
#endif

DLL1_API int _stdcall add(int a, int b);
DLL1_API int _stdcall sub(int a, int b);
cpp文件
DLL1_API int _stdcall add(int a, int b)
{
	return a + b;
}

DLL1_API int _stdcall sub(int a, int b)
{
	return a - b;
}
编译后,dumpbin此dll的信息,看到名字仍然改变了

Dump of file Win32Dll1.dll

File Type: DLL

  Section contains the following exports for Win32Dll1.dll

    00000000 characteristics
    548BD74D time date stamp Sat Dec 13 14:06:05 2014
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 000110F5 _add@8 = @ILT+240(_add@8)
          2    1 0001117C _sub@8 = @ILT+375(_sub@8)

如果没有添加_stdcall关键字,默认就是c调用约定,标准调用就是WINAPI调用也就是pascal调用,这种调用方式与C调用约定不一致,@8中8的表明add函数的参数所占字节数,好了终结一下就是:C调用约定    !=  WINAPI(pascal/_stdcall)约定。下一篇文章也讲到了这个问题

那这个怎么解决呢,可以使用def文件解决名字冲突的问题,如下重新建了一个工程便于测试

cpp文件

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

int _stdcall sub(int a, int b)
{
	return a - b;
}
def文件
LIBRARY Win32Dll2

EXPORTS
add
sub
exports此程序,如下
    00000000 characteristics
    548BDA27 time date stamp Sat Dec 13 14:18:15 2014
        0.00 version
           1 ordinal base
           2 number of functions
           2 number of names

    ordinal hint RVA      name

          1    0 000110F5 add = @ILT+240(?add@@YGHHH@Z)
          2    1 00011140 sub = @ILT+315(?sub@@YGHHH@Z)
按照刚才测试dll1的方式,把dll2也放入刚才的测试程序,测试是OK的

【显式方式加载DLL】

显示加载时不需要指定lib和头文件信息,需要使用LoadLibary和GetProcAddress函数来获取相应的函数,如下

	// TODO: 在此添加控件通知处理程序代码
	HMODULE hModule = LoadLibrary(_T("Win32Dll2.dll"));
	if (NULL == hModule)
	{
		MessageBox(_T("Load Library Failed!"));
		return;
	}
	typedef int ( *pfnAddProc)(int a, int b);
	pfnAddProc add = (pfnAddProc)GetProcAddress(hModule, "add");
	if(NULL == add)
	{
		MessageBox(_T("Get function address failed!"));
		return;
	}

	CString str;
	int a = 100, b = 101;
	str.Format(_T("%d + %d = %d\n"), a, b, add(a, b));
	AfxMessageBox(str);
	FreeLibrary(hModule); //如果不释放,会一直存在,直到程序结束,任务管理器可以看到使用内存
直接把刚才编译的dll2放进去,会发现会崩溃,是由于采用的调用方式不一样,将函数指针的类型修改一下就行了,如下

typedef int (_stdcall *pfnAddProc)(int a, int b); //修改了调用约定
再编译运行就ok;当然移除dll再测试,就会发现加载dll失败了

除了上面使用函数名字的方式 还可以使用dll中的序号来加载,如下

pfnAddProc add = (pfnAddProc)GetProcAddress(hModule, MAKEINTRESOURCE(1));
后面一个将int转换为LPCSTR函数,会把指定的函数序号转换为相应的函数名字字符串,一般不建议使用这种方式,容易出错

随便测试一下dll的使用情况

加载DLL前的内存使用


加载了DLL后的使用情况


内存从3088k->3100k(包括弹出框的内存占用),GDI对象从41->42增加了1,是由于当前有一个窗口对象弹出,关闭后这个值会恢复到41,用户对象从22->26增加了4




  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值