动态连接库

 

动态连接库
动态连接库,简称 DLL(Dynamic-Link Library) ,它是基于 Windows 程序设计的一个非常重要的组成部分。在建立应用程序的可执行文件时,不必将 DLL 连接到程序中,而是在运行时动态装载 DLL ,装载时 DLL 被映射到进程的地址空间中。
一. DLL 概述
使用普通的函数库,在程序链接时将库中的代码拷贝到可执行文件中,这是一种静态链接,在多个同样的程序执行时,系统保留了许多重复的代码副本,造成内存资源浪费。使用 DLL 的动态链接并不是将库代码拷贝,只是在程序中记录了函数的入口点和接口,在程序执行时才将库代码装入内存。不管多少程序使用 DLL ,内存中都只有一个 DLL 的副本,当没有程序使用时,系统就将它移出内存,减少了对内存和磁盘的要求。
DLL 是一种基于 Windows 的程序模块,它不仅可以包含可执行的代码,还可以包含有数据,各种资源,扩大了库文件的使用范围。比如:在系统目录下有一个 Comdlg32.dll 文件,它包含了公共对话框的代码和资源。有些设备驱动程序也是由动态链接库实现的 ( 扩展名一般是 drv)
二. MFC 中的 DLL
MFC 类库本身是用 DLL 形式实现的共享库。在新建一个工程时,你可以选择使用静态库的还是动态的 DLL 库。其中,动态共享库就是 Visual C++6.0 安装在系统目录和其他目录下的一些 DLL ,例如安装在系统 System32 下的 MFC42.dll 文件。
工程建好以后,也可以使用 Project|Settings… 菜单项,选择对话框中的 General 标签,其中的内容可以改变使用动态共享库还是静态库的设置。但是需要注意的是,使用共享库编出的应用程序在发布时,要将开发时所使用的 VC++ 对应版本的 MFC DLL 文件一同发布,主要是考虑到这种情况:用户使用你编制的软件,但用户计算机上没有安装相应的 VC++ 或者是版本不同,他的系统中就没有这些 DLL 或和她们版本不同,那你的软件将不能在用户的计算机上运行。
三. DLL 入出口函数
DLL 程序本身并不能运行,它需要一个入出口函数,就像 main 函数一样,在应用程序使用 DLL 中的内容之前,系统先调用入出口函数完成 DLL 的初始化和终止工作。
一个 DLL 可以有一个入出口函数,系统在某些时候会调用这个 DLL 入出口函数。通常是完成针对应用程序的初始化和结束处理。如果建立的是只有资源的 DLL 或不需要这种处理的 DLL ,就不必实现此函数。 VC++ 已经定义了简单的 DllMain DLL 入出口点函数,它完成了一些初始化工作,包括对 C 运行时库调用的支持等。所以如果你的 DLL 程序中没有这样的函数,链接器会自动将这个缺省的 DllMain 链接上。
一般 DLL 的入出口函数是 DllMain 函数,在 MFC AppWizard 自动生成的两种 Regular DLL 中则是另外一种形式,下面分别介绍。
(1)DllMain 函数
DllMain 函数在系统调入或撤除这个 DLL 时调用,这些动作一般发生在应用程序使用 LoadLibrary FreeLibrary 等函数以及进程线程启动终止的时候。一般它的结构如下:
BOOL APIENTRY
DllMain(HINSTANCE hInstance,DWORD dwReason,LPVOID lpReserved)
{
 switch(deReason)
 {
 case DLL_PROCESS_ATTACH;
 break;
 case DLL_THREAD_ATTACH;
 break;
 case DLL_THREAD_DETACH;
 break;
 case DLL_PROCESS_DETACH;
 break;
 }
 return TRUE;
}
其中,参数 hInstance DLL 模块句柄, lpReserved 用于指定 DLL 初始化和清除的一些内容的指针,参数 deReason 表明了调用 DllMain 函数的原因:
DLL_PROCESS_ATTACH DLL 被链接到当前进程的地址空间中。
DLL_THREAD_ATTACH :当前进程创建一个新线程,新线程要访问 DLL
DLL_THREAD_DETACH :表示线程与 DLL 分离。
DLL_PROCESS_DETACH :表示进程和 DLL 分离。
根据列出的原因,进行相应的处理工作。
(1)MFC AppWizard 生成的 RegularDLL 的入出口
每个 Regular DLL 都有 MFC AppWizard 自动生成的一个 CWinApp 派生类的对象,与 MFC 应用程序一样,它是在 CWinApp 派生类的成员函数 InitInstance ExitInstance 中完成初始化和终止工作的。
实际上, MFC 提供了一个最基本的 DllMain 函数,你在这种 DLL 中不必自己编写 DllMain 函数,由 MFC 提供的这个函数在装载 DLL 时调用 InitInstance ,而在 DLL 退出时调用 ExitInstance 。所需要完成的初始化和终止工作就在这两个函数中完成。
四. DLL 中导出函数
知道了 DLL 的入出口函数, DLL 就要开始被用户应用程序使用了,有哪些内容可以被用户使用呢?下面将分别介绍 DLL 中的函数,数据和资源。
DLL 中定义有两种函数:导出函数和内部函数,内部函数只能在 DLL 内部使用,它的定义和使用和普通程序一样。导出函数可以被其他模块调用,其他模块就要知道该 DLL 导出了哪些函数,函数定义接口等信息。 DLL 中包含有导出表,其中有每个导出函数的名字,只有导出表中的函数可以被其他可执行程序调用。下面将介绍导出函数的三中方法。
(1) 使用 DEF 文件导出函数
模块定义文件 (DEF) 是由一个或多个用于描述 DLL 属性的语句组成的文本文件。
NAME 语句,指出生成程序或 DLL 的文件名;
LIBRARY 语句,指出 DLL 的内部名字,这个语句告诉链接器要生成的是 DLL
DESCRIPITON 语句,描述 DLL 的用途;
STACKSIZE 语句,设置堆栈大小;
SECTIONS 语句,设置段属性;
EXPORTS 语句,列出被导出函数的名字,以及其他信息;
VERSION 语句给出该 DLL 的版本号;
分号开头的语句是注释。
例子如下:
LIBRARY “MyDll”
DESCRIPTION ‘TEST DLL’
EXPORTS
   ; 在此输出函数
   Func1 OtherName=Func1
   Func1 @1
   Func2 @2
   Func3 @3 NONAME
