注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

jasonyang9的博客

随便写写

 
 
 

日志

 
 

(怀旧系列)VC程序设计(孙鑫老师)听课笔记:19 动态链接库  

2013-03-06 10:39:35|  分类: programming |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
(怀旧系列)VC程序设计(孙鑫老师)听课笔记:19 动态链接库 - jasonyang9 - jasonyang9的博客
 ==========
动态链接库
==========

动态链接库
----------
* 自从微软推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直是Windows操作系统的基础。
* 动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某项工作的函数。只有在其他模块调用动态链接库中的函数时,它才发挥作用。
* Windows API中的所有函数都包含在DLL中。其中有3个最重要的DLL:Kernel32.dll,包含用于管理内存、进程和线程的各个函数(如CreateThread);User32.dll,包含用户执行用户界面任务(如窗口的创建和消息的传送)的各个函数(如CreateWindow);GDI32.dll,包含用于画图和显示文本的各个函数(如LineTo)。

静态库和动态库
--------------
* 静态库:函数和数据被编译进一个二进制文件(.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来,来创建最终的可执行文件(.EXE文件)。
* 动态库:在使用动态库的时候,往往提供两个文件:一个引入库(.LIB)和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。

使用动态链接库的好处
--------------------
* 可采用多种编程语言来编写(可用Delphi、VB等其他语言来编写)
* 增强产品的功能(可替代的动态链接库,实现功能增强)
* 提供二次开发的平台
* 简化项目管理(协同开发,并行开发)
* 节省磁盘空间和内存(DLL被共享)
* 有助于资源的共享(纯资源的DLL)
* 有助于实现应用程序的本地化(支持多语言)

动态链接库加载的两种方式
------------------------
* 隐式链接
* 显式加载

隐式链接
--------

新建工程Win32 Dynamic-Link Library,Dll1。

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

int subtract(int a, int b)
{
return a - b;
}

编译后生成Dll1.dll,但这个动态链接库文件中的函数还不能被调用,因为这些函数没有被“导出”。

用dumpbin工具(在Visual Studio安装目录下)查看DLL文件中“导出”了哪些函数。

转到DLL所在目录。
dumpbin -exports Dll1.dll

Dump of file Dll1.dll

File Type: DLL

Summary

7000 .data
1000 .idata
2000 .rdata
2000 .reloc
2A000 .text

输出中没有导出的函数信息。必须修改源代码才能导出函数。

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

_declspec(dllexport) int substract(int a, int b)
{
return a - b;
}

编译后会产生Dll1.lib(引用库文件,导出的函数、变量的符号名)和Dll1.exp(输出库文件)。

再次用dumpbin工具查看DLL信息。

dumpbin -exports Dll1.dll

Dump of file Dll1.dll

File Type: DLL

Section contains the following exports for Dll1.dll

0 characteristics
4143D015 time data stamp Sun Sep 12 12:27:01 2004
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name <--- ordinal(导出函数的序号)hint(提示码)RVA(地址值)name(导出的函数名)
1 0 0000100A ?add@@YAHHH@2 <--- C++会按自己的命名规则修改导出的函数名(名字改编、名字粉碎),以免发生重名
2 1 00001005 ?substract@@YAHHH@2

Summary

7000 .data
1000 .idata
3000 .rdata
2000 .reloc
2A000 .text

编写一个测试DLL的程序。
新建MFC AppWizard,DllTest,Dialog based。
增加2个按钮:“Add”(IDC_BTN_ADD),“Subtract”(IDC_BTN_SUBTRACT),增加命令响应。

extern int add(int a, int b); // 声明这2个函数是外部定义的
extern int subtract(int a, int b);

void CDllTestDlg::OnBtnAdd()
{
CString str;
str.Format("5 + 3 = %d", add(5, 3)); // 调用add做加法
MessageBox(str);
}

void CDllTestDlg::OnBtnSubtract()
{
CString str;
str.Format("5 - 3 = %d", subtract(5, 3)); // 调用subtract做减法
MessageBox(str);
}

编译通过(因为函数有声明),但链接时出错(因为找不到函数的具体实现):
error LNK2001: unresolved external symbol "int __cdecl add(int, int)" (?add@@YAHHH@2)
error LNK2001: unresolved external symbol "int __cdecl subtract(int, int)" (?substract@@YAHHH@2)

将Dll1工程下Debug目录中的Dll1.lib文件复制到DllTest工程目录下,DllTest工程设置,Link标签页中的Object\library modules中加入Dll1.lib。然后再编译连接,就能通过了。
但要注意,虽然能编译通过,但.lib文件只包含链接时需要的信息,提供.exe文件建立动态链接时用到的重定位表,并没有函数实现的代码。

可以查看.exe程序的输入信息,转到DllTest的Debug目录下,执行:

dumpbin -imports dlltest.exe

Dump of file dlltest.exe

File Type: EXECUTABLE IMAGE

Section contains the following imports:

Dll1.dll
417414 Import Address Table
41708C Import Name Table
0 time date stamp
0 Index of first forwarder reference

1 ?subtract@@YAHHH@2 <--- 导入的函数名
0 ?add@@YAHHH@2 <--- 导入的函数名

MFC42D.DLL <--- Debug模式下需要用到MFC42D.DLL(Release模式下用的是MFC42.DLL)

...

MSVCRTD.dll <--- C运行库

...

KERNEL32.dll <--- KERNEL32.dll

...

USER32.dll <--- USER32.dll

...

MFC042D.DLL <--- MFC042D.DLL

...

Summary

...

尝试运行程序,出现“无法找到DLL”错误,因为动态链接库访问时,在运行后加载程序会根据可执行模块的输入信息(按一定的路径顺序)搜索对应的动态链接库,如果失败则弹出错误。
将Dll1.DLL文件复制到搜索目录中即可运行程序。
除了dumpbin工具外,还可以用图形界面的Dependency Walker来查看程序对DLL的依赖。

还可以用_declspec(dllimport)标识符来替代extern声明函数是从DLL导入的,生成的程序运行效率更高。

_declspec(dllimport) int add(int a, int b); // 标识函数是从DLL导入的
_declspec(dllimport) int subtract(int a, int b);

void CDllTestDlg::OnBtnAdd()
{
CString str;
str.Format("5 + 3 = %d", add(5, 3)); // 调用add做加法
MessageBox(str);
}

void CDllTestDlg::OnBtnSubtract()
{
CString str;
str.Format("5 - 3 = %d", subtract(5, 3)); // 调用subtract做减法
MessageBox(str);
}

一般来讲,DLL提供者应该附带头文件,提供导出的函数原型声明等。
在Dll1工程中增加头文件Dll1.h。

// Dll1.h
_declspec(dllimport) int add(int a, int b); // 标识函数是从DLL导入的
_declspec(dllimport) int subtract(int a, int b); // 注意:头文件是给调用DLL中函数的程序使用的,所以这里写的是dllimport

在测试程序中包含这个头文件。

// DllTestApp.cpp
#include "..\Dll1\Dll1.h"

编译链接正常。

但此头文件只能被调用DLL的程序使用,动态链接库工程自身无法使用。

// Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif

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

// Dll1.cpp
#define DLL1_API _declspec(dllexport) // 在Dll1.cpp中定义DLL1_API为_declspec(dllexport)
#include "Dll1.h" // 展开Dll1.h,发现DLL1_API已被定义,保持原样,即函数被声明为_declspec(dllexport) int add(int a, int b);和_declspec(dllexport) int subtract(int a, int b);,即为“导出”所用

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

int substract(int a, int b)
{
return a - b;
}

当在DllTest工程中包含Dll1.h时,由于DLL1_API没有被定义过,因此会被定义为_declspec(dllimport),即函数被声明为_declspec(dllimport) int add(int a, int b);和_declspec(dllimport) int subtract(int a, int b);,即为“导入”所用。
那么这个头文件既可以被动态链接库本身使用,也可以被调用动态链接库的程序使用。

动态链接库除了可以导出函数,还可以导出C++的类。

// Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif

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

class DLL1_API Point // 注意在class关键字后、类名前加入_declspec标识符
{
public: // 导出类后其访问权限不会改变,private权限的成员变量或函数仍然不可从外部访问
void output(int x, int y);

};


// Dll1.cpp
#define DLL1_API _declspec(dllexport) // 在Dll1.cpp中定义DLL1_API为_declspec(dllexport)
#include "Dll1.h" // 展开Dll1.h,发现DLL1_API已被定义,保持原样,即函数被声明为_declspec(dllexport) int add(int a, int b);和_declspec(dllexport) int subtract(int a, int b);,即为“导出”所用
#include <windows.h> // 用到了API函数
#include <stdio.h> // 用到了C标准输入输出函数

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

int substract(int a, int b)
{
return a - b;
}

void Point::output(int x, int y) // 类成员函数的实现(不要需再加_declspec标识符)
{
HWND hwnd = GetForegroundWindow(); // 用GetForegroundWindow获得当前窗口的句柄
HDC hdc = GetDC(hwnd); // 获得窗口DC

char buf[20]; // 字符数组
memset(buf, 0, 20); // 置零
sprintf(buf, "x = %d, y = %d", x, y); // 格式化输出
TextOut(hdc, 0, 0, buf, strlen(buf)); // 在0,0位置输出字符串
ReleaseDC(hwnd, hdc);
}

编译后,将Dll1.lib和Dll1.dll拷贝到DllTest工程目录下(Dll1.h不需要拷贝,因为包含时就指定了路径)。
在DllTest工程中增加按钮:“Output”(IDC_BTN_OUTPUT),增加命令响应。

void CDllTestDlg::OnBtnOutput()
{
Point pt;
pt.output(5, 3);
}

编译后运行正确。

也可以只导出类中的某个函数(必须是public访问权限)。

// Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif

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

class Point
{
public:
DLL1_API void output(int x, int y); // 导出output函数
void test(); // test不导出
};

// Dll1.cpp
#define DLL1_API _declspec(dllexport) // 在Dll1.cpp中定义DLL1_API为_declspec(dllexport)
#include "Dll1.h" // 展开Dll1.h,发现DLL1_API已被定义,保持原样,即函数被声明为_declspec(dllexport) int add(int a, int b);和_declspec(dllexport) int subtract(int a, int b);,即为“导出”所用
#include <windows.h> // 用到了API函数
#include <stdio.h> // 用到了C标准输入输出函数

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

int substract(int a, int b)
{
return a - b;
}

void Point::output(int x, int y)
{
HWND hwnd = GetForegroundWindow();
HDC hdc = GetDC(hwnd);

char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x = %d, y = %d", x, y);
TextOut(hdc, 0, 0, buf, strlen(buf));
ReleaseDC(hwnd, hdc);
}

void Point::test() // test的实现
{

}

虽然类没有被导出,但还是可以在DllTest中定义一个类的对象,然后再调用output成员函数。

为了让C编译器也能使用DLL库,必须加上extern "C"标识符,这样导出的函数名称就会保持不变。

// Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport) // 这里要加上extern "C"
#endif

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

// Dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport) // 这里要加上extern "C"
#include "Dll1.h"
#include <windows.h>
#include <stdio.h>

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

int substract(int a, int b)
{
return a - b;
}

用dumpbin工具查看DLL信息。

dumpbin -exports Dll1.dll

Dump of file Dll1.dll

File Type: DLL

Section contains the following exports for Dll1.dll

0 characteristics
4143D015 time data stamp Sun Sep 12 12:27:01 2004
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name <--- ordinal(导出函数的序号)hint(提示码)RVA(地址值)name(导出的函数名)
1 0 0000100A add <--- 函数名称保持不变
2 1 00001005 subtract <--- 函数名称保持不变

Summary

7000 .data
1000 .idata
3000 .rdata
2000 .reloc
2A000 .text

用extern "C"只能导出全局函数,无法导出类的成员函数。
如果在函数前增加_stdcall(标准调用约定)则即使加了extern "C",导出的函数名称还是会有所改变。

// Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport) // 这里要加上extern "C"
#endif

DLL1_API int _stdcall add(int a, int b);
DLL1_API int _stdcall subtract(int a, int b);

// Dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport) // 这里要加上extern "C"
#include "Dll1.h"
#include <windows.h>
#include <stdio.h>

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

int _stdcall substract(int a, int b)
{
return a - b;
}

用dumpbin工具查看DLL信息。

dumpbin -exports Dll1.dll

Dump of file Dll1.dll

File Type: DLL

Section contains the following exports for Dll1.dll

0 characteristics
4143D015 time data stamp Sun Sep 12 12:27:01 2004
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name <--- ordinal(导出函数的序号)hint(提示码)RVA(地址值)name(导出的函数名)
1 0 0000100A _add@8 <--- 函数名称变了,增加了前导_和后缀@8(8表示其参数占8个字节)
2 1 00001005 _subtract@8

Summary

7000 .data
1000 .idata
3000 .rdata
2000 .reloc
2A000 .text

新建一个Win32 Dynamic-Link Library,Dll2。

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

int subtract(int a, int b)
{
return a - b;
}

到工程目录下,新建文本文件Dll2.def(模块定义文件),加入到工程中,编辑:

LIBRARY Dll2

EXPORTS
add
subtract

EXPORTS详细用法请见MSDN。这样可以让编译器保持函数名称不变。

以上是隐式链接的方法。

显式加载
--------

HMODULE LoadLibrary(
LPCTSTR lpFileName // file name of module
);
映射指定的可执行模块到调用进程的地址空间。
lpFileName [in],指向空终止的字符串,表示可执行模块(DLL、EXE文件)名字。

FARPROC GetProcAddress(
HMODULE hModule, // handle to DLL module
LPCSTR lpProcName // function name
);
获取指定的导出动态链接库函数的地址。
hModule [in],动态链接库模块句柄
lpProcName [in],函数名称(如果是一个序号,则低位字为序号,高位字为0)

修改DllTest工程,去除工程设置,Link标签页中的Object\library modules中的Dll1.lib(动态加载不需要.lib库文件)。

void CDllTestDlg::OnBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary("Dll2.dll"); // 加载DLL文件
typedef int (*ADDPROC)(int a, int b); // 定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add"); // 将GetProcAddress返回的指针赋值给Add
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5 + 3 = %d", Add(5, 3));
MessageBox(str);
}

