注:本文为 “Windows DLL” 相关文章翻译合集。
未校。
Sunbreak
2021-02-23
这篇文章主要介绍了动态链接库(DLL)的相关内容,包括 DLL 与 C 语言运行时的故事、与应用程序的区别、使用优势、类型、链接方式(隐式和显式)、创建仅有资源的 DLL、导入和导出方法、初始化方式以及运行时的库行为等,并提供了相应的示例代码和说明。
原文地址:https://www.tenouk.com/ModuleBB.html
原文作者:
发布时间:约 2004 年前后
在这个模块中我们有什么?
__declspec
__declspec(dllimport)
导入应用程序__declspec(dllexport)
从 DLL 中导出。我的训练时间:xx 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
应该获得的能力。
注:本模块是一个通用的 MSDN 文档,涉及 Visual C++ 上的 C/C++ 运行时库和 MFC。让我们先来了解一下全貌吧!
动态链接库(DLL)是一个可执行文件,作为一个共享的函数库。动态链接为一个进程提供了一种方法来调用一个不属于其可执行代码的函数。函数的可执行代码位于 DLL 中,它包含一个或多个函数,这些函数被编译、链接,并与使用它们的进程分开存储。DLL 还有利于数据和资源的共享。多个应用程序可以同时访问内存中一个 DLL 副本的内容。动态链接与静态链接的不同之处在于,它允许可执行模块(无论是.dll 还是.exe 文件)在运行时只包含定位 DLL 函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库中获取所有被引用的函数,并将其与你的代码一起放入可执行文件中。使用动态链接代替静态链接有几个优点。DLL 可以节省内存,减少交换,节省磁盘空间,升级更容易,提供后市场支持,提供扩展 MFC 库类的机制,支持多语言程序,方便创建国际版本。
尽管 DLLs 和应用程序都是可执行的程序模块,但它们在几个方面是不同的。对于终端用户来说,最明显的区别是 DLL 不是可以直接执行的程序。从系统的角度来看,应用程序和 DLL 有两个根本的区别。
动态链接具有以下优点。
使用 DLL 的一个潜在的缺点是,应用程序并不是自成一体的;它依赖于一个单独的 DLL 模块的存在。
使用 Visual C++,你可以用 C 或 C++ 构建。
可执行文件以两种方式之一链接到(或加载)DLL。
隐式链接有时被称为静态加载或加载时动态链接。显式链接有时被称为动态加载或运行时动态链接。在隐式链接中,使用 DLL 的可执行文件链接到由 DLL 制作者提供的导入库(.LIB 文件)。当使用 DLL 的可执行程序被加载时,操作系统会加载该 DLL。客户端可执行文件调用 DLL 的导出函数,就像这些函数包含在可执行文件中一样。
使用显式链接时,使用 DLL 的可执行程序必须进行函数调用,以显式加载和卸载 DLL,并访问 DLL 的导出函数。客户端可执行文件必须通过函数指针来调用导出的函数。一个可执行文件可以用这两种链接方法使用同一个 DLL。此外,这些机制并不相互排斥,因为一个可执行文件可以隐式地链接到 DLL,而另一个可执行文件可以显式地附加到它。
要隐式链接到 DLL,可执行文件必须从 DLL 的提供者那里获得以下内容。
使用 DLL 的可执行文件必须在每个包含对导出函数调用的源文件中包含包含导出函数(或 C++ 类)的头文件。从编码的角度来看,对导出函数的函数调用就像其他函数调用一样。要建立调用的可执行文件,必须与导入库链接。如果你使用的是外部的 makefile,请指定导入库的文件名,在这里列出你要链接的其他对象(.OBJ)文件或库。操作系统在加载调用的可执行文件时,必须能够找到.DLL 文件。
通过显式链接,应用程序必须在运行时调用一个函数来显式加载 DLL。要显式链接到一个 DLL,应用程序必须。
例如
typedef UINT(CALLBACK* LPFNDLLFUNC1)(DWORD, UINT);
...
HINSTANCE hDLL; // Handle to DLL
LPFNDLLFUNC1 lpfnDllFunc1; // Function pointer
DWORD dwParam1;
UINT uParam2, uReturnVal;
hDLL = LoadLibrary("MyDLL");
if(hDLL != NULL)
{
lpfnDllFunc1 =(LPFNDLLFUNC1)GetProcAddress(hDLL, "DLLFunc1");
if(!lpfnDllFunc1)
{
//handle the error
FreeLibrary(hDLL);
return SOME_ERROR_CODE;
}
else
{
//call the function
uReturnVal = lpfnDllFunc1(dwParam1, uParam2);
}
}
当应用程序的代码调用导出的 DLL 函数时,会发生隐式链接。当调用可执行文件的源代码被编译或组装时,DLL 函数调用会在对象代码中产生一个外部函数引用。为了解决这个外部引用,应用程序必须与 DLL 制作者提供的导入库(.LIB 文件)链接。导入库只包含加载 DLL 和实现对 DLL 中函数调用的代码。在导入库中找到一个外部函数会通知链接器该函数的代码在 DLL 中。为了解决对 DLL 的外部引用,链接器只需在可执行文件中添加信息,告诉系统在进程启动时在哪里找到 DLL 代码。
当系统启动包含动态链接引用的程序时,它会使用程序可执行文件中的信息来定位所需的 DLL。如果无法定位 DLL,系统就会终止进程,并显示一个报告错误的对话框,如果正在构建应用程序,则可能输出以下错误信息。
testdll.obj : error LNK2019: unresolved external symbol "int __cdecl mydll(char *)"(?mydll@@YAHPAD@Z)referenced in function _main
Debug/mydlltest.exe : fatal error LNK1120: 1 unresolved externals
否则,系统会将 DLL 模块映射到进程的地址空间。如果任何一个 DLL 有一个入口点函数(用于初始化和终止代码),操作系统就会调用该函数。传递给切入点函数的参数之一指定了一个代码,该代码表示 DLL 附加到进程中。如果切入点函数没有返回 TRUE,系统就会终止进程并报告错误。
最后,系统修改进程的可执行代码,为 DLL 函数提供起始地址。与程序的其他代码一样,DLL 代码在进程启动时就被映射到进程的地址空间中,只有在需要时才加载到内存中。因此,在以前的 Windows 版本中,.DEF 文件用来控制加载的 PRELOAD 和 LOADONCALL 代码属性不再有意义。
大多数应用程序使用隐式链接,因为它是最简单的链接方法。然而,有时显式链接是必要的。下面是一些使用显式链接的常见原因。
这里有两个显式链接的危害需要注意。
__declspec(thread)
,如果显式链接,就会引起保护故障。在用 LoadLibrary()加载 DLL 后,只要代码引用这些数据,就会引起保护故障。(Static-extent 数据包括全局和本地静态项。)因此,当你创建一个 DLL 时,你应该避免使用线程本地存储,或者告知 DLL 用户潜在的陷阱(以防他们尝试动态加载)。纯资源 DLL 是一个只包含资源的 DLL,如图标、位图、字符串和对话框。使用只包含资源的 DLL 是在多个程序中共享同一资源集的好方法。它也是为应用程序提供多语言本地化资源的好方法。要创建一个资源专用 DLL,您需要创建一个新的 Win32 DLL(非 MFC)项目,并将您的资源添加到该项目中。
_main
的引用链接到 DLL 中;创建一个仅有资源的 DLL 时需要这个选项。使用资源专用 DLL 的应用程序应该调用 LoadLibrary()来显式链接到 DLL。要访问资源,可以调用通用函数 FindResource()和 LoadResource(),这两个函数适用于任何类型的资源,或者调用以下资源专用函数之一。
当应用程序使用完资源后,应该调用 FreeLibrary()。
您可以使用两种方法将公共符号导入应用程序或从 DLL 中导出函数。
__declspec(dllimport)
或 __declspec(dllexport)
。模块定义(.DEF)文件是一个文本文件,它包含了一个或多个模块声明,这些声明描述了 DLL 的各种属性。如果你没有使用 __declspec(dllimport)
或 __declspec(dllexport)
来导出 DLL 的函数,那么 DLL 需要一个 .DEF 文件。你可以使用 .DEF 文件导入到应用程序中或者从 DLL 中导出。
__declspec
32 位版本的 Visual C++ 使用 __declspec(dllimport)
和 __declspec(dllexport)
来代替以前在 16 位版本的 Visual C++ 中使用的 __export
关键字。你不需要使用 __declspec(dllimport)
来让你的代码正确编译,但是这样做可以让编译器生成更好的代码。编译器能够生成更好的代码,因为它知道一个函数是否存在于 DLL 中,所以编译器可以生成跳过通常会出现在一个跨越 DLL 边界的函数调用中的间接层次的代码。然而,你必须使用 __declspec(dllimport)
来导入 DLL 中使用的变量。如果使用适当的.DEF 文件 EXPORTS 部分,__declspec(dllexport)
是不需要的。增加了 __declspec(dllexport)
来提供一种简单的方法来从 .EXE 或 .DLL 中导出函数,而无需使用 .DEF 文件。Win32 Portable Executable(PE)格式被设计为最小化为修复导入而必须触及的页面数量。为了做到这一点,它将任何程序的所有导入地址放在一个叫做导入地址表的地方。这使得加载器在访问这些导入时,只需修改一两个页面。
__declspec(dllimport)
导入应用程序一个使用 DLL 定义的公共符号的程序被称为导入它们。当你为使用你的 DLL 来构建的应用程序创建头文件时,在公共符号的声明中使用 __declspec(dllimport)
。无论你是用.DEF 文件还是用 __declspec(dllexport)
关键字导出,关键字 __declspec(dllimport)
都能发挥作用。为了使你的代码更易读,定义一个 __declspec(dllimport)
的宏,然后用这个宏来声明每个导入的符号。
#define DllImport __declspec(dllimport)
DllImport int j;
DllImport void func();
在函数声明中使用 __declspec(dllimport)
是可选的,但是如果你使用这个关键字,编译器会产生更有效的代码。然而,你必须使用 __declspec(dllimport)
才能让导入的可执行文件访问 DLL 的公共数据符号和对象。请注意,您的 DLL 的用户仍然需要与导入库链接。您可以为 DLL 和客户端应用程序使用相同的头文件。要做到这一点,请使用一个特殊的预处理符号,它表明你是在构建 DLL 还是在构建客户端应用程序。例如
#ifdef _EXPORTING
#define CLASS_DECLSPEC __declspec(dllexport)
#else
#define CLASS_DECLSPEC __declspec(dllimport)
#endif
class CLASS_DECLSPEC CExampleA : public CObject
{
... class definition ... };
.DLL 文件的布局与.EXE 文件非常相似,但有一个重要的区别:DLL 文件包含一个导出表。导出表包含 DLL 向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL 中的任何其他函数都是 DLL 的私有函数。DLL 的导出表可以通过 DUMPBIN 工具(Visual Studio 自带的,或者你可以尝试更强大的工具,PEBrowser( www.smidgeonsoft.prohosting.com/),使用 / EXPORTS 选项来查看。你可以使用两种方法从 DLL 中导出函数。
__declspec(dllexport)
。当用这两种方法导出函数时,一定要使用 __stdcall
调用约定。模块定义(.DEF)文件是一个文本文件,它包含了一个或多个描述 DLL 各种属性的模块语句。如果你没有使用 __declspec(dllexport)
关键字来导出 DLL 的函数,DLL 需要一个.DEF 文件。一个最小的.DEF 文件必须包含以下模块定义语句。
例如,一个包含实现二进制搜索树的代码的 DLL 可能看起来像下面这样。
LIBRARY BTREE
EXPORTS
Insert @1
Delete @2
Member @3
Min @4
如果您使用 MFC DLL 向导来创建 MFC DLL,向导会为您创建一个骨架 .DEF 文件,并自动将其添加到您的项目中。添加要导出到该文件的函数名称。对于非 MFC DLL,您必须自己创建 .DEF 文件并将其添加到您的项目中。如果您要导出 C++ 文件中的函数,您必须将装饰的名称放在.DEF 文件中,或者使用 extern “C” 用标准的 C 语言链接定义您导出的函数。如果您需要将装饰名放在.DEF 文件中,您可以使用 DUMPBIN 工具或使用链接器 / MAP 选项来获得它们。注意,编译器产生的装饰名是编译器特有的。如果您将 Visual C++ 编译器产生的装饰名放入.DEF 文件中,那么链接到您的 DLL 的应用程序也必须使用相同版本的 Visual C++ 来构建,以便调用应用程序中的装饰名与 DLL 的.DEF 文件中导出的名称相匹配。如果你正在构建一个扩展 DLL(MFC),并使用一个.DEF 文件导出,请在包含导出类的头文件的开头和结尾放置以下代码。
#undef AFX_DATA
#define AFX_DATA AFX_EXT_DATA
//
#undef AFX_DATA
#define AFX_DATA
这些行确保内部使用的 MFC 变量或添加到你的类中的 MFC 变量能从你的扩展 DLL 中导出(或导入)。例如,当使用 DECLARE_DYNAMIC 派生一个类时,该宏会展开将一个 CRuntimeClass 成员变量添加到你的类中。漏掉这四行可能会导致你的 DLL 编译或链接不正确,或者在客户端应用程序链接到 DLL 时导致错误。
当构建 DLL 时,链接器使用.DEF 文件创建一个导出(.EXP)文件和一个导入库(.LIB)文件。然后链接器使用导出文件来构建.DLL 文件。隐式链接到 DLL 的可执行文件会在构建时链接到导入库。请注意,MFC 本身使用.DEF 文件从 MFCx0.DLL 导出函数和类。
__declspec(dllexport)
从 DLL 中导出。微软在 Visual C++ 的 16 位编译器版本中引入了 __export
,允许编译器自动生成导出名,并将它们放在一个.LIB 文件中。然后,这个.LIB 文件就可以像静态的.LIB 一样,用来与 DLL 链接。在 32 位编译器版本中,你可以使用 __declspec(dllexport)
关键字从 DLL 中导出数据、函数、类或类成员函数。__declspec(dllexport)
将导出指令添加到对象文件中,因此你不需要使用 .DEF 文件。这种便利性在尝试导出装饰的 C++ 函数名时最为明显。由于没有标准的名称装饰规范,所以在不同的编译器版本之间,导出的函数名称可能会发生变化。如果你使用 __declspec(dllexport)
,重新编译 DLL 和依赖的.EXE 文件是必要的,只是为了说明任何命名约定的变化。许多导出指令,如 ordinals、NONAME 和 PRIVATE,只能在 a.DEF 文件中进行,没有 a.DEF 文件就无法指定这些属性。然而,除了使用 .DEF 文件之外,使用 __declspec(dllexport)
不会导致构建错误。要导出函数,__declspec(dllexport)
关键字必须出现在调用约定关键字的左边,如果有指定关键字的话。例如:
__declspec(dllexport)void __stdcall WilBeExportedFunctionName(void);
而真正的例子可能是这样的。
__declspec(dllexport)int mydll(LPTSTR lpszMsg)
要导出一个类中所有的公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示。
class __declspec(dllexport)CExampleExport : public CObject
{
... class definition ... };
当构建你的 DLL 时,你通常会创建一个头文件,其中包含你要导出的函数原型和 / 或类,并在头文件的声明中添加 __declspec(dllexport)
。为了使你的代码更易读,为 __declspec(dllexport)
定义一个宏,并对你要导出的每个符号使用这个宏。
#define DllExport __declspec(dllexport)
__declspec(dllexport)
在 DLL 的导出表中存储函数名。
通常,你的 DLL 有初始化代码(如分配内存),当你的 DLL 加载时必须执行。当使用 Visual C++ 时,你在哪里添加代码来初始化你的 DLL 取决于你正在构建的 DLL 的种类。如果你不需要添加初始化或终止代码,那么在构建你的 DLL 时就没有什么特别的事情要做。如果你需要初始化你的 DLL,下面的表格描述了添加代码的位置。
DLL 的类型 | 在哪里添加初始化和终止代码 |
---|---|
常规 DLL | 在 DLL 的 CWinApp()对象的 InitInstance()和 ExitInstance()中 |
扩展 DLL | 在 MFC DLL 向导生成的 DllMain()函数中 |
非 MFC DLL | 在你提供的一个名为 DllMain()的函数中。 |
在 Win32 中,所有的 DLL 都可能包含一个可选的入口点函数(通常称为 DllMain()),该函数在初始化和终止时都会被调用。这使你有机会根据需要分配或释放额外的资源。Windows 在四种情况下调用入口点函数:进程附加、进程分离、线程附加和线程分离。C 运行时库提供了一个名为 _DllMainCRTStartup()
的入口点函数,它调用 DllMain()。根据 DLL 的种类,要么在源代码中应该有一个叫做 DllMain()的函数,要么使用 MFC 库中提供的 DllMain()。
由于扩展 DLL 没有 CWinApp 派生的对象(和普通 DLL 一样),所以你应该将你的初始化和终止代码添加到 MFC DLL 向导生成的 DllMain()函数中。向导提供了以下扩展 DLL 的代码。在下面的代码部分,PROJNAME 是你的项目名称的占位符。
#include "stdafx.h"
#include
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE [] = __FILE__;
#endif
static AFX_EXTENSION_MODULE PROJNAMEDLL = {
NULL, NULL };
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
if(dwReason == DLL_PROCESS_ATTACH)
{
TRACE0("PROJNAME.DLL Initializing!\n");
// Extension DLL one-time initialization
AfxInitExtensionModule(PROJNAMEDLL, hInstance)