DEF 文件中的字符串如果包含空格,分号或和关键词相同,要用双引号引起来。
EXPORTS 语句后,函数名首先出现,还可以象 Func1 OtherName=Func1 语句一样将同一个函数以不同的名字输出,也就是说,输出的名字不一定是函数的原名。在 @ 之后选择一个序号,在使用 DLL 的应用程序中也可以通过这个序号来调用 DLL 中的函数。加上 NONAME 标志,表明不输出函数名而只输出函数的序号。这就意味着应用程序只能用序号访问 DLL 中的函数。
新建一个工程时,使用 MFC AppWizard(dll) 自动创建 DLL ,它会创建一个 DEF 文件的框架并添加到这个工程中,而 Win32 Dynamic-Link Library 来生成 DLL ,都没有直接生成 DEF 文件,如果需要,必须自己创建并加入到工程中。
在编译建立 DLL 时,编译链接器查找工程中是否有 DEF 文件,如果有,将使用这个文件来生成一个导出文件 (.exp) 和一个导入库文件 (.lib) 。然后使用导出文件来生成一个 DLL 文件。若没有,则不能生成 LIB 文件,对于想隐式连接这个 DLL 的用户应用程序来说,将因找不到 DLL 导出的函数(就在 LIB 文件中)而在链接时失败。
(1) 使用关键字 _declspec(dllexport)
在定义函数时,可以使用关键字 _delspec(dllexport) 来从 DLL 中导出数据,函数和类或者类的成员函数。
例如:如下这样使用关键字来导出函数:
void _declspec(dllexport) Func1(void)
它用于类定义时,与在函数定义时一样使用,导出了类中的所有 public 数据成员和成员函数。
Class _declspec(dllexport) CMyExportClass:public CObject
{
 //……………….
}
如果使用这个关键字,则可以不需要 DEF 文件。编译时可以生成 LIB 文件提供给应用程序使用。
与这个关键字相对应,还有 _declspec(dllimport) 关键字可以用于在应用程序中引入 DLL 中的数据,函数和类及对象等。
使用时还要注意一个区别:
_declspec 用在声明变量或对象的类型名之前,例如:
_declspec(dllimport) class X{} varX;
这个属性应用在 varX 这个对象上,说明引入的是 DLL 中的 varX ,这成了 DLL 导出变量的方法。
_declspec 放在 class struct 关键字之后,应用在用户定义的类型名,例如:
Class _declspec(dllimport) X{};
说明引入的是 X 这种类定义结构,这样就可以在用户的应用程序中使用这个类定义。
(1) 使用 AFX_EXT_CLASS 导出
MFC 扩展 DLL 使用宏 AFX_EXT_CLASS 来导出类,链接这种 DLL 的应用程序或其他 DLL 使用这个宏来导入类。
MFC 自动生成的程序框架直接支持了在 DLL 和在应用程序中的这个宏定义,这样,使用 AFX_EXT_CLASS MFC 扩展 DLL 和应用程序就可以使用相同的头文件,给开发工作带来便利。
如果定义了 _AFXDLL _AFXEXT ,这表明目标文件是 MFC 扩展 DLL AFX_EXT_CLASS MFC 定义成了关键字 _declspec(dllexport) ,而只定义了 _AFXDLL, 没有定义 _AFXEXT ,这个宏被定义成了 _declspec(dllimport) ,供应用程序使用。实际上,在 MFC 框架中, _AFXDLL 表明使用共享 MFC DLL 。这个宏可以导出整个类,如:
class AFX_EXT_CLASS CMyExportClass::public CObject{….}
也可以导出类中的某个成员:
class CExampleDialog:public CDialog
{
 public:
      AFX_EXT_CLASS CExampleDialog();
      AFX_EXT_CLASS int DoModal();
}
五. DLL 中的数据和内存
(1)   DLL 中导出数据
DLL 既可以导出函数,也可以导出数据供应用程序使用。
1> 使用 DEF 文件 CONSTANT 关键词导出
DEF 文件中导出语句 EXPORTS 除了 NONAME 标志外,还有一个 CONSTANT 标志,表明前面的导出名不是函数名,而是一个数据变量。
例如:在 DLL 中定义了一个整型变量:
int nVariable = 0;
DEF 文件中使用下列语句:
EXPORTS
nVariable    @5 CONSTANT
在用户应用程序中,用以下语句来使用 DLL 导出的数据:
extern int nVariable;
printf(“DLL 中的 nVariable=%d”,*(int*)nVariable);
注意,这种方法使用的并不是变量本身,而是 DLL 中导出变量的指针,应用程序必须通过强制指针转换来使用。
这样使用,变量命名的约定( Windows 中用前缀表示变量的类型,指针变量前缀一般是 p )就和其他变量不统一了。另一个问题就是和 DLL 中的使用表示法不一样,可能给一同开发 DLL 和用户应用程序带来混乱。
可以采用以下的方法解决上述问题:
在应用程序中定义:
extern int nVariable;
#define nVariable *(int *)nVariable
2> 或在 DEF 文件中写成
EXPORTS
    pVariable = nVariable @5 CONSTANT