注意:DLL中的调用约定和使用DLL程序中的必须一致。
对于动态加载调用DLL的程序,用dumpbin看不到其导入的函数。

新建一个Win32 Dynamic-Link Library,Dll3。

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

编译后将Dll3.dll拷贝到DllTest工程目录下,修改OnBtnAdd,编译后运行出现“获取函数地址失败!”,原因是Dll3中导出的函数名被C++编译器改编。

void CDllTestDlg::OnBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary("Dll3.dll"); // 加载DLL文件
typedef int (*ADDPROC)(int a, int b); // 定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add"); // 将GetProcAddress返回的指针赋值给Add
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5 + 3 = %d", Add(5, 3));
MessageBox(str);
}

用dumpbin工具查看DLL信息。

dumpbin -exports Dll3.dll

Dump of file Dll3.dll

File Type: DLL

Section contains the following exports for Dll3.dll

0 characteristics
4143D015 time data stamp Sun Sep 12 12:27:01 2004
0.00 version
1 ordinal base
2 number of functions
2 number of names

ordinal hint RVA name <--- ordinal(导出函数的序号)hint(提示码)RVA(地址值)name(导出的函数名)
1 0 0000100A ?add@@YAHHH@2

Summary

7000 .data
1000 .idata
3000 .rdata
2000 .reloc
2A000 .text