3> 应用程序中设计如下:
extern int pVariable;
printf(“DLL 中的 nVariable=%d”,*(int*)pVariable);
这样用不同的方法给变量起了别名,和命名约定与 DLL 中的名字就一致了。
其实使用 _declspec(dllimpot) 关键词就简单多了。
   _declspec(dllimport) int nVariable;
   printf(“DLL 中的 nVariable=%d”,nVariable);
4> 使用 DEF 文件 DATA 关键词导出   DEF 文件中用 DATA 关键词来代替 CONSTANT 使用,其他没有区别。
5> 使用 _declspec(dllexport) 关键词导出   也可以不使用 DEF 文件,而在源程序中使用 _declspec(dllexport) 关键词来修饰定义要导出的变量。
在用户应用程序中也要使用 _declspec(dllimport) 关键词来引入对 DLL 中导出变量的使用。
(2)   多个进程共享 DLL 中的数据和内存
首先说明一下 DLL 数据共享和导出数据的区别。导出数据是指这个 DLL 中的数据在应用程序视野之内,并可以使用,但它不一定是多进程共享的,而 DLL 中共享数据,是指多进程调用 DLL 时内存中只保留一个数据副本供它们共同拥有,它不一定导出,可能只是 DLL 内部使用而应用程序无法使用。
DLL 中定义的数据和申请的内存,缺省的一般都是每一个调用它的进程保留一份副本,不管是导出的还是 DLL 内部使用没有导出的。这可以看作是私有的。要想共享使用同一份 DLL 中数据和内存,需要设计实现。
在多线程中,同一个进程下的所有线程都共享使用这个进程的所有资源,对于 DLL 中的数据和内存也不例外。下面讨论在多进程中,也就是在不同的应用程序之间共享使用的情况。
1>    共享数据
在程序设计中,可以使用 #pragma data_seg(“.DatSegName”) 编译指令在编译这个 DLL 程序的指定声明一个数据段。
例如:
#pragma data_seg(“.MyDataSegName”)
int nVariable;
#pragma data_seg()
这样在 DLL 中声明了一个名为 .MyDataSegName 的数据段,其中的内容就是 nVariable 。名字前面以一个小点打头,通常这样约定:代码段名前加下划线,数据段名前打点。
如果不指明这个数据段的属性,它和其他段没什么区别,也就成为每个调用 DLL 的进程私有的了。在 DEF 文件中,使用 SECTIONS 语句来指明这个段的属性。
SECTIOS
    .MyDataSegName READ WRITE SHARED
SHARED 标志表明共享属性。
这样,当许多进程调用 DLL 时,它们都使用的是同一个 nVariable 变量。
    2> 动态分配共享内存
当在 DLL 中需要使用动态分配的内存作为共享内存,在多个进程中使用的时候,可以使用内存映射文件实现。
在第一个进程调用创建内存映射文件的程序时,它真正创建了这个对象,其他进程在用同一个内存映射文件名来创建时,系统返回的是同一个对象在不同进程中可以使用的对象句柄。这样在多个进程中就实现了共享这个内存映射文件。
只有当所有使用这个 DLL 的进程终止后,系统才将这个 DLL 从内存中释放,同时将这个内存映射文件对象删除。
六. 程序链接
DLL 不能单独运行,必须和用户应用程序相连接才能使用。
链接 DLL 到一个应用程序中主要有两种方式:隐式链接和显式链接。
(1)   隐式链接
使用 DLL 的应用程序先链接到编译 DLL 时生成的导入库 LIB 文件,在执行这个应用程序时,系统也装载它所需要的 DLL
采用这种方法,在应用程序退出之前, DLL 一直存在与该程序运行进程的地址空间中。
要使用隐式链接,用于应用程序必须能够从 DLL 开发者那里获得以下信息:
1)                包含导出函数以及类声明的头文件,在程序开发时要知道函数名和函数接口信息。
2)                DLL 的导入库 LIB 文件,应用程序在编译链接时需要。
3)                实际的 DLL 文件,它是在应用程序运行时所必需的。
应用程序中加入含有导出函数的头文件,这样在编程时调用导出函数和调用其他函数完全一样。
可以将 DLL 项目中的输出对外接口的头文件拷贝到自己的工程中,并填加到项目中来,或者在程序里直接用 include 语句将这个文件加上路径名来使用。
DLL LIB 文件加入应用程序中,可以使用 Project|Add To Project|Files… 菜单项弹出的对话框来选择相应的 LIB 文件,也可以用另外的方法,在 Project|Settings.… 弹出对话框中选择 Link 标签,在其中的 ”Object/Library Modules” 输入指定的 LIB 文件名。在应用程序编译链接时就可以找到这个导入库文件。
(2)   显式链接
显式链接是指应用程序在运行时通过函数调用来显示装载和下载 DLL ,并通过函数指针来调用 DLL 的导出函数。
1)      调用 LoadLibrary AfxLoadLibrary 函数装载 DLL 并得到模块句柄。
AfxLoadLibrary 函数原型如下:
HINSTANCE AFXAPI AfxLoadLibrary(LPCTSTR lpszModuleName);
参数 lpszModuleName 给出 DLL EXE 文件名,返回得到相应模块的句柄。
2)      调用 GetProcAddress 来获取导出函数的指针。
FARPROC GetProcAddress(HMODULE hModule,   //DLL 模块的句柄
LPCSTR lpProcName    // 要获取的函数的名字
);
这个函数通过给定的 lpProcName 函数的名字获得函数指针来调用。前面提到的 DEF 文件中可以将输出函数定义成 NONAME 的,只输出一个函数的序号,如果知道都是哪些函数的话,在此也可以这么使用:
GetProcAddress(hMyDllModule,MAKEINTRESOURCE(MyFuncID));
MyFuncID 即输出函数的序号, MAKEINTRESOURCE 是将整数转换成所使用字符串。
3)      在使用完毕以后,调用 FreeLibrary AfxFreeLibrary 函数来释放 DLL
AfxFreeLibrary 函数原型如下:
BOOL AFXAPI AfxFreeLibrary(HINSTANCE hInstLib);
hInstLib 就是前面装入的模块句柄,用这个函数将 DLL 从一个应用程序中卸载掉。
因为是使用指针来调用函数,并没有用函数名,所以在开发应用程序时,就不再需要太多的 DLL 信息,但对于函数接口必须要清楚,运行时也要提供相关的 DLL 文件。
使用显式链接,不需要 DLL 项目提供头文件,但使用函数的序号或只是函数指针来调用,很容易发生错误,设计时要协调好 DLL 和应用程序的接口。
七. DLL 的使用和调试
(1)   DLL 的使用
系统运行一个调用 DLL 的应用程序时,将在下列位置查找该 DLL
1)      含有该应用程序文件的目录。
2)      当前执行所在目录。
3)      Windows 的系统目录 (system system32)
4)      Windows 目录。
5)      在环境变量 PATH 中列出的目录。
若找不到这个 DLL 文件,系统将显式对话框提示并立即终止程序执行。所以在应用程序设计时要考虑到它使用的 DLL 的目录位置问题。
一个设计良好的应用程序,要考虑到本身所带的 DLL 问题,在软件安装时,将相关的程序和各种库文件,安装在各自目录中,而当删除时,又能将这些不再需要的文件清除干净,减少系统垃圾。
(2)   如何调试 DLL
由于 DLL 本身是不可执行的,给它的开发和调试工作带来了一定困难,现在 VC++6.0 的集成开发环境,强大的编辑调试功能基本上解决了这个问题。
1> 同时使用 DLL 和应用程序的工程来调试 DLL
如果开发 DLL 和用户应用程序两个工程,需要调试 DLL ,将两个工程在同一个工作区打开。
可以采用的方法有:将开发 DLL 程序的工程添加到开发应用程序工程的工作区中。使用 Project|Insert Project into Workspace….. 菜单弹出的对话框选择,也可以在已经打开一个工程的情况下,直接使用 File|New….. 菜单选择 Project 标签来创建另一个新工程,选中添加到当前工作区。
这样一个工作区中,有两个程序的工程同时进行开发。
为了能够调试 DLL 程序,两个工程都使用 Win32 Debug 版本。在 Project|Settings…. 的对话框 Link 标签下都选中 Generate Debug Info
Project|Settings….. 对话框的 Debug 标签下, Category 选择 Addtioal DLLs, 将要调试的 DLL 文件加入其中。每一个 Modules 前面的选中框表示在开始调试前是否装入这个模块文件。
同时,在 Project|Dependencies…. 对话框选择应用程序的工程依赖于 DLL 的工程,在 dll 程序发生改动时,编译用户应用程序,可以根据文件新旧比较把 DLL 的工程也编译链接了。
DLL 工程在程序改动后,编译链接生成新版本出来,为了不要在 DLL 和应用程序两个工程之间经常来回手工拷贝 dll 文件,在 DLL 工程的 Project|Settings… 对话框中 Post-Build Step 标签下可加上将编译好的 DLL 拷贝到应用程序能找到的目录的一个命令,如: ”copy /Debug/MyDll.dll c:/MyApp/Debug” ,这样做的目的是在经常编译调试 DLL 的同时,每编译一次之后,都执行这个拷贝命令,将最新版本的 DLL 文件提交给应用程序使用。
如果开发的应用程序使用隐式链接 DLL ,它需要从 DLL 工程中获取包含 DLL 导出信息的头文件和编译生成的导入库 LIB 文件。头文件在程序中用 #include 语句加入,使用 Project|Add to project|Files….. 菜单弹出的对话框将 DLL 工程的 LIB 文件加入到应用程序的工程中。
2>    使用应用程序的工程来调试 DLL
打开应用程序的工程,在 Project|Settings…. 菜单弹出框的 Debug 标签下, Category 选择 General ,在 Program Arguments 中指定应用程序命令行参数 ( 可以不设置 )
Category 选择 Addtioal DLLs ,指定调试的 DLL 文件。如果使用远程调试 ( Build|Debugger Remote Connection…. 菜单中设置 ) ,要给出完整的网络路径。
这个 DLL 必须是编译成 Win32 Debug 版本的程序,包含有调试信息。这样尽管 DLL 的源程序不是这个工程的组成部分,也可以在应用程序和 DLL 的源程序中设置断点。
3>    使用 DLL 的工程来调试 DLL
打开 DLL 的工程,在 Project|Settings…. 菜单弹出对话框的 Debug 标签下, Category 标签选择 General ,为调试这个 DLL 指定一个可执行程序,它可以就是另外开发的使用该 DLL 的用户应用程序。
4>    只有 DLL 文件和源程序来调试 DLL
或许你只有 DLL 文件和源程序,但没有工程文件,也就不知道源程序和这个 DLL 工程的关系,这种情况下也可以进行调试。使用 File|Open…. 菜单打开 DLL 文件,这个 DLL 文件必须是以前编译的包含有调试信息的文件,在 Project|Settings….. 菜单弹出对话框的 Debug 标签下, Category 选择 General ,为调试这个 Dll 指定一个可执行程序,就可以开始调试了。
通过上面的设置,无论是开发应用程序还是制作 DLL ,都简化了操作,而且确保使用的是最新的 DLL 版本。作好这些设置工作以后,就可以利用应用程序来对 DLL 程序进行调试了。在调试的过程中,可以从应用程序单步跟踪到 DLL 程序中去,在 DLL 中设置断点,在应用程序调用 DLL 中的程序,执行到这个断点时,也会中断,以便检查此时的 DLL 中的状态。
 
终于写完了,呵呵,写了半天,快累死了 ^_^ ,周末本来应该好好休息的,可是自己现在不总结一下,那就更没有时间了。还是我最喜欢的一句话,痛并快乐着 ^_* 。希望对大家有所帮助。

 

你可能感兴趣的:(工作,Class,dll,mfc,vc++,library)