将改编过的函数名称“?add@@YAHHH@2”复制下来,替换DllTest中的add。

void CDllTestDlg::OnBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary("Dll3.dll"); // 加载DLL文件
typedef int (*ADDPROC)(int a, int b); // 定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "?add@@YAHHH@2"); // 将GetProcAddress返回的指针赋值给Add
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5 + 3 = %d", Add(5, 3));
MessageBox(str);
}

编译运行成功。

还可以用序号来获取函数指针。

void CDllTestDlg::OnBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary("Dll3.dll"); // 加载DLL文件
typedef int (*ADDPROC)(int a, int b); // 定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, MAKEINTRESOURCE(1)); // 将GetProcAddress返回的指针赋值给Add
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5 + 3 = %d", Add(5, 3));
MessageBox(str);
}

对于动态链接库,有一个可选的入口函数DllMain。

BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to the DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved // reserved
);
hinstDLL [in],指向DLL模块的句柄,动态链接库加载时,DLL模块的句柄会被传递进来(这个参数是系统提供的,和main中的2个参数类似)
fdwReason [in],指示动态链接库的入口函数为何被调用(这个参数是系统提供的,和main中的2个参数类似)
lpvReserved [in],保留

不要在DllMain中调用复杂的函数,因为这时有可能一些系统的DLL还未加载,那么结果就是失败,程序退出。

新建MFC AppWizard(dll)可以让VC生成一个MFC类库支持的动态链接库工程。

* Regular DLL with MFC statically linked(常规DLL,MFC静态链接,库可以直接发布)
* Regular DLL using shared MFC DLL(常规DLL,MFC动态链接,库要和MFC42.dll同时发布)
* MFC Extension DLL (using shared MFC DLL)(MFC扩展DLL,可导出MFC的类,库要和MFC42.dll同时发布)

此外,在调用完成后可以用FreeLibrary减少加载库的计数,当加载计数为0时,模块就被卸载,句柄失效。

void CDllTestDlg::OnBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary("Dll3.dll"); // 加载DLL文件
typedef int (*ADDPROC)(int a, int b); // 定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst, MAKEINTRESOURCE(1)); // 将GetProcAddress返回的指针赋值给Add
if (!Add)
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5 + 3 = %d", Add(5, 3));
MessageBox(str);
FreeLibrary(hInst); // 释放DLL

 }

  评论这张
 
阅读(623)| 评论(0)
推荐 转载

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017