注:本文为掘金 Sunbreak 博主“ Windows DLL 系列 ” DeepL/Translator 的翻译合集。
未校。
Windows 动态链接库 - DLL 第一部分:故事
Sunbreak
2021-02-23
这篇文章主要介绍了动态链接库(DLL)的相关内容,包括 DLL 与 C 语言运行时的故事、与应用程序的区别、使用优势、类型、链接方式(隐式和显式)、创建仅有资源的 DLL、导入和导出方法、初始化方式以及运行时的库行为等,并提供了相应的示例代码和说明。
原文地址:https://www.tenouk/ModuleBB.html
原文作者:<www.tenouk/>
发布时间:约 2004 年前后
在这个模块中我们有什么?
- 动态链接库和 C 语言运行时故事
- 应用程序和 DLL 的区别
- 使用 DLL 的优势
- DLL 的类型
- 将一个可执行文件链接到一个 DLL
- 隐性使用链接
- 明确使用链接
- 确定使用哪种链接方法
- 隐性链接
- 明确链接
- 创建一个仅有资源的 DLL
- 进口和出口
- 使用.DEF 文件
- 使用
__declspec
- 使用
__declspec(dllimport)
导入应用程序 - 从 DLL 中导出
- 使用
__declspec(dllexport)
从 DLL 中导出。 - 初始化一个 DLL
- 初始化扩展 DLL(对于 MFC 程序)
- 初始化非 MFC DLLs
- 运行时的库行为
我的训练时间:xx 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
应该获得的能力。
- 能够理解、构建和运行动态链接库程序。
- 能够体会到使用 DLL 与普通应用相比的好处。
- 能够从 MSDN 文档中收集信息,以了解和使用 DLL。
动态链接库和 C 语言运行时的故事
注:本模块是一个通用的 MSDN 文档,涉及 Visual C++ 上的 C/C++ 运行时库和 MFC。让我们先来了解一下全貌吧!
动态链接库(DLL)是一个可执行文件,作为一个共享的函数库。动态链接为一个进程提供了一种方法来调用一个不属于其可执行代码的函数。函数的可执行代码位于 DLL 中,它包含一个或多个函数,这些函数被编译、链接,并与使用它们的进程分开存储。DLL 还有利于数据和资源的共享。多个应用程序可以同时访问内存中一个 DLL 副本的内容。动态链接与静态链接的不同之处在于,它允许可执行模块(无论是.dll 还是.exe 文件)在运行时只包含定位 DLL 函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库中获取所有被引用的函数,并将其与你的代码一起放入可执行文件中。使用动态链接代替静态链接有几个优点。DLL 可以节省内存,减少交换,节省磁盘空间,升级更容易,提供后市场支持,提供扩展 MFC 库类的机制,支持多语言程序,方便创建国际版本。
应用程序和 DLL 的区别
尽管 DLLs 和应用程序都是可执行的程序模块,但它们在几个方面是不同的。对于终端用户来说,最明显的区别是 DLL 不是可以直接执行的程序。从系统的角度来看,应用程序和 DLL 有两个根本的区别。
- 一个应用程序可以有多个实例同时在系统中运行,而一个 DLL 只能有一个实例。
- 应用程序可以拥有诸如堆栈、全局内存、文件句柄和消息队列等东西,但 DLL 不能。
使用 DLL 的优势
动态链接具有以下优点。
- 节省内存,减少交换。许多进程可以同时使用一个 DLL,在内存中共享一个 DLL 的副本。相比之下,Windows 必须为每个使用静态链接库构建的应用程序在内存中加载一份库代码。
- 节省了磁盘空间。许多应用程序可以在磁盘上共享 DLL 的单一副本。相比之下,每个使用静态链接库构建的应用程序都有库代码作为单独的副本链接到其可执行映像中。
- DLL 的升级更容易。当 DLL 中的函数发生变化时,只要函数的参数和返回值不发生变化,使用它们的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- 提供后市场支持。例如,可以修改显示驱动 DLL,以支持应用程序出厂时没有的显示器。
- 支持多语言程序。用不同编程语言编写的程序可以调用相同的 DLL 函数,只要程序遵循函数的调用约定即可。程序和 DLL 函数必须在以下方面兼容:函数期望其参数被推到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及是否有任何参数在寄存器中传递。
- 提供了一个扩展 MFC 库类的机制。您可以从现有的 MFC 类中派生出类,并将它们放在 MFC 扩展 DLL 中,供 MFC 应用程序使用。
- 简化国际版本的创建。通过在 DLL 中放置资源,可以更容易地创建应用程序的国际版本。您可以将您的应用程序的每个语言版本的字符串放置在一个单独的资源 DLL 中,并让不同语言版本加载适当的资源。
使用 DLL 的一个潜在的缺点是,应用程序并不是自成一体的;它依赖于一个单独的 DLL 模块的存在。
DLL 的类型
使用 Visual C++,你可以用 C 或 C++ 构建。
- 用 C 或 C++ 构建不使用微软基础类库(MFC)的 Win32 DLL。
- 你可以用 Win32 应用向导创建一个非 MFC DLL 项目。
- 通过 MFC DLL 向导可以获得 MFC 库本身,可以是静态链接库,也可以是一些 DLL。如果你的 DLL 使用的是 MFC,Visual C++ 支持三种不同的 DLL 开发方案。
- 构建一个静态链接 MFC 的常规 DLL。
- 构建一个动态链接 MFC 的常规 DLL。
- 构建一个 MFC 扩展 DLL。这些总是动态链接 MFC。
将一个可执行文件链接到一个 DLL
可执行文件以两种方式之一链接到(或加载)DLL。
- 隐式链接
- 显式链接
隐式链接有时被称为静态加载或加载时动态链接。显式链接有时被称为动态加载或运行时动态链接。在隐式链接中,使用 DLL 的可执行文件链接到由 DLL 制作者提供的导入库(.LIB 文件)。当使用 DLL 的可执行程序被加载时,操作系统会加载该 DLL。客户端可执行文件调用 DLL 的导出函数,就像这些函数包含在可执行文件中一样。
使用显式链接时,使用 DLL 的可执行程序必须进行函数调用,以显式加载和卸载 DLL,并访问 DLL 的导出函数。客户端可执行文件必须通过函数指针来调用导出的函数。一个可执行文件可以用这两种链接方法使用同一个 DLL。此外,这些机制并不相互排斥,因为一个可执行文件可以隐式地链接到 DLL,而另一个可执行文件可以显式地附加到它。
使用隐式链接
要隐式链接到 DLL,可执行文件必须从 DLL 的提供者那里获得以下内容。
- 一个头文件(.H 文件),包含导出的函数和 / 或 C++ 类的声明。
- 要链接的导入库(.LIB 文件)。当 DLL 被构建时,链接器会创建导入库。
- 实际的 DLL(.DLL 文件)。
使用 DLL 的可执行文件必须在每个包含对导出函数调用的源文件中包含包含导出函数(或 C++ 类)的头文件。从编码的角度来看,对导出函数的函数调用就像其他函数调用一样。要建立调用的可执行文件,必须与导入库链接。如果你使用的是外部的 makefile,请指定导入库的文件名,在这里列出你要链接的其他对象(.OBJ)文件或库。操作系统在加载调用的可执行文件时,必须能够找到.DLL 文件。
使用显式链接
通过显式链接,应用程序必须在运行时调用一个函数来显式加载 DLL。要显式链接到一个 DLL,应用程序必须。
- 调用 LoadLibrary()(或类似的函数)来加载 DLL 并获得一个模块句柄。
- 调用 GetProcAddress()获得应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针来调用 DLL 的函数,编译器不会产生外部引用,所以不需要与导入库链接。
- 当处理完 DLL 后,调用 FreeLibrary()。
例如
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 代码属性不再有意义。
显式链接
大多数应用程序使用隐式链接,因为它是最简单的链接方法。然而,有时显式链接是必要的。下面是一些使用显式链接的常见原因。
- 应用程序在运行前不知道必须加载的 DLL 的名称。例如,应用程序可能需要从配置文件中获取 DLL 的名称和导出的函数。
- 如果在进程启动时没有找到 DLL,使用隐式链接的进程就会被操作系统终止。使用显式链接的进程在这种情况下不会被终止,可以尝试从错误中恢复。例如,进程可以通知用户这个错误,并让用户指定另一个 DLL 的路径。
- 如果任何一个被链接到的 DLLs 的 DllMain()函数失败,使用隐式链接的进程也会被终止。在这种情况下,使用显式链接的进程不会被终止。
- 隐式链接到许多 DLL 的应用程序可能会启动缓慢,因为 Windows 在应用程序加载时加载所有的 DLL。为了提高启动性能,应用程序可以在加载后立即隐式链接到那些需要的 DLL,并等待在需要时显式链接到其他 DLL。
- 显式链接消除了用导入库链接应用程序的需要。如果 DLL 中的变化导致导出序数发生变化,使用显式链接的应用程序不必重新链接(假设他们调用 GetProcAddress()时使用的是函数名而不是序数值),而使用隐式链接的应用程序必须重新链接到新的导入库。
这里有两个显式链接的危害需要注意。
- 如果 DLL 有一个 DllMain()入口点函数,操作系统会在调用 LoadLibrary()的线程的上下文中调用该函数。如果因为之前调用 LoadLibrary()而没有相应调用 FreeLibrary()函数,DLL 已经被连接到进程中,那么这个入口点函数就不会被调用。如果 DLL 使用 DllMain()函数为进程的每个线程执行初始化,那么显式链接可能会引起问题,因为当 LoadLibrary()(或 AfxLoadLibrary())被调用时存在的线程将不会被初始化。
- 如果一个 DLL 将静态扩展数据声明为
__declspec(thread)
,如果显式链接,就会引起保护故障。在用 LoadLibrary()加载 DLL 后,只要代码引用这些数据,就会引起保护故障。(Static-extent 数据包括全局和本地静态项。)因此,当你创建一个 DLL 时,你应该避免使用线程本地存储,或者告知 DLL 用户潜在的陷阱(以防他们尝试动态加载)。
创建一个仅有资源的 DLL
纯资源 DLL 是一个只包含资源的 DLL,如图标、位图、字符串和对话框。使用只包含资源的 DLL 是在多个程序中共享同一资源集的好方法。它也是为应用程序提供多语言本地化资源的好方法。要创建一个资源专用 DLL,您需要创建一个新的 Win32 DLL(非 MFC)项目,并将您的资源添加到该项目中。
- 在 “新建项目” 对话框中选择 “Win32 项目”,并在 “Win32 项目向导” 中指定 DLL 项目类型。
- 为 DLL 创建一个包含资源(如字符串或菜单)的新资源脚本,并保存.rc 文件。
- 在 “项目” 菜单上,单击 “添加现有项目”,并将新的.rc 文件插入到项目中。
- 指定 / NOENTRY 链接器选项。/NOENTRY 可以防止链接器将对
_main
的引用链接到 DLL 中;创建一个仅有资源的 DLL 时需要这个选项。 - 构建 DLL。
使用资源专用 DLL 的应用程序应该调用 LoadLibrary()来显式链接到 DLL。要访问资源,可以调用通用函数 FindResource()和 LoadResource(),这两个函数适用于任何类型的资源,或者调用以下资源专用函数之一。
- FormatMessage()
- LoadAccelerators()
- LoadBitmap()
- LoadCursor()
- LoadIcon()
- LoadMenu()
- LoadString()
当应用程序使用完资源后,应该调用 FreeLibrary()。
导入和导出
您可以使用两种方法将公共符号导入应用程序或从 DLL 中导出函数。
- 在构建 DLL 时使用模块定义(.DEF)文件。
- 在主应用程序的函数定义中使用关键字
__declspec(dllimport)
或__declspec(dllexport)
。
使用.DEF 文件
模块定义(.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 中导出
.DLL 文件的布局与.EXE 文件非常相似,但有一个重要的区别:DLL 文件包含一个导出表。导出表包含 DLL 向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL 中的任何其他函数都是 DLL 的私有函数。DLL 的导出表可以通过 DUMPBIN 工具(Visual Studio 自带的,或者你可以尝试更强大的工具,PEBrowser( www.smidgeonsoft.prohosting/),使用 / EXPORTS 选项来查看。你可以使用两种方法从 DLL 中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建 DLL 时使用该.DEF 文件。如果你想从 DLL 中按序号而不是按 byname 导出函数,请使用这种方法。
- 在函数的定义中使用关键字
__declspec(dllexport)
。
当用这两种方法导出函数时,一定要使用 __stdcall
调用约定。模块定义(.DEF)文件是一个文本文件,它包含了一个或多个描述 DLL 各种属性的模块语句。如果你没有使用 __declspec(dllexport)
关键字来导出 DLL 的函数,DLL 需要一个.DEF 文件。一个最小的.DEF 文件必须包含以下模块定义语句。
- 文件中的第一条语句必须是 LIBRARY 语句。该语句标识了.DEF 文件属于一个 DLL。LIBRARY 语句后面是 DLL 的名称。链接器将这个名字放在 DLL 的导入库中。
- EXPORTS 语句列出了 DLL 导出的函数的名称和可选的序数值。你可以通过在函数名称后面用 at 符号(@)和一个数字给函数分配一个序数值。当您指定序数值时,它们必须在 1 到 N 的范围内,其中 N 是 DLL 导出的函数的数量。
例如,一个包含实现二进制搜索树的代码的 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
// <body of your header file>
#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 有初始化代码(如分配内存),当你的 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(对于 MFC 程序)
由于扩展 DLL 没有 CWinApp 派生的对象(和普通 DLL 一样),所以你应该将你的初始化和终止代码添加到 MFC DLL 向导生成的 DllMain()函数中。向导提供了以下扩展 DLL 的代码。在下面的代码部分,PROJNAME 是你的项目名称的占位符。
#include "stdafx.h"
#include <afxdllx.h>
#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);
// Insert this DLL into the resource chain
new CDynLinkLibrary(Dll3DLL);
}
else if(dwReason == DLL_PROCESS_DETACH)
{
TRACE0("PROJNAME.DLL Terminating!\n");
}
return 1; //ok
}
MFC 扩展 DLL 相关操作说明
在初始化过程中创建一个新的 CDynLinkLibrary
对象,允许扩展 DLL
导出 CRuntimeClass
对象或资源到客户端应用程序。
若要从一个或多个常规 DLL
中使用你的扩展 DLL
,必须导出一个创建 CDynLinkLibrary
对象的初始化函数。该函数必须从每个使用扩展 DLL
的常规 DLL
中调用。在使用任何扩展 DLL
的导出类或函数之前,调用这个初始化函数的合适位置是在常规 DLL
的 CWinApp
派生对象的 InitInstance()
成员函数中。
在 MFC DLL
向导生成的 DllMain()
中,对 AfxInitExtensionModule
的调用捕获了模块的运行时类(CRuntimeClass
结构)以及它的对象工厂(COleObjectFactory
对象),以便在创建 CDynLinkLibrary
对象时使用。应当检查 AfxInitExtensionModule
的返回值;若从 AfxInitExtensionModule
返回的值为零,则从 DllMain()
函数中返回零。
若你的扩展 DLL
将被显式链接到一个可执行文件(意味着可执行文件调用 AfxLoadLibrary
来链接到 DLL
),应当在 DLL_PROCESS_DETACH
上添加对 AfxTermExtensionModule
的调用。这个函数允许 MFC
在每个进程脱离扩展 DLL
时清理扩展 DLL
(这发生在进程退出时,或者当 DLL
因 AfxFreeLibrary
调用而被卸载时)。若你的扩展 DLL
将隐式链接到应用程序,那么对 AfxTermExtensionModule
的调用是不必要的。显式链接到扩展 DLL
的应用程序必须在释放 DLL
时调用 AfxTermExtensionModule
。
如果应用程序使用多个线程,它们还应当使用 AfxLoadLibrary
和 AfxFreeLibrary
(而非 Win32
函数 LoadLibrary()
和 FreeLibrary()
)。使用 AfxLoadLibrary
和 AfxFreeLibrary
能够确保在加载和卸载扩展 DLL
时执行的启动和关闭代码不会破坏全局 MFC
状态。
因为 MFCx0.DLL
在调用 DllMain
的时候已经完全初始化了,所以可以在 DllMain
中分配内存和调用 MFC
函数(与 16 位版本的 MFC
不同)。扩展 DLL
可以通过处理 DllMain()
函数中的 DLL_THREAD_ATTACH
和 DLL_THREAD_DETACH
情况来处理多线程。当线程附加和脱离 DLL
时,这些情况会传递给 DllMain()
。当 DLL
附加时,调用 TlsAlloc()
可以让 DLL
为每一个附加到 DLL
的线程维护线程本地存储(TLS
)索引。
请注意,头文件 AFXDLLX.H
中包含了扩展 DLL
中使用的结构的特殊定义,例如 AFX_EXTENSION_MODULE
和 CDynLinkLibrary
的定义。应当在你的扩展 DLL
中包含这个头文件。请注意,重要的是既不要定义也不要取消定义 stdafx.h
中的任何 _AFX_NO_XXX
宏。请注意,示例中包含了一个名为 LibMain()
的入口点函数,但应当将这个函数命名为 DllMain()
,以便它能与 MFC
和 C
运行时库一起工作。
初始化非 MFC DLLs
要初始化非 MFC DLL,你的 DLL 源代码必须包含一个叫做 DllMain()的函数。下面的代码提供了一个基本的骨架,展示了 DllMain()的定义。
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
...
case DLL_THREAD_ATTACH:
...
case DLL_THREAD_DETACH:
...
case DLL_PROCESS_DETACH:
...
}
return TRUE;
}
运行时的库行为
C/C++ 运行时库代码执行 DLL 启动序列,无需像 Windows 3.x 中那样需要与单独的模块链接。C/C++ 运行时库代码中包含了名为 DllMainCRTStartup()
DLL 入口点函数。DllMainCRTStartup()
数做了几件事,包括调用_CRT_INIT,它初始化 C/C++ 运行时库,并调用静态、非本地变量上的 C++ 构造函数。如果没有这个函数,运行时库将处于未初始化状态。CRT_INIT
可以用于静态链接的 CRT,也可以用于从用户 DLL 链接到 CRT DLLmsvcrt.dll。
虽然可以使用 / ENTRY: linker 选项指定另一个入口点函数,但不建议这样做,因为你的入口点函数将不得不重复 DllMainCRTStartup()
做的一切。当用 Visual C++ 构建 DLL 时,DllMainCRTStartup()
自动链接进来,你不需要使用 / ENTRY: linker 选项指定一个入口点函数。
除了初始化 C 运行时库之外,DllMainCRTStartup()
调用一个叫做 DllMain()的函数。根据你正在构建的 DLL 的种类,Visual C++ 为你提供了 DllMain()
,并且它被链接进来,这样 DllMainCRTStartup()
是有东西可以调用。这样一来,如果你不需要初始化你的 DLL,那么在构建你的 DLL 时就没有什么特别的事情要做。如果你需要初始化你的 DLL,你在哪里添加你的代码取决于你编写的 DLL 的种类。
C/C++ 运行时库代码在静态、非本地变量上调用构造函数和析构函数。例如,在下面的 DLL 源代码中,Equus 和 Sugar 是类 CHorse 的两个静态的、非局部的对象,定义在 HORSES.H
中。在源代码中没有包含对 CHorse 的构造函数的调用,也没有对 destructor 函数的调用,因为这些对象是在任何函数之外定义的。因此,对这些构造函数和反构造函数的调用必须由运行时代码来执行。应用程序的运行时库代码也执行这个功能。
#include "horses.h"
CHorse Equus(ARABIAN, MALE);
CHorse Sugar(THOROUGHBRED, FEMALE);
BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)
...
每当一个新进程试图使用 DLL 时,操作系统都会为 DLL 的数据创建一个单独的副本:这称为 “进程附加”。DLL 的运行时库代码会调用所有全局对象的构造函数(如果有的话),然后在选择进程附加的情况下调用 DllMain()
函数。与此相反的情况是进程 detach:运行时库代码在进程 detach 被选中的情况下调用 DllMain()
,然后调用一系列终止函数,包括 atexit()
函数、全局对象的析构函数和静态对象的析构函数。需要注意的是,进程 attach 中的事件顺序与进程 detach 中的顺序是相反的。
在线程附加和线程分离过程中,运行时库代码也会被调用,但运行时代码本身并不进行初始化或终止。
Windows 动态链接库 - DLL 第二部分:方案实例
Sunbreak
2021-02-23
这篇文章主要介绍了动态链接库(DLL)的相关知识,包括 DLL 与 Windows APIs 的关系、动态链接的类型(加载时和运行时)、DLL 与内存管理、优点、入口点函数、创建及更新 DLL、重定向、数据相关内容等,还提到了一些应掌握的 Win32 技能。
关联问题:DLL 方案有哪些 DLL 实例怎样动态链接库啥作用
原文地址:www.tenouk/ModuleCC.ht…
原文作者:www.tenouk/
发布时间:约 2004 年前后
我们在这个模块里有什么?
- 动态链接库和 Windows APIs 故事和示例
- 动态链接的类型
- 负载时间动态链接
- 运行时动态链接
- DLL 和内存管理
- 动态链接的优势
- 动态链接库入口点功能
- 调用进入点函数
- 入门点功能定义
- 输入点函数返回值
- 动态链接库创建
- 创建源文件
- 输出函数
- 使用__declspec(dllexport)从 DLL 中导出。
- 导出 C 函数在 C 或 C++ 语言可执行文件中使用。
- 创建一个导入库
- 动态链接库更新
- 动态链接库重定向
- 动态链接库数据
- 可变范围
- 动态内存分配
- 线程本地存储
我的训练时间:zz 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可参见 MFC GUI 编程步骤教程。
应该掌握的 Win32 技能。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的 DLL。
- 能够创建 DLL 的导出和导入函数。
动态链接库与 Windows API 的故事与实例。
注:本模块中提供的部分信息与上一模块重复。Tenouk 在此表示歉意。
动态链接库(DLL)是一个模块,它包含了可以被另一个模块(应用程序或另一个 DLL)使用的函数和数据。一个 DLL 可以定义两种函数。
- Exported - 是为了被其他模块调用,也可以从定义它们的 DLL 中调用。
- 内部函数。- 通常是为了只从定义它们的 DLL 内部调用。
虽然一个 DLL 可以导出数据,但它的数据一般只被它的函数使用。但是,并不能阻止另一个模块对该地址进行读写。DLL 提供了一种将应用程序模块化的方法,因此功能可以更容易地更新和重用。当几个应用程序同时使用相同的功能时,它们还有助于减少内存开销,因为虽然每个应用程序都得到自己的数据副本,但它们可以共享代码。
Windows 应用程序编程接口(API)是以一组动态链接库的形式实现的,因此任何使用 Windows API 的进程都会使用动态链接。动态链接允许一个模块在加载时或运行时只包含定位导出的 DLL 函数所需的信息。动态链接不同于我们更熟悉的静态链接,在静态链接中,链接器将库函数的代码复制到每个调用它的模块中。
动态链接的类型
有两种方法可以调用 DLL 中的函数。
- 在加载时动态链接中,一个模块(应用程序或其他模块)对导出的 DLL 函数进行显式调用,就像它们是本地函数一样。这需要将模块与包含函数的 DLL 的导入库进行链接。导入库为系统提供加载 DLL 所需的信息,并在应用程序加载时定位导出的 DLL 函数。
- 在运行时动态链接中,模块(应用程序或另一个模块)在运行时使用 LoadLibrary()或 LoadLibraryEx()函数加载 DLL。DLL 加载完毕后,模块调用 GetProcAddress()函数来获取导出的 DLL 函数的地址。模块使用 GetProcAddress()返回的函数指针调用导出的 DLL 函数。这样就不需要导入库了。
加载时动态链接
当系统启动一个使用加载时动态链接的程序时,会利用链接器放在文件中的信息来定位进程所使用的 DLL 的名称。然后系统依次在以下位置搜索 DLLs。
- 应用程序加载的目录
- 当前目录。
- 系统目录。使用 GetSystemDirectory()函数来获取该目录的路径。
- 16 位系统目录。没有获取该目录路径的函数,但会搜索该目录。对于 Windows Me/98/95:该目录不存在。
- Windows 目录。使用 GetWindowsDirectory()函数来获取该目录的路径。
- PATH 环境变量中列出的目录。在命令提示符下输入 PATH 命令,可确认目录。
对于 Windows Server 2003,Windows XP SP1:HKLM/System/CurrentControlSet/Control/Session Manager/SafeDllSearchMode 的默认值为 1(在系统和 Windows 目录之后搜索当前目录)。对于 Windows XP。 如果 HKLMSystem/CurrentControlSet/Control/Session Manager/SafeDllSearchMode 为 1,则在系统和 Windows 目录之后搜索当前目录,但在 PATH 环境变量中的目录之前搜索。默认值是 0(在系统和 Windows 目录之前搜索当前目录)。请注意,这个值在加载时是以每个进程为基础进行缓存的。
如果系统无法找到所需的 DLL,则会终止进程,并显示一个向用户报告错误的对话框。否则,系统会将 DLL 映射到进程的虚拟地址空间,并递增 DLL 引用计数。系统调用入口点函数。该函数接收到一个代码,表示进程正在加载 DLL。如果切入点函数没有返回 TRUE,系统将终止进程并报告错误。最后,系统用导入的 DLL 函数的起始地址修改函数地址表。DLL 在初始化过程中被映射到进程的虚拟地址空间中,只有在需要时才加载到物理内存中。
运行时动态链接
当应用程序调用 LoadLibrary()或 LoadLibraryEx()函数时,系统尝试使用加载时动态链接中使用的相同搜索序列来定位 DLL。如果搜索成功,系统将 DLL 模块映射到进程的虚拟地址空间,并递增引用计数。如果对 LoadLibrary()或 LoadLibraryEx()的调用指定了一个 DLL,其代码已经映射到调用进程的虚拟地址空间中,该函数只是返回一个 DLL 的句柄,并递增 DLL 的引用计数。请注意,两个基本文件名和扩展名相同但在不同目录下的 DLL 不被认为是同一个 DLL。
系统在调用 LoadLibrary()或 LoadLibraryEx()的线程的上下文中调用入口点函数。如果 DLL 已经被进程通过调用 LoadLibrary()或 LoadLibraryEx()加载,而没有相应的 FreeLibrary()函数的调用,则不调用入口点函数。
如果系统找不到 DLL,或者入口点函数返回 FALSE,LoadLibrary()或 LoadLibraryEx()返回 NULL。如果 LoadLibrary()或 LoadLibraryEx()成功,它将返回一个 DLL 模块的句柄。进程可以在调用 GetProcAddress()、FreeLibrary()或 FreeLibraryAndExitThread()函数时使用这个句柄来识别 DLL。
GetModuleHandle()函数返回一个在 GetProcAddress()、FreeLibrary()或 FreeLibraryAndExitThread()中使用的句柄。GetModuleHandle()函数只有在 DLL 模块已经通过加载时链接或通过之前对 LoadLibrary()或 LoadLibraryEx()的调用映射到进程的地址空间时才会成功。与 LoadLibrary()或 LoadLibraryEx()不同的是,GetModuleHandle()不会递增模块引用计数。GetModuleFileName()函数检索与 GetModuleHandle()、LoadLibrary()或 LoadLibraryEx()返回的句柄相关联的模块的完整路径。
进程可以使用 GetProcAddress()来获取 DLL 中使用 LoadLibrary()或 LoadLibraryEx()、GetModuleHandle()返回的 DLL 模块句柄的导出函数的地址。当不再需要 DLL 模块时,进程可以调用 FreeLibrary()或 FreeLibraryAndExitThread()。这些函数会递减模块引用计数,如果引用计数为零,则从进程的虚拟地址空间中解映射 DLL 代码。运行时动态链接使进程能够继续运行,即使 DLL 不可用。然后,进程可以使用另一种方法来完成其目标。例如,如果一个进程无法找到一个 DLL,它可以尝试使用另一个 DLL,或者它可以通知用户一个错误。如果用户能够提供缺失的 DLL 的完整路径,即使 DLL 不在正常的搜索路径中,进程也可以利用这个信息来加载该 DLL。这种情况与加载时链接形成了鲜明的对比,在加载时链接中,如果系统找不到 DLL,就会简单地终止进程。如果 DLL 使用 DllMain()函数为进程的每个线程执行初始化,运行时动态链接可能会引起问题,因为在调用 LoadLibrary()或 LoadLibraryEx()之前存在的线程不会调用入口点。
DLLs 和内存管理
每个加载 DLL 的进程都会将其映射到它的虚拟地址空间中。进程将 DLL 加载到其虚拟地址后,就可以调用导出的 DLL 函数。系统为每个 DLL 维护一个每线程的引用计数。当一个线程加载 DLL 时,引用计数会递增 1。当进程终止时,或者当参考计数变为零时(仅运行时动态链接),DLL 将从进程的虚拟地址空间中卸载。像任何其他函数一样,导出的 DLL 函数在调用它的线程的上下文中运行。因此,以下条件适用。
- 调用 DLL 的进程的线程可以使用 DLL 函数打开的句柄。同样,调用进程的任何线程打开的句柄也可以在 DLL 函数中使用。
- DLL 使用调用线程的栈和调用进程的虚拟地址空间。
- DLL 从调用进程的虚拟地址空间中分配内存。
动态链接的优点
与静态链接相比,动态链接具有以下优点。
- 在同一基址加载同一 DLL 的多个进程在物理内存中共享一个 DLL 的副本。这样做可以节省系统内存,减少交换。
- 当 DLL 中的函数发生变化时,只要函数参数、调用约定和返回值不发生变化,使用这些函数的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- DLL 可以提供后市场支持。例如,一个显示驱动 DLL 可以被修改为支持应用程序最初出厂时不可用的显示器。
- 用不同编程语言编写的程序可以调用相同的 DLL 函数,只要这些程序遵循函数使用的相同调用约定。调用惯例(如 C、Pascal 或标准调用)控制了调用函数必须将参数推到堆栈上的顺序,是函数还是调用函数负责清理堆栈,以及是否有任何参数在寄存器中传递。更多的信息,请参见编译器附带的文档。
使用 DLL 的一个潜在的缺点是应用程序不是自足的,它依赖于一个单独的 DLL 模块的存在。如果使用加载时动态链接的进程需要一个在进程启动时没有找到的 DLL,系统就会终止该进程,并给用户一个错误信息。在这种情况下,系统不会终止使用运行时动态链接的进程,但缺失的 DLL 所导出的函数对程序不可用。
动态链接库入口点函数
DLL 可以选择性地指定一个切入点函数,如果存在,系统会在进程或线程加载或卸载时调用切入点函数。如果存在,每当一个进程或线程加载或卸载 DLL 时,系统就会调用该入口点函数。它可以用来执行简单的初始化和清理任务。例如,它可以在创建新线程时设置线程本地存储,并在线程终止时清理它。
如果你将你的 DLL 与 C 运行时库链接,它可能会为你提供一个入口点函数,并允许你提供一个单独的初始化函数。查看你的运行时库的文档以了解更多信息。如果您提供了自己的入口点,请参见下一节中的 DllMain()函数。DllMain()这个名字是用户定义函数的占位符。您必须在构建 DLL 时指定实际使用的名称。
调用入口点函数
每当发生以下任何一个事件时,系统就会调用入口点函数。
- 进程加载 DLL。对于使用加载时动态链接的进程,DLL 在进程初始化期间被加载。对于使用运行时链接的进程,DLL 在 LoadLibrary()或 LoadLibraryEx()返回之前被加载。
- 进程会卸载 DLL。当进程终止或调用 FreeLibrary()函数,并且引用计数为零时,DLL 被卸载。如果进程因 TerminateProcess()或 TerminateThread()函数而终止,系统不会调用 DLL 入口点函数。
- 在加载了 DLL 的进程中会创建一个新的线程。你可以使用 DisableThreadLibraryCalls()函数来禁用线程创建时的通知。
- 已加载 DLL 的进程的线程正常终止,不使用 TerminateThread()或 TerminateProcess()。当进程卸载 DLL 时,入口点函数只对整个进程调用一次,而不是对进程的每个现有线程调用一次。你可以使用 DisableThreadLibraryCalls()来禁止线程终止时的通知。
每次只有一个线程可以调用入口点函数。系统在引起调用函数的进程或线程的上下文中调用切入点函数,这允许 DLL 使用其切入点函数在调用进程的虚拟地址空间中分配内存或打开进程可访问的句柄。这使得 DLL 可以使用它的切入点函数在调用进程的虚拟地址空间中分配内存,或者打开进程可以访问的句柄。切入点函数还可以通过使用线程本地存储(TLS)为新线程分配私有的内存。
入门点函数定义
DLL 入口点函数必须用标准调用约定(__stdcall
)来声明。若没有正确声明 DLL 入口点,则 DLL 不会被加载,系统会显示一条消息,表明必须用 WINAPI
声明 DLL 入口点。
对于 Windows Me/98/95:若 DLL 入口点声明不正确,则 DLL 未被加载,系统显示一条名为 "Error starting program"
的消息,指示用户检查文件以确定问题所在。
在函数的主体中,可以处理以下 DLL 入口点被调用的任意组合情况:
- 一个进程加载 DLL(
DLL_PROCESS_ATTACH
)。 - 当前进程创建了一个新的线程(
DLL_THREAD_ATTACH
)。 - 一个线程正常退出(
DLL_THREAD_DETACH
)。 - 一个进程卸载 DLL(
DLL_PROCESS_DETACH
)。
入口点函数应该只执行简单的初始化任务。它不能调用 LoadLibrary()
或 LoadLibraryEx()
函数(或调用这些函数的函数),因为这可能会在 DLL 加载顺序中产生依赖性循环,这可能导致 DLL 在系统执行其初始化代码之前就被使用。
同样,入口点函数不能调用 FreeLibrary()
函数(或调用 FreeLibrary()
的函数),因为这可能会导致在系统执行其终止代码后使用 DLL。
调用 Kernel32.dll
中的其他函数是安全的,因为当调用切入点函数时,保证这个 DLL 会被加载到进程地址空间中。通常情况下,entry-point
函数会创建同步对象,如关键部分和 mutexes,并使用 TLS。
不要调用注册表函数,因为它们位于 Advapi32.dll
中。
如果你正在与 C 运行时库进行动态链接,不要调用 malloc()
,而是调用 HeapAlloc()
。
调用 Kernel32.dll
以外的导入函数可能会导致难以诊断的问题。例如,调用 User
、Shell
和 COM
函数可能会导致访问违规错误,因为它们的 DLL 中的一些函数会调用 LoadLibrary()
来加载其他系统组件。
下面的例子演示了如何构造 DLL 入口点函数。
#include <windows.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, //handle to DLL module
DWORD fdwReason, //reason for calling function
LPVOID lpReserved) //reserved
{
// Perform actions based on the reason for calling.
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
// Successful DLL_PROCESS_ATTACH.
return TRUE;
}
输入点函数返回值
当因为进程正在加载而调用 DLL 入口点函数时,该函数返回 TRUE 表示成功。对于使用加载时链接的进程,返回值为 FALSE 会导致进程初始化失败,进程终止。对于使用运行时链接的进程,返回值为 FALSE 会导致 LoadLibrary()
或 LoadLibraryEx()
函数返回 NULL,表示失败。系统会立即用 DLL_PROCESS_DETACH
调用你的入口点函数,并卸载 DLL。当该函数因其他原因被调用时,入口点函数的返回值将被忽略。
动态链接库的创建
要创建一个动态链接库(DLL),你必须创建一个或多个源代码文件,也可能创建一个用于导出函数的链接器文件。如果您计划允许使用您的 DLL 的应用程序使用加载时动态链接,您还必须创建一个导入库。
创建源代码文件
DLL 的源文件包含导出的函数和数据,内部函数和数据,以及 DLL 的可选入口点函数。您可以使用任何支持创建基于 Windows 的 DLL 的开发工具。如果你的 DLL 可能被一个多线程的应用程序使用,你应该使你的 DLL 成为 “线程安全”。你必须同步访问 DLL 的所有全局数据以避免数据损坏。你还必须确保你只链接到线程安全的库。例如,Microsoft 速 Visual C++ 速包含了多个版本的 C 运行时库,一个是不线程安全的,两个是线程安全的。更多细节请参考模块 A。
导出函数
如何指定 DLL 中的哪些函数应该被导出取决于你所使用的开发工具。一些编译器允许你通过在函数声明中使用修饰符直接在源代码中导出函数。其他时候,你必须在传递给链接器的文件中指定导出。例如,使用 Visual C++,有两种可能的方法来导出 DLL 函数。
- 用
__declspec
修饰符或者是… - 用一个.def 文件。
如果你使用 __declspec
修饰符,就没有必要使用 .def 文件。
使用 __declspec(dllexport)
从 DLL 中导出。
.DLL 文件的布局与.EXE 文件非常相似,但有一个重要的区别:DLL 文件包含一个导出表。导出表包含 DLL 向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL 中的任何其他函数都是 DLL 的私有函数。可以通过 DUMPBIN 工具的 / EXPORTS 选项来查看 DLL 的导出表。您可以使用两种方法从 DLL 中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建 DLL 时使用该.DEF 文件。如果你想从 DLL 中按序号而不是按名称导出函数,请使用这种方法。
- 在函数的定义中使用关键字__declspec(dllexport)。
当用这两种方法导出函数时,一定要使用 __stdcall
的调用约定。微软在 Visual C++ 的 16 位编译器版本中引入了 __export
,允许编译器自动生成导出名,并将它们放在一个.LIB 文件中。然后,这个.LIB 文件可以像静态.LIB 一样与 DLL 链接使用。
在 32 位编译器版本中,你可以使用 __declspec(dllexport)
关键字从 DLL 中导出数据、函数、类或类成员函数。__declspec(dllexport)
将导出指令添加到对象文件中,因此你不需要使用一个.DEF 文件。
这种便利性在试图导出装饰的 C++ 函数名时最为明显。没有标准的名称装饰规范,所以导出的函数名可能会在不同的编译器版本之间发生变化。如果你使用 __declspec(dllexport)
,重新编译 DLL 和依赖的 .EXE 文件是必要的,只是为了考虑到任何命名约定的变化。
许多导出指令,例如 ordinals、NONAME 和 PRIVATE,只能在 .DEF 文件中进行,没有 .DEF 文件就无法指定这些属性。然而,除了使用 .DEF 文件之外,使用 __declspec(dllexport)
不会导致构建错误。要导出函数,__declspec(dllexport)
关键字必须出现在调用约定关键字的左边,如果有指定关键字的话。例如:
__declspec(dllexport)void __cdecl FunctionName(void);
要导出一个类中所有的公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示。
class __declspec(dllexport)CExampleExport : public CObject
{ ... class definition ... };
当构建你的 DLL 时,你通常会创建一个头文件,其中包含你要导出的函数原型和 / 或类,并在头文件的声明中添加 __declspec(dllexport)
。为了使你的代码更易读,为 __declspec(dllexport)
定义一个宏,并对你要导出的每个符号使用这个宏。例如:
#define DllExport __declspec(dllexport)
__declspec(dllexport)
在 DLL 的导出表中存储函数名。当把 DLL 源代码从 Win16 移植到 Win32 时,用 __declspec(dllexport)
替换 __export
的每个实例。作为参考,在 Win32 WINBASE.H 头文件中搜索 __declspec(dllimport)
的使用实例。
导出 C 函数在 C 或 C++ 语言可执行文件中的用途
如果你在用 C 语言编写的 DLL 中有一些函数,而你想从 C 语言或 C++ 语言的模块中访问这些函数,你应该使用 __cplusplus
预处理程序宏来确定正在编译哪种语言,如果是从 C++ 语言的模块中使用这些函数,则用 C 链接来声明这些函数。如果你使用这种技术,并为你的 DLL 提供头文件,这些函数可以被 C 和 C++ 用户使用,而不会有任何改变。
下面的代码显示了一个可以被 C 和 C++ 客户端应用程序使用的头文件。
// MyCFuncs.h
#ifdef __cplusplus
extern "C" { //only need to export C interface if used by C++ source code
#endif
__declspec(dllimport)void MyCFunc();
__declspec(dllimport)void AnotherCFunc();
#ifdef __cplusplus
}
#endif
如果你需要将 C 函数链接到你的 C++ 可执行文件中,而函数声明头文件没有使用上述技术,在 C++ 源文件中,做如下操作,以防止编译器装饰 C 函数名。
extern "C" {
#include "MyCHeader.h"
}
创建一个导入库
一个导入库(.lib)文件包含了链接器所需的信息,以解析对导出的 DLL 函数的外部引用,因此系统可以在运行时定位指定的 DLL 和导出的 DLL 函数。例如,要调用 CreateWindow()函数,你必须将你的代码与导入库 User32.lib 链接。原因是 CreateWindow()驻留在一个名为 User32.dll 的系统 DLL 中,而 User32.lib 是用于解析你的代码中对 User32.lib 中导出函数的调用的导入库。链接器会创建一个包含每个函数调用地址的表。当加载 DLL 时,对 DLL 中函数的调用将被固定起来。当系统在初始化进程时,它会加载 User32.dll,因为进程依赖于该 DLL 中的导出函数,它更新函数地址表中的条目。所有对 CreateWindow()的调用都会调用 User32.dll 中导出的函数。警告。 在 DLL 中调用 ExitProcess()函数可能导致意外的应用程序或系统错误。只有当您知道哪些应用程序或系统组件将加载 DLL,并且在此上下文中调用 ExitProcess()是安全的,才能确保从 DLL 中调用 ExitProcess()。
动态链接库更新
有时需要用较新的版本替换 DLL。在替换 DLL 之前,请执行版本检查,以确保您是在用较新的版本替换旧版本。可以替换正在使用的 DLL。替换正在使用的 DLL 的方法取决于您使用的操作系统。在 Windows XP 和更高版本上,应用程序应使用隔离应用程序和并排装配体。如果执行以下步骤,则无需重新启动计算机。
- 使用 MoveFileEx()函数重命名被替换的 DLL。不要指定 MOVEFILE_ALLOWED,并确保重命名的文件在包含原始文件的同一卷上。你也可以简单地重命名同一目录下的文件,给它一个不同的扩展名。
- 将新的 DLL 复制到包含重命名 DLL 的目录中。现在所有的应用程序都将使用新的 DLL。
- 使用带有 MOVEFILE_DELAY_UNTIL_REBOOT 的 MoveFileEx()来删除重命名的 DLL。
在你进行这个替换之前,应用程序将使用原来的 DLL,直到它被卸载。在你进行替换之后,应用程序将使用新的 DLL。当你编写一个 DLL 时,你必须注意确保它已经为这种情况做好了准备,特别是当 DLL 维护全局状态信息或与其他服务通信时。如果 DLL 没有为全局状态信息或通信协议的更改做好准备,更新 DLL 将需要您重新启动计算机,以确保所有应用程序都使用相同版本的 DLL。对于 Windows Me/98/95:因为不支持 MoveFileEx(),所以需要重新启动计算机。
动态链接库重定向
当应用程序加载的 DLL 版本与出厂时的版本不同时,可能会出现问题。从 Windows 2000 开始,您可以通过创建一个重定向文件来确保您的应用程序使用 DLL 的正确版本。重定向文件的内容会被忽略,但它的存在会强制应用程序目录中的所有 DLL 从该目录加载。
重定向文件必须命名为:appname.local。
例如,如果应用程序的名称是 editor.exe,重定向文件就命名为 editor.exe.local。你必须将 itor.exe.local 安装在包含 editor.exe 的同一目录中。你也必须在同一目录下安装 DLLs。如果存在重定向文件,LoadLibrary()和 LoadLibraryEx()函数会改变其搜索顺序。如果指定了路径,并且存在应用程序的重定向文件,这些函数就会在应用程序的目录中搜索 DLL。如果 DLL 存在于应用程序的目录中,这些函数忽略指定的路径,从应用程序的目录中加载 DLL。如果模块不在应用程序的目录中,这些函数从指定的目录中加载 DLL。例如,一个应用程序 c:\myapp\myapp.exe 使用以下路径调用 LoadLibrary()。
c:program files/common files/system/mydll.dll
如果 c:\myapp\myapp.exe.local 和 c:\myapp\mydll.dll 存在,LoadLibrary()将加载 c:\myapp\mydll.dll。否则,LoadLibrary()将加载 c:program files\common files\system\mydll.dll。在包含应用程序的同一目录下安装应用程序的 DLLs 是一个很好的做法,即使你不使用重定向。它可以确保安装你的应用程序不会覆盖 DLL 的其他副本并导致其他应用程序失败。此外,其他应用程序也不会覆盖您的 DLL 副本而导致应用程序失败。
动态链接库数据
动态链接库(DLL)可以包含全局数据或本地数据。
变量范围
DLL 变量的默认范围与应用程序中声明的变量的范围相同。DLL 源代码文件中的全局变量对使用 DLL 的每个进程都是全局的。静态变量的作用域限于它们被声明的块。因此,默认情况下,每个进程都有自己的 DLL 全局变量和静态变量实例。您的开发工具可能允许您覆盖全局变量和静态变量的默认范围。
动态内存分配
当 DLL 使用任何一个内存分配函数(GlobalAlloc()、LocalAlloc()、HeapAlloc()和 VirtualAlloc())分配内存时,内存是在调用进程的虚拟地址空间中分配的,并且只有该进程的线程可以访问。DLL 可以使用文件映射来分配可以在进程间共享的内存。
线程本地存储
线程本地存储(TLS)函数使 DLL 能够为多线程进程的每个线程分配一个索引,用于存储和检索不同的值。例如,一个电子表格应用程序可以在用户每次打开一个新的电子表格时创建一个新的同一线程实例。为各种电子表格操作提供函数的 DLL 可以使用 TLS 来保存每个电子表格的当前状态信息(行、列等)。
警告。 Visual C++ 编译器支持一种语法,可以让你声明线程本地变量。__declspec(thread)
。如果你在一个 DLL 中使用这个语法,你将不能使用 LoadLibrary()或 LoadLibraryEx()显式加载 DLL。如果你的 DLL 将被显式加载,你必须使用线程本地存储函数来代替 __declspec(thread)
。
Windows 动态链接库 - DLL 第三部分:方案实例
Sunbreak
2021-02-23
这篇文章主要介绍了 Windows 动态链接库(DLL)的相关内容,包括创建简单的 DLL 程序、使用加载时动态链接和运行时动态链接,还列举了相关代码示例及可能出现的问题,最后提供了进一步阅读和挖掘的参考资料。
关联问题:DLL 方案有哪些 DLL 实例怎样 DLL 如何应用
原文地址:www.tenouk/ModuleCC1.h…
原文作者:www.tenouk/
发布时间:约 2004 年前后
本模块有哪些内容?
- 创建一个简单的动态链接库程序
- 使用加载时动态链接
- 使用运行时动态链接
我的训练时间: aa 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
Win32 编程技能。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的动态链接库。
- 能够创建 DLL 的导出和导入函数。
创建一个简单的动态链接库程序
下面快照演示如何创建和使用 DLL。首先我们来看看如何创建 DLL 项目的步骤。
选择 Win32 控制台项目,把项目名称。
选择 DLL 单选按钮并勾选 “空项目”。
然后照常添加 C++ 源文件。
复制并粘贴下面的源代码。构建即可,不要运行。稍后,我们要创建一个应用程序,在我们的 DLL 程序中使用 mydll()函数。接下来我们就可以构建我们的 DLL 程序了。在这个例子中,我们构建的是 Release 版本。
示例相关说明
下面的例子 mysrcdll.cpp
是创建一个简单的 DLL
程序 mydllpro.dll
所需要的源代码。
文件 mysrcdll.cpp
包含一个简单的字符串打印函数,叫做 mydll()
。
mydllpro.dll
并没有定义一个入口点函数,因为它是与 C
运行时库链接在一起的,没有自己的初始化或清理功能要执行。
// Project name: mydllpro, File name: mysrcdll.cpp, generating mydllpro.dll, mydllpro.lib...
// The mydll function writes a null-terminated string to
//the standard output device...
// The export mechanism used here is the __declspec(export)
//method supported by Microsoft Visual Studio, but any
//other export method supported by your development
//environment may be substituted.
// For WinXp, don't forget to add
// Advapi32.lib library if needed...
#define _WIN32_WINNT 0x0501
#include <windows.h>
#include <stdio.h>
#define EOF(-1)
#ifdef __cplusplus // If used by C++ code,
extern "C" { //we need to export the C interface
#endif
__declspec(dllexport)int mydll(LPTSTR lpszMsg)
{
DWORD cchWritten;
HANDLE hStdout;
BOOL fRet;
printf("-This is mydll.dll file lol!-\n");
// Get a handle to the standard output device.
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if(INVALID_HANDLE_VALUE == hStdout)
{
//failed to get the handle, give some message, get error code, just exit...
printf("GetStdHandle(), invalid handle, error: GetLastError().\n");
return EOF;
}
else
printf("GetStdHandle(), standard handle is OK.\n");
// Write a null-terminated string to the standard output device.
while(*lpszMsg != '\0')
{
fRet = WriteFile(hStdout, lpszMsg, 1, &cchWritten, NULL);
if((fRet == FALSE)||(cchWritten != 1))
// If something wrong just exit or provide meaningful message/error code...
return EOF;
//else, write more...
lpszMsg++;
}
printf("\n");
return 1;
}
#ifdef __cplusplus
}
#endif
设置发布版本。
或者使用配置管理器…
好了,最后让建立程序。然后,验证一下准备使用的 DLL 文件(mydllpro.dll)的创建。注意,mydllpro.lib 文件将用于隐式链接(加载)。
使用加载时动态链接
创建了一个 DLL 之后,就可以在应用程序中使用它了。下面的文件 mydllloadtime.cpp 是一个空的 Win32 控制台应用程序的源代码,它使用从 mydll.dll 导出的 mydll()函数。请注意,我们之前创建的 DLL 程序中没有任何用户定义的头文件。
// File: mydllloadtime.cpp
// A simple program that uses mydll()from mydllpro.dll.
// For WinXp, don't forget to add
#define _WIN32_WINNT 0x0501
#include <windows.h>
//call to a function in the mydllpro.dll
__declspec(dllimport)int mydll(LPTSTR);
// Another form: int mydll(LPTSTR);
int main()
{
int Ret = 1;
Ret = mydll("This message was printed using the DLL function");
return Ret;
}
因为 mydllloadtime.cpp 隐式调用 DLL 函数,所以应用程序的模块必须与导入的 librarymydllpro.lib 链接。在本例中,mydllpro.lib 和 mydllpro.dll 被复制到项目目录下。
不幸的是,尽管已经遵循了所有必要的步骤,但当调用 mydll()时,这个程序失败了。错误如下所示。
...
...
Loaded kernel32.lib(KERNEL32.dll)
Found __NULL_IMPORT_DESCRIPTOR
Referenced in kernel32.lib(KERNEL32.dll)
Loaded kernel32.lib(KERNEL32.dll)
Found KERNEL32_NULL_THUNK_DATA
Referenced in kernel32.lib(KERNEL32.dll)
...
...
testdll.obj : error LNK2019: unresolved external symbol "int __cdecl mydll(char *)"(?mydll@@YAHPAD@Z)referenced in function _main
Release/mydlltest.exe : fatal error LNK1120: 1 unresolved externals
从构建输出来看,库文件已经被搜索,但没有被加载 / 处理。Tenouk 没有在 Visual C++ 编译器上尝试这个例子。让我们尝试使用同样的代码来实现 Run-Time Dynamic Linking。
使用 Run-Time Dynamic Linking
你可以在加载时和运行时动态链接中使用同一个 DLL。下面的源代码产生了与上一节中加载时示例相同的输出。程序使用 LoadLibrary()函数来获取 mydll.dll 的句柄。如果 LoadLibrary()成功,程序在 GetProcAddress()函数中使用返回的句柄来获取 DLL 的 mydll()函数的地址。
调用 DLL 函数后,程序调用 FreeLibrary()函数来卸载 DLL。下面的例子说明了运行时和加载时动态链接之间的重要区别。如果 themydll.dll 文件不可用,使用加载时动态链接的应用程序就会简单地终止。然而,运行时动态链接的例子可以对错误做出响应。
这个项目是一个空的控制台模式的应用程序。将 mydllpro.dll 复制到项目目录或系统目录或前面提到的任何其他目录序列中。在本例中,mydllpro.dll 已被复制到项目目录下。
// File: testmydllruntime.cpp
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access mydll()in mydllpro.dll.
// For WinXp, don't forget to add
#define _WIN32_WINNT 0x0501
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module created in the previous example.
hinstLib = LoadLibrary("mydllpro");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The dll handle is valid...\n");
ProzAdd =(MYPROC)GetProcAddress(hinstLib, "mydll");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Pass some text, mydll()will display it on the standard output...
(ProcAdd)("\nThis message is via DLL function...\n");
}
else
printf("\nThe function address is NOT valid, error: % d.\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK, error: % d.\n", GetLastError());
}
else
printf("The dll handle is not valid, error: % d.\n", GetLastError());
// If unable to call the DLL function, use an alternative.
if(!fRunTimeLinkSuccess)
printf("This message via alternative method...\n");
return 0;
}
一个示例输出。
好了,它工作了!该程序使用运行时动态链接;在创建该程序时,你不应该像我们的 Load-Time 动态链接示例那样与导入库链接。让我们继续看更多的故事。
Windows 动态链接库 - DLL 第四部分:方案实例
Sunbreak
2021-02-23
文章主要介绍了动态链接库(DLL)的相关内容,包括在 DLL 中使用共享内存、测试 MainDll()、函数调用公约、使用线程本地存储技术、测试 DLL 程序、DLL 参考及过时功能等,还提供了相关的代码示例和函数说明。
关联问题:DLL 方案怎样 DLL 实例有哪些 DLL 作用是什么
原文地址:www.tenouk/ModuleCC2.h…
原文作者:www.tenouk/
发布时间:约 2004 年前后
在这个模块中我们有什么?
- 在动态链接库中使用共享内存
- 测试我们的 MainDll()
- 函数调用公约
- 在动态链接库中使用线程本地存储技术
- 测试 DLL 程序
- 动态链接库参考
- 职能
- 过时的功能
我的训练时间: xyz 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
Win32 的编程技巧。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的动态链接库。
- 能够创建 DLL 的导出和导入函数。
动态链接库中共享内存的使用。
本节介绍了 DLL 入口点函数如何使用文件映射对象来设置内存,使加载 DLL 的进程可以共享内存。共享的 DLL 内存只在 DLL 被加载时才会持续存在。本例使用文件映射将一个命名的共享内存块映射到每个加载 DLL 的进程的虚拟地址空间中。要做到这一点,入口点函数必须。
- 调用 CreateFileMapping()函数获得一个文件映射对象的句柄。第一个加载 DLL 的进程创建文件映射对象。随后的进程打开现有对象的句柄。
- 调用 MapViewOfFile()函数将一个视图映射到虚拟地址空间。这样,进程就可以访问共享内存。
请注意,虽然您可以通过为 CreateFileMapping()的 lpAttributes 参数传递一个 NULL 值来指定默认的安全属性,但您可以选择使用 SECURITY_ATTRIBUTES 结构来提供额外的安全性。
这是一个空的 DLL 工程(程序),你需要将你的工程设置为使用__stdcall(使用 WINAPI)约定的 dllmain()。以下是 Visual C++ .Net 的设置。
// Project name: moredll, File name: dllentryfunc.cpp generating moredll.dll
//but no moredll.lib! The DLL entry-point function sets up shared memory using
//a named file-mapping object.
#include <windows.h>
#include <stdio.h>
#include <memory.h>
#define SHMEMSIZE 4096
static LPVOID lpvMem = NULL; //pointer to shared memory
static HANDLE hMapObject = NULL; //handle to file mapping
BOOL DllMain(HINSTANCE hinstDLL, // DLL module handle
DWORD fdwReason, //reason called
LPVOID lpvReserved) //reserved
{
BOOL fInit, fIgnore;
switch(fdwReason)
{
// The DLL is loading due to process
//initialization or a call to LoadLibrary.
case DLL_PROCESS_ATTACH:
printf("The DLL is loading...from moredll.dll.\n");
// Create a named file mapping object.
hMapObject = CreateFileMapping(
INVALID_HANDLE_VALUE, //use paging file
NULL, //default security attributes
PAGE_READWRITE, //read/write access
0, //size: high 32-bits
SHMEMSIZE, //size: low 32-bits
"dllmemfilemap"); //name of map object
if(hMapObject == NULL)
return FALSE;
else
printf("CreateFileMapping()is OK.\n");
// The first process to attach initializes memory.
fInit =(GetLastError()!= ERROR_ALREADY_EXISTS);
// Get a pointer to the file-mapped shared memory.
lpvMem = MapViewOfFile(
hMapObject, //object to map view of
FILE_MAP_WRITE, //read/write access
0, //high offset: map from
0, //low offset: beginning
0); //default: map entire file
if(lpvMem == NULL)
return FALSE;
else
printf("MapViewOfFile()is OK.\n");
// Initialize memory if this is the first process.
if(fInit)
memset(lpvMem, '\0', SHMEMSIZE);
break;
// The attached process creates a new thread.
case DLL_THREAD_ATTACH:
printf("The attached process creates a new thread...from moredll.dll.\n");
break;
// The thread of the attached process terminates.
case DLL_THREAD_DETACH:
printf("The thread of the attached process terminates... from moredll.dll.\n");
break;
// The DLL is unloading from a process due to
//process termination or a call to FreeLibrary().
case DLL_PROCESS_DETACH:
printf("The DLL is unloading from a process... from moredll.dll.\n");
// Unmap shared memory from the process's address space.
fIgnore = UnmapViewOfFile(lpvMem);
// Close the process's handle to the file-mapping object.
fIgnore = CloseHandle(hMapObject);
break;
default:
printf("Reason called not matched, error if any: % d... from moredll.dll.\n", GetLastError());
break;
}
return TRUE;
UNREFERENCED_PARAMETER(hinstDLL);
UNREFERENCED_PARAMETER(lpvReserved);
}
// Can be commented out for this example...
// SetSharedMem()sets the contents of shared memory.
VOID SetSharedMem(LPTSTR lpszBuf)
{
LPTSTR lpszTmp = "Testing some string";
// Get the address of the shared memory block.
lpszTmp =(LPTSTR)lpvMem;
// Copy the null-terminated string into shared memory.
while(*lpszBuf)
*lpszTmp++ = *lpszBuf++;
*lpszTmp = '\0';
printf("The content: % s.\n", lpszTmp);
}
// Can be commented out for this example...
// GetSharedMem()gets the contents of shared memory.
VOID GetSharedMem(LPTSTR lpszBuf, DWORD cchSize)
{
LPTSTR lpszTmp;
// Get the address of the shared memory block.
lpszTmp =(LPTSTR)lpvMem;
// Copy from shared memory into the caller's buffer.
while(*lpszTmp && --cchSize)
*lpszBuf++ = *lpszTmp++;
*lpszBuf = '\0';
printf("The caller buffer: % s.\n", lpszBuf);
}
一个示例输出。
将你的项目改为 Release 版本,并重建 DLL 程序。 如果没有错误,将 DLL 文件(本例中为 moredll.dll)复制到 Windows 系统目录。我们将在下一节测试这个 DLL 文件。
测试我们的 MainDll()
让我们通过执行下面的简单程序来测试生成的 moredll.dll。 这里没有函数调用(导出)。moredll.dll 已经被复制到 C:\WINDOWS/System32 目录下(Windows Xp Pro)。
// File: testdll.cpp, using moredll.dll that uses Dllmain()
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access Dllmain()of moredll.dll.
// No function to be exported, just testing...
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module, moredll.dll...this module has been copied
//to C:\WINDOWS\System32 directory...
hinstLib = LoadLibrary("moredll");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The DLL handle is valid...\n");
ProcAdd =(MYPROC)GetProcAddress(hinstLib, "Anonymfunction");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Ready to execute DLLmain()...
}
else
printf("The function address is not valid, error: % d.\n", GetLastError());
}
else
printf("\nThe DLL handle is NOT valid, error: % d\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK.\n");
return 0;
}
一个示例输出。
错误。127 - 找不到指定的过程。(ERROR_PROC_NOT_FOUND)应该是预期的,因为我们没有在 DLL 程序中定义任何函数。注意,共享内存可以在每个进程中映射到不同的地址。因此,每个进程都有自己的 lpvMem 参数实例,它被声明为一个全局变量,因此它对所有 DLL 函数都可用。本例假设 DLL 全局数据是不共享的,所以每个加载 DLL 的进程都有自己的 lpvMem 实例。
在这个例子中,当最后一个文件映射对象的句柄关闭时,共享内存就会被释放。为了创建持久的共享内存,DLL 可以在第一次加载 DLL 时创建一个分离的进程。如果这个分离的进程使用 DLL 并且没有终止,它就有一个文件映射对象的句柄,防止共享内存被释放。
函数调用公约
每一次函数调用都会有一个栈帧的创建。 如果我们能研究一下函数调用的操作,以及函数的堆栈框架是如何构造和销毁的,是非常有用的。 对于函数的调用,编译器有一些用于调用的约定。 惯例是指一种标准化的,但不是文档化的标准的做事方式。例如,C/C++ 的函数调用惯例告诉编译器这样的事情。
- 函数参数被推到堆栈上的顺序。
- 不管是调用函数还是被调用函数(callee)的责任,在调用结束后将参数从栈中删除,这就是栈清理过程。
- 编译器用来标识各个函数的命名约定。
调用约定的例子有 __stdcall
,__pascal
,__cdecl
和 __fastcall
(对于 Microsoft Visual C++)。 调用约定属于一个函数的签名,因此具有不同调用约定的函数之间是不兼容的。 目前 C/C++ 编译器厂商之间,甚至一个编译器的不同版本之间,对于函数调用方案的命名都没有标准。这就是为什么如果你链接了其他编译器编译的对象文件,可能会产生不一样的命名方案,从而导致外挂无法解决。 对于 Borland 和 Microsoft 编译器,你可以在返回类型和函数名称之间明确指定一个特定的调用约定,如下所示。
void __cdecl TestFunc(float a, char b, char c); // Borland and Microsoft
或者如前面的例子所示,你可以通过 Visual C++/.Net 的设置来实现。对于 GNU GCC 来说,你可以使用__attribute__关键字,在写函数定义时,后面加上关键字__attribute__,然后用双括号说明调用约定,如下图所示。
或者如前面的例子所示,你可以通过 Visual C++/.Net 的设置来实现。对于 GNU GCC 来说,你可以使用__attribute__关键字,在写函数定义的时候,后面跟着关键字__attribute__,然后用双括号说明调用约定,如下所示。
void TestFunc(float a, char b, char c)__attribute__((cdecl)); // GNU GCC
以微软 Visual C++ 编译器为例,其使用的函数调用约定有三种,如下表所示。
关键字 | 堆栈清理 ~~~~~~~~ | 参数传递 |
---|---|---|
__cdecl | 调用者 | 按相反的顺序(从右到左)推送堆栈中的参数。 调用者清理堆栈。 这是支持变量函数(参数数量可变或类型列表,如 printf())的 C 语言以及 C++ 程序的默认调用惯例。 cdecl 调用约定比 __stdcall 创建更大的可执行文件,因为它要求每个函数调用都包含堆栈清理代码。 |
__stdcall | 被调用者 | 也称为 __pascal 。 在堆栈中以相反的顺序(从右到左)推送参数。 使用这种调用惯例的函数需要一个函数原型。 Callee 清理堆栈。 这是 Win32 API 函数中使用的标准约定。 |
__fastcall | 被调用者 | 参数存储在寄存器中,然后推到堆栈上。 __fastcall 调用惯例规定,函数的参数尽可能在寄存器中传递。 Callee 清理堆栈。 |
基本上,C 函数调用时,调用者会将一些参数推送到堆栈上,调用函数,然后弹出堆栈,清理这些推送的参数。 下面以汇编语言中的 __cdecl
为例进行说明。
/*example of __cdecl*/
push arg1
push arg2
call function
add ebp, 12 ;stack cleanup
而对于 __stdcall
的例子。
/*example of __stdcall*/
push arg1
push arg2
call function
/* No stack cleanup, it will be done by caller */
在动态链接库中使用线程本地存储技术
本节介绍了使用 DLL 入口点函数来设置线程本地存储(TLS)索引,为多线程进程的每个线程提供私有存储。入口点函数使用 TlsAlloc()
函数在进程加载 DLL 时分配一个 TLS 索引。每个线程都可以使用这个索引来存储指向自己内存块的指针。当用 DLL_PROCESS_ATTACH
值调用入口点函数时,代码会执行以下操作。
- 使用
TlsAlloc()
函数分配一个 TLS 索引。 - 分配一个内存块给进程的初始线程使用。
- 在调用
TlsSetValue()
函数时使用 TLS 索引来存储分配到内存的指针。
每当进程创建一个新的线程时,都会用 DLL_THREAD_ATTACH 值调用入口点函数。然后,entry-point 函数为新线程分配一个内存块,并通过使用 TLS 索引来存储一个指向它的指针。每个线程都可以在调用 TlsGetValue()
时使用 TLS 索引来检索自己内存块的指针。
当一个线程终止时,用 DLL_THREAD_DETACH 值调用入口点函数,该线程的内存被释放。当一个进程终止时,使用 DLL_PROCESS_DETACH 值调用入口点函数,TLS 索引中指针引用的内存被释放。
TLS 索引被存储在一个全局变量中,使得所有的 DLL 函数都可以使用它。下面的例子假定 DLL 的全局数据是不共享的,因为 TLS 索引对于每个加载 DLL 的进程来说不一定是相同的。这是一个空的 DLL 工程(程序)。
// Project name: moredll, File name: dllntls.cpp, generating moredll.dll
// Using Thread Local Storage in a Dynamic Link Library
#include <windows.h>
#include <stdio.h>
static DWORD dwTlsIndex; //address of shared memory
// DllMain()is the entry-point function for this DLL.
BOOL DllMain(HINSTANCE hinstDLL, // DLL module handle
DWORD fdwReason, //reason called
LPVOID lpvReserved) //reserved
{
LPVOID lpvData;
BOOL fIgnore;
switch(fdwReason)
{
// The DLL is loading due to process
//initialization or a call to LoadLibrary.
case DLL_PROCESS_ATTACH:
printf("Loading the DLL...\n");
// Allocate a TLS index.
if((dwTlsIndex = TlsAlloc())== 0xFFFFFFFF)
return FALSE;
// No break: Initialize the index for first thread.
// The attached process creates a new thread.
case DLL_THREAD_ATTACH:
printf("The attached process creating a new thread...\n");
// Initialize the TLS index for this thread.
lpvData =(LPVOID)LocalAlloc(LPTR, 256);
if(lpvData != NULL)
fIgnore = TlsSetValue(dwTlsIndex, lpvData);
break;
// The thread of the attached process terminates.
case DLL_THREAD_DETACH:
printf("The thread of the attached process terminates...\n");
// Release the allocated memory for this thread.
lpvData = TlsGetValue(dwTlsIndex);
if(lpvData != NULL)
LocalFree((HLOCAL)lpvData);
break;
// DLL unload due to process termination or FreeLibrary.
case DLL_PROCESS_DETACH:
printf("DLL unloading...\n");
// Release the allocated memory for this thread.
lpvData = TlsGetValue(dwTlsIndex);
if(lpvData != NULL)
LocalFree((HLOCAL)lpvData);
// Release the TLS index.
TlsFree(dwTlsIndex);
break;
default:
printf("Reason called not matched, error if any: % d...\n", GetLastError());
break;
}
return TRUE;
UNREFERENCED_PARAMETER(hinstDLL);
UNREFERENCED_PARAMETER(lpvReserved);
}
当一个进程使用该 DLL 的加载时链接时,切入点函数足以管理线程的本地存储。使用运行时链接的进程可能会出现问题,因为在调用 LoadLibrary()
函数之前存在的线程没有调用 entry-point 函数,所以没有为这些线程分配 TLS 内存。下面的例子解决了这个问题,检查 TlsGetValue()
函数返回的值,如果该值表明该线程的 TLS 槽没有设置,则分配内存。
LPVOID lpvData;
// Retrieve a data pointer for the current thread.
lpvData = TlsGetValue(dwTlsIndex);
// If NULL, allocate memory for this thread.
if(lpvData == NULL)
{
lpvData =(LPVOID)LocalAlloc(LPTR, 256);
if(lpvData != NULL)
TlsSetValue(dwTlsIndex, lpvData);
}
将你的项目改为 Release 模式,然后重建 DLL 程序。 如果没有错误,将 DLL 文件(本例中为 moredll.dll)复制到 Windows 系统目录下。我们将在下一节测试该 DLL 文件。
测试 DLL 程序
以前面的测试程序为例,让我们测试一下 DLL 程序。
// File: testdll.cpp, using moredll.dll that uses Dllmain()
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access Dllmain()of moredll.dll.
// No function to be exported/imported, just testing...
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module, moredll.dll...this module has been copied
//to C:\WINDOWS\System32 directory...
hinstLib = LoadLibrary("moredll");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The DLL handle is valid...\n");
ProcAdd =(MYPROC)GetProcAddress(hinstLib, "Anonymfunction");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Ready to execute DllMain()...
}
else
printf("The function address is not valid, error: % d.\n", GetLastError());
}
else
printf("\nThe DLL handle is NOT valid, error: % d\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK.\n");
return 0;
}
一个输出示例。
好吧,我们的 DLL 程序看起来不错。这一切的人。享受你的 C / C++ 之旅吧
动态链接库参考
以下元素用于动态链接。
功能 | 说明 |
---|---|
DisableThreadLibraryCalls() | 禁用指定 DLL 的线程附加和线程分离通知。 |
DllMain() | 一个进入 DLL 的可选入口点。 |
FreeLibrary() | 减少加载的 DLL 的引用次数。当引用数达到零时,模块将从调用进程的地址空间中解映射。 |
FreeLibraryAndExitThread() | 将加载的 DLL 的引用数减一,然后调用 ExitThread()来终止调用线程。 |
GetDllDirectory() | 检索用于查找应用程序的 DLLs 的搜索路径的应用程序特定部分。 |
GetModuleFileName() | 检索包含指定模块的文件的完全限定路径。 |
GetModuleFileNameEx() | 检索包含指定模块的文件的完全限定路径。 |
GetModuleHandle() | 读取指定模块的模块句柄。 |
GetModuleHandleEx() | 读取指定模块的模块句柄。 |
GetProcAddress() | 从指定的 DLL 中检索导出的函数或变量的地址。 |
LoadLibrary() | 将指定的可执行模块映射到调用进程的地址空间。 |
LoadLibraryEx() | 将指定的可执行模块映射到调用进程的地址空间。 |
SetDllDirectory() | 修改用于查找应用程序的 DLL 的搜索路径。 |
过时的功能
LoadModule()
函数只是为了兼容 16 位版本的 Windows 而提供的。
进一步的阅读和挖掘
- Microsoft Visual C++,在线 MSDN。
- 结构、枚举、联合和 typedef 故事可以参考 C/C++ 结构、枚举、联合和 typedef。
- 多字节、Unicode 字符和本地化请参考 Locale、宽字符和 Unicode(Story)和 Windows 用户与组编程教程(Implementation)。
- Windows 数据类型是 Windows 数据类型。
- 查看 Amazon 上最畅销的 C / C++ 和 Windows 书籍。
链接库(动态链接库,静态链接库)
红渐
2022-03-20
阅读并整理了一些网上的资料,做个记录
库最基本的概述:是共享代码的方法
链接库是什么
库: 专门存储可重复使用的代码块的文件
但是一般不会直接分享源代码,而是分享库的二进制版本 ——链接库
一个目标文件中使用的函数或变量,可能定义在其他的目标文件中,也可能定义在某个链接库文件中。链接器完成完成链接工作的方式有两种,分别是:
- 无论缺失的地址位于其它目标文件还是链接库,链接库都会逐个找到各目标文件中缺失的地址。采用此链接方式生成的可执行文件,可以独立载入内存运行;
- 链接器先从所有目标文件中找到部分缺失的地址,然后将所有目标文件组织成一个可执行文件。如此生成的可执行文件,仍缺失部分函数和变量的地址,待文件执行时,需连同所有的链接库文件一起载入内存,再由链接器完成剩余的地址修复工作,才能正常执行。
第一种链接方式为静态链接;第二种,链接所有目标文件也为静态链接,但是在内存中链接属于动态链接
静态链接库
有很多缺点:
-
文件体积很大
-
如果存在多个可执行文件,都链接同一个静态库,那么每个可执行文件都要全量链接该静态库,造成资源浪费
-
一旦程序中有模块更新,那么都要重新链接才能运行
动态链接库
在 Linux 上习惯称为共享库、共享对象文件
动态链接:将链接的时机推迟到程序运行时再进行
1.动态链接库和可执行文件是分别载入内存的
2.多个程序用同一个动态链接库时,所有程序可共享一份动态链接库,避免空间浪费
- 方便了程序的更新,如果程序有一个模块更新,那么只需要替换旧的模块,然后动态链接在一起
但是,每次程序运行都需重新链接,比静态链接库相比损失了大概 5% 的性能
后缀:linux 中为 .so,windows 中为 .dll
gcc -shared -fpic -o libvector.so addvec.c mulvec.c
gcc -o prog2 main.c ./libvector.so// 通过共享库构建可执行文件
-shared: 指定创建共享库
-fpic: 共享库可以加载到内存任意位置
libvector.so
的代码没有真的被复制到 prog2 中,而静态库就会。
执行过程中,加载器会发现 prog2 中存在一个 名为 .interp
的 section,其包含了动态链接器的路径名
实际上,动态链接器也是一个 so 文件,ld-linux.so。被加载器加载到内存中运行,然后动态链接器执行重定位代码和数据的工作
具体的执行过程:
-
重定位 libc.so(C 语言函数库) 到一个位置
-
重定位 libvector.so到另一个位置
-
重定位 prog2 中的那些 定义于 libc.so libvector.so 的引用符号
如何使用(动态)共享库
-dlopen:可以打开一个共享库文件,RTLD_LAZY 代表,当共享库中函数执行时再进行符号解析
-dlsym:handle 是共享库文件的句柄,symbol 是符号名。返回符号的地址
-dlclose:卸载共享库
via:
-
Windows 动态链接库 - DLL 第一部分:方案实例 - 掘金
https://juejin/post/6932276742551765005 -
Windows 动态链接库 - DLL 第二部分:方案实例 - 掘金
https://juejin/post/6932333618983337992 -
Windows 动态链接库 - DLL 第三部分:方案实例 - 掘金
https://juejin/post/6932337818014482439 -
Windows 动态链接库 - DLL 第四部分:方案实例 - 掘金
https://juejin/post/6932342156229279757 -
链接库(动态链接库,静态链接库)库最基本的概述:是共享代码的方法 链接库是什么 库 - 掘金 2022-03-20
https://juejin/post/7077090970159808543
注:本文为掘金 Sunbreak 博主“ Windows DLL 系列 ” DeepL/Translator 的翻译合集。
未校。
Windows 动态链接库 - DLL 第一部分:故事
Sunbreak
2021-02-23
这篇文章主要介绍了动态链接库(DLL)的相关内容,包括 DLL 与 C 语言运行时的故事、与应用程序的区别、使用优势、类型、链接方式(隐式和显式)、创建仅有资源的 DLL、导入和导出方法、初始化方式以及运行时的库行为等,并提供了相应的示例代码和说明。
原文地址:https://www.tenouk/ModuleBB.html
原文作者:<www.tenouk/>
发布时间:约 2004 年前后
在这个模块中我们有什么?
- 动态链接库和 C 语言运行时故事
- 应用程序和 DLL 的区别
- 使用 DLL 的优势
- DLL 的类型
- 将一个可执行文件链接到一个 DLL
- 隐性使用链接
- 明确使用链接
- 确定使用哪种链接方法
- 隐性链接
- 明确链接
- 创建一个仅有资源的 DLL
- 进口和出口
- 使用.DEF 文件
- 使用
__declspec
- 使用
__declspec(dllimport)
导入应用程序 - 从 DLL 中导出
- 使用
__declspec(dllexport)
从 DLL 中导出。 - 初始化一个 DLL
- 初始化扩展 DLL(对于 MFC 程序)
- 初始化非 MFC DLLs
- 运行时的库行为
我的训练时间:xx 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
应该获得的能力。
- 能够理解、构建和运行动态链接库程序。
- 能够体会到使用 DLL 与普通应用相比的好处。
- 能够从 MSDN 文档中收集信息,以了解和使用 DLL。
动态链接库和 C 语言运行时的故事
注:本模块是一个通用的 MSDN 文档,涉及 Visual C++ 上的 C/C++ 运行时库和 MFC。让我们先来了解一下全貌吧!
动态链接库(DLL)是一个可执行文件,作为一个共享的函数库。动态链接为一个进程提供了一种方法来调用一个不属于其可执行代码的函数。函数的可执行代码位于 DLL 中,它包含一个或多个函数,这些函数被编译、链接,并与使用它们的进程分开存储。DLL 还有利于数据和资源的共享。多个应用程序可以同时访问内存中一个 DLL 副本的内容。动态链接与静态链接的不同之处在于,它允许可执行模块(无论是.dll 还是.exe 文件)在运行时只包含定位 DLL 函数的可执行代码所需的信息。在静态链接中,链接器从静态链接库中获取所有被引用的函数,并将其与你的代码一起放入可执行文件中。使用动态链接代替静态链接有几个优点。DLL 可以节省内存,减少交换,节省磁盘空间,升级更容易,提供后市场支持,提供扩展 MFC 库类的机制,支持多语言程序,方便创建国际版本。
应用程序和 DLL 的区别
尽管 DLLs 和应用程序都是可执行的程序模块,但它们在几个方面是不同的。对于终端用户来说,最明显的区别是 DLL 不是可以直接执行的程序。从系统的角度来看,应用程序和 DLL 有两个根本的区别。
- 一个应用程序可以有多个实例同时在系统中运行,而一个 DLL 只能有一个实例。
- 应用程序可以拥有诸如堆栈、全局内存、文件句柄和消息队列等东西,但 DLL 不能。
使用 DLL 的优势
动态链接具有以下优点。
- 节省内存,减少交换。许多进程可以同时使用一个 DLL,在内存中共享一个 DLL 的副本。相比之下,Windows 必须为每个使用静态链接库构建的应用程序在内存中加载一份库代码。
- 节省了磁盘空间。许多应用程序可以在磁盘上共享 DLL 的单一副本。相比之下,每个使用静态链接库构建的应用程序都有库代码作为单独的副本链接到其可执行映像中。
- DLL 的升级更容易。当 DLL 中的函数发生变化时,只要函数的参数和返回值不发生变化,使用它们的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- 提供后市场支持。例如,可以修改显示驱动 DLL,以支持应用程序出厂时没有的显示器。
- 支持多语言程序。用不同编程语言编写的程序可以调用相同的 DLL 函数,只要程序遵循函数的调用约定即可。程序和 DLL 函数必须在以下方面兼容:函数期望其参数被推到堆栈上的顺序,是函数还是应用程序负责清理堆栈,以及是否有任何参数在寄存器中传递。
- 提供了一个扩展 MFC 库类的机制。您可以从现有的 MFC 类中派生出类,并将它们放在 MFC 扩展 DLL 中,供 MFC 应用程序使用。
- 简化国际版本的创建。通过在 DLL 中放置资源,可以更容易地创建应用程序的国际版本。您可以将您的应用程序的每个语言版本的字符串放置在一个单独的资源 DLL 中,并让不同语言版本加载适当的资源。
使用 DLL 的一个潜在的缺点是,应用程序并不是自成一体的;它依赖于一个单独的 DLL 模块的存在。
DLL 的类型
使用 Visual C++,你可以用 C 或 C++ 构建。
- 用 C 或 C++ 构建不使用微软基础类库(MFC)的 Win32 DLL。
- 你可以用 Win32 应用向导创建一个非 MFC DLL 项目。
- 通过 MFC DLL 向导可以获得 MFC 库本身,可以是静态链接库,也可以是一些 DLL。如果你的 DLL 使用的是 MFC,Visual C++ 支持三种不同的 DLL 开发方案。
- 构建一个静态链接 MFC 的常规 DLL。
- 构建一个动态链接 MFC 的常规 DLL。
- 构建一个 MFC 扩展 DLL。这些总是动态链接 MFC。
将一个可执行文件链接到一个 DLL
可执行文件以两种方式之一链接到(或加载)DLL。
- 隐式链接
- 显式链接
隐式链接有时被称为静态加载或加载时动态链接。显式链接有时被称为动态加载或运行时动态链接。在隐式链接中,使用 DLL 的可执行文件链接到由 DLL 制作者提供的导入库(.LIB 文件)。当使用 DLL 的可执行程序被加载时,操作系统会加载该 DLL。客户端可执行文件调用 DLL 的导出函数,就像这些函数包含在可执行文件中一样。
使用显式链接时,使用 DLL 的可执行程序必须进行函数调用,以显式加载和卸载 DLL,并访问 DLL 的导出函数。客户端可执行文件必须通过函数指针来调用导出的函数。一个可执行文件可以用这两种链接方法使用同一个 DLL。此外,这些机制并不相互排斥,因为一个可执行文件可以隐式地链接到 DLL,而另一个可执行文件可以显式地附加到它。
使用隐式链接
要隐式链接到 DLL,可执行文件必须从 DLL 的提供者那里获得以下内容。
- 一个头文件(.H 文件),包含导出的函数和 / 或 C++ 类的声明。
- 要链接的导入库(.LIB 文件)。当 DLL 被构建时,链接器会创建导入库。
- 实际的 DLL(.DLL 文件)。
使用 DLL 的可执行文件必须在每个包含对导出函数调用的源文件中包含包含导出函数(或 C++ 类)的头文件。从编码的角度来看,对导出函数的函数调用就像其他函数调用一样。要建立调用的可执行文件,必须与导入库链接。如果你使用的是外部的 makefile,请指定导入库的文件名,在这里列出你要链接的其他对象(.OBJ)文件或库。操作系统在加载调用的可执行文件时,必须能够找到.DLL 文件。
使用显式链接
通过显式链接,应用程序必须在运行时调用一个函数来显式加载 DLL。要显式链接到一个 DLL,应用程序必须。
- 调用 LoadLibrary()(或类似的函数)来加载 DLL 并获得一个模块句柄。
- 调用 GetProcAddress()获得应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针来调用 DLL 的函数,编译器不会产生外部引用,所以不需要与导入库链接。
- 当处理完 DLL 后,调用 FreeLibrary()。
例如
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 代码属性不再有意义。
显式链接
大多数应用程序使用隐式链接,因为它是最简单的链接方法。然而,有时显式链接是必要的。下面是一些使用显式链接的常见原因。
- 应用程序在运行前不知道必须加载的 DLL 的名称。例如,应用程序可能需要从配置文件中获取 DLL 的名称和导出的函数。
- 如果在进程启动时没有找到 DLL,使用隐式链接的进程就会被操作系统终止。使用显式链接的进程在这种情况下不会被终止,可以尝试从错误中恢复。例如,进程可以通知用户这个错误,并让用户指定另一个 DLL 的路径。
- 如果任何一个被链接到的 DLLs 的 DllMain()函数失败,使用隐式链接的进程也会被终止。在这种情况下,使用显式链接的进程不会被终止。
- 隐式链接到许多 DLL 的应用程序可能会启动缓慢,因为 Windows 在应用程序加载时加载所有的 DLL。为了提高启动性能,应用程序可以在加载后立即隐式链接到那些需要的 DLL,并等待在需要时显式链接到其他 DLL。
- 显式链接消除了用导入库链接应用程序的需要。如果 DLL 中的变化导致导出序数发生变化,使用显式链接的应用程序不必重新链接(假设他们调用 GetProcAddress()时使用的是函数名而不是序数值),而使用隐式链接的应用程序必须重新链接到新的导入库。
这里有两个显式链接的危害需要注意。
- 如果 DLL 有一个 DllMain()入口点函数,操作系统会在调用 LoadLibrary()的线程的上下文中调用该函数。如果因为之前调用 LoadLibrary()而没有相应调用 FreeLibrary()函数,DLL 已经被连接到进程中,那么这个入口点函数就不会被调用。如果 DLL 使用 DllMain()函数为进程的每个线程执行初始化,那么显式链接可能会引起问题,因为当 LoadLibrary()(或 AfxLoadLibrary())被调用时存在的线程将不会被初始化。
- 如果一个 DLL 将静态扩展数据声明为
__declspec(thread)
,如果显式链接,就会引起保护故障。在用 LoadLibrary()加载 DLL 后,只要代码引用这些数据,就会引起保护故障。(Static-extent 数据包括全局和本地静态项。)因此,当你创建一个 DLL 时,你应该避免使用线程本地存储,或者告知 DLL 用户潜在的陷阱(以防他们尝试动态加载)。
创建一个仅有资源的 DLL
纯资源 DLL 是一个只包含资源的 DLL,如图标、位图、字符串和对话框。使用只包含资源的 DLL 是在多个程序中共享同一资源集的好方法。它也是为应用程序提供多语言本地化资源的好方法。要创建一个资源专用 DLL,您需要创建一个新的 Win32 DLL(非 MFC)项目,并将您的资源添加到该项目中。
- 在 “新建项目” 对话框中选择 “Win32 项目”,并在 “Win32 项目向导” 中指定 DLL 项目类型。
- 为 DLL 创建一个包含资源(如字符串或菜单)的新资源脚本,并保存.rc 文件。
- 在 “项目” 菜单上,单击 “添加现有项目”,并将新的.rc 文件插入到项目中。
- 指定 / NOENTRY 链接器选项。/NOENTRY 可以防止链接器将对
_main
的引用链接到 DLL 中;创建一个仅有资源的 DLL 时需要这个选项。 - 构建 DLL。
使用资源专用 DLL 的应用程序应该调用 LoadLibrary()来显式链接到 DLL。要访问资源,可以调用通用函数 FindResource()和 LoadResource(),这两个函数适用于任何类型的资源,或者调用以下资源专用函数之一。
- FormatMessage()
- LoadAccelerators()
- LoadBitmap()
- LoadCursor()
- LoadIcon()
- LoadMenu()
- LoadString()
当应用程序使用完资源后,应该调用 FreeLibrary()。
导入和导出
您可以使用两种方法将公共符号导入应用程序或从 DLL 中导出函数。
- 在构建 DLL 时使用模块定义(.DEF)文件。
- 在主应用程序的函数定义中使用关键字
__declspec(dllimport)
或__declspec(dllexport)
。
使用.DEF 文件
模块定义(.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 中导出
.DLL 文件的布局与.EXE 文件非常相似,但有一个重要的区别:DLL 文件包含一个导出表。导出表包含 DLL 向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL 中的任何其他函数都是 DLL 的私有函数。DLL 的导出表可以通过 DUMPBIN 工具(Visual Studio 自带的,或者你可以尝试更强大的工具,PEBrowser( www.smidgeonsoft.prohosting/),使用 / EXPORTS 选项来查看。你可以使用两种方法从 DLL 中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建 DLL 时使用该.DEF 文件。如果你想从 DLL 中按序号而不是按 byname 导出函数,请使用这种方法。
- 在函数的定义中使用关键字
__declspec(dllexport)
。
当用这两种方法导出函数时,一定要使用 __stdcall
调用约定。模块定义(.DEF)文件是一个文本文件,它包含了一个或多个描述 DLL 各种属性的模块语句。如果你没有使用 __declspec(dllexport)
关键字来导出 DLL 的函数,DLL 需要一个.DEF 文件。一个最小的.DEF 文件必须包含以下模块定义语句。
- 文件中的第一条语句必须是 LIBRARY 语句。该语句标识了.DEF 文件属于一个 DLL。LIBRARY 语句后面是 DLL 的名称。链接器将这个名字放在 DLL 的导入库中。
- EXPORTS 语句列出了 DLL 导出的函数的名称和可选的序数值。你可以通过在函数名称后面用 at 符号(@)和一个数字给函数分配一个序数值。当您指定序数值时,它们必须在 1 到 N 的范围内,其中 N 是 DLL 导出的函数的数量。
例如,一个包含实现二进制搜索树的代码的 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
// <body of your header file>
#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 有初始化代码(如分配内存),当你的 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(对于 MFC 程序)
由于扩展 DLL 没有 CWinApp 派生的对象(和普通 DLL 一样),所以你应该将你的初始化和终止代码添加到 MFC DLL 向导生成的 DllMain()函数中。向导提供了以下扩展 DLL 的代码。在下面的代码部分,PROJNAME 是你的项目名称的占位符。
#include "stdafx.h"
#include <afxdllx.h>
#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);
// Insert this DLL into the resource chain
new CDynLinkLibrary(Dll3DLL);
}
else if(dwReason == DLL_PROCESS_DETACH)
{
TRACE0("PROJNAME.DLL Terminating!\n");
}
return 1; //ok
}
MFC 扩展 DLL 相关操作说明
在初始化过程中创建一个新的 CDynLinkLibrary
对象,允许扩展 DLL
导出 CRuntimeClass
对象或资源到客户端应用程序。
若要从一个或多个常规 DLL
中使用你的扩展 DLL
,必须导出一个创建 CDynLinkLibrary
对象的初始化函数。该函数必须从每个使用扩展 DLL
的常规 DLL
中调用。在使用任何扩展 DLL
的导出类或函数之前,调用这个初始化函数的合适位置是在常规 DLL
的 CWinApp
派生对象的 InitInstance()
成员函数中。
在 MFC DLL
向导生成的 DllMain()
中,对 AfxInitExtensionModule
的调用捕获了模块的运行时类(CRuntimeClass
结构)以及它的对象工厂(COleObjectFactory
对象),以便在创建 CDynLinkLibrary
对象时使用。应当检查 AfxInitExtensionModule
的返回值;若从 AfxInitExtensionModule
返回的值为零,则从 DllMain()
函数中返回零。
若你的扩展 DLL
将被显式链接到一个可执行文件(意味着可执行文件调用 AfxLoadLibrary
来链接到 DLL
),应当在 DLL_PROCESS_DETACH
上添加对 AfxTermExtensionModule
的调用。这个函数允许 MFC
在每个进程脱离扩展 DLL
时清理扩展 DLL
(这发生在进程退出时,或者当 DLL
因 AfxFreeLibrary
调用而被卸载时)。若你的扩展 DLL
将隐式链接到应用程序,那么对 AfxTermExtensionModule
的调用是不必要的。显式链接到扩展 DLL
的应用程序必须在释放 DLL
时调用 AfxTermExtensionModule
。
如果应用程序使用多个线程,它们还应当使用 AfxLoadLibrary
和 AfxFreeLibrary
(而非 Win32
函数 LoadLibrary()
和 FreeLibrary()
)。使用 AfxLoadLibrary
和 AfxFreeLibrary
能够确保在加载和卸载扩展 DLL
时执行的启动和关闭代码不会破坏全局 MFC
状态。
因为 MFCx0.DLL
在调用 DllMain
的时候已经完全初始化了,所以可以在 DllMain
中分配内存和调用 MFC
函数(与 16 位版本的 MFC
不同)。扩展 DLL
可以通过处理 DllMain()
函数中的 DLL_THREAD_ATTACH
和 DLL_THREAD_DETACH
情况来处理多线程。当线程附加和脱离 DLL
时,这些情况会传递给 DllMain()
。当 DLL
附加时,调用 TlsAlloc()
可以让 DLL
为每一个附加到 DLL
的线程维护线程本地存储(TLS
)索引。
请注意,头文件 AFXDLLX.H
中包含了扩展 DLL
中使用的结构的特殊定义,例如 AFX_EXTENSION_MODULE
和 CDynLinkLibrary
的定义。应当在你的扩展 DLL
中包含这个头文件。请注意,重要的是既不要定义也不要取消定义 stdafx.h
中的任何 _AFX_NO_XXX
宏。请注意,示例中包含了一个名为 LibMain()
的入口点函数,但应当将这个函数命名为 DllMain()
,以便它能与 MFC
和 C
运行时库一起工作。
初始化非 MFC DLLs
要初始化非 MFC DLL,你的 DLL 源代码必须包含一个叫做 DllMain()的函数。下面的代码提供了一个基本的骨架,展示了 DllMain()的定义。
BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch(ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
...
case DLL_THREAD_ATTACH:
...
case DLL_THREAD_DETACH:
...
case DLL_PROCESS_DETACH:
...
}
return TRUE;
}
运行时的库行为
C/C++ 运行时库代码执行 DLL 启动序列,无需像 Windows 3.x 中那样需要与单独的模块链接。C/C++ 运行时库代码中包含了名为 DllMainCRTStartup()
DLL 入口点函数。DllMainCRTStartup()
数做了几件事,包括调用_CRT_INIT,它初始化 C/C++ 运行时库,并调用静态、非本地变量上的 C++ 构造函数。如果没有这个函数,运行时库将处于未初始化状态。CRT_INIT
可以用于静态链接的 CRT,也可以用于从用户 DLL 链接到 CRT DLLmsvcrt.dll。
虽然可以使用 / ENTRY: linker 选项指定另一个入口点函数,但不建议这样做,因为你的入口点函数将不得不重复 DllMainCRTStartup()
做的一切。当用 Visual C++ 构建 DLL 时,DllMainCRTStartup()
自动链接进来,你不需要使用 / ENTRY: linker 选项指定一个入口点函数。
除了初始化 C 运行时库之外,DllMainCRTStartup()
调用一个叫做 DllMain()的函数。根据你正在构建的 DLL 的种类,Visual C++ 为你提供了 DllMain()
,并且它被链接进来,这样 DllMainCRTStartup()
是有东西可以调用。这样一来,如果你不需要初始化你的 DLL,那么在构建你的 DLL 时就没有什么特别的事情要做。如果你需要初始化你的 DLL,你在哪里添加你的代码取决于你编写的 DLL 的种类。
C/C++ 运行时库代码在静态、非本地变量上调用构造函数和析构函数。例如,在下面的 DLL 源代码中,Equus 和 Sugar 是类 CHorse 的两个静态的、非局部的对象,定义在 HORSES.H
中。在源代码中没有包含对 CHorse 的构造函数的调用,也没有对 destructor 函数的调用,因为这些对象是在任何函数之外定义的。因此,对这些构造函数和反构造函数的调用必须由运行时代码来执行。应用程序的运行时库代码也执行这个功能。
#include "horses.h"
CHorse Equus(ARABIAN, MALE);
CHorse Sugar(THOROUGHBRED, FEMALE);
BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason_for_call, LPVOID lpReserved)
...
每当一个新进程试图使用 DLL 时,操作系统都会为 DLL 的数据创建一个单独的副本:这称为 “进程附加”。DLL 的运行时库代码会调用所有全局对象的构造函数(如果有的话),然后在选择进程附加的情况下调用 DllMain()
函数。与此相反的情况是进程 detach:运行时库代码在进程 detach 被选中的情况下调用 DllMain()
,然后调用一系列终止函数,包括 atexit()
函数、全局对象的析构函数和静态对象的析构函数。需要注意的是,进程 attach 中的事件顺序与进程 detach 中的顺序是相反的。
在线程附加和线程分离过程中,运行时库代码也会被调用,但运行时代码本身并不进行初始化或终止。
Windows 动态链接库 - DLL 第二部分:方案实例
Sunbreak
2021-02-23
这篇文章主要介绍了动态链接库(DLL)的相关知识,包括 DLL 与 Windows APIs 的关系、动态链接的类型(加载时和运行时)、DLL 与内存管理、优点、入口点函数、创建及更新 DLL、重定向、数据相关内容等,还提到了一些应掌握的 Win32 技能。
关联问题:DLL 方案有哪些 DLL 实例怎样动态链接库啥作用
原文地址:www.tenouk/ModuleCC.ht…
原文作者:www.tenouk/
发布时间:约 2004 年前后
我们在这个模块里有什么?
- 动态链接库和 Windows APIs 故事和示例
- 动态链接的类型
- 负载时间动态链接
- 运行时动态链接
- DLL 和内存管理
- 动态链接的优势
- 动态链接库入口点功能
- 调用进入点函数
- 入门点功能定义
- 输入点函数返回值
- 动态链接库创建
- 创建源文件
- 输出函数
- 使用__declspec(dllexport)从 DLL 中导出。
- 导出 C 函数在 C 或 C++ 语言可执行文件中使用。
- 创建一个导入库
- 动态链接库更新
- 动态链接库重定向
- 动态链接库数据
- 可变范围
- 动态内存分配
- 线程本地存储
我的训练时间:zz 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可参见 MFC GUI 编程步骤教程。
应该掌握的 Win32 技能。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的 DLL。
- 能够创建 DLL 的导出和导入函数。
动态链接库与 Windows API 的故事与实例。
注:本模块中提供的部分信息与上一模块重复。Tenouk 在此表示歉意。
动态链接库(DLL)是一个模块,它包含了可以被另一个模块(应用程序或另一个 DLL)使用的函数和数据。一个 DLL 可以定义两种函数。
- Exported - 是为了被其他模块调用,也可以从定义它们的 DLL 中调用。
- 内部函数。- 通常是为了只从定义它们的 DLL 内部调用。
虽然一个 DLL 可以导出数据,但它的数据一般只被它的函数使用。但是,并不能阻止另一个模块对该地址进行读写。DLL 提供了一种将应用程序模块化的方法,因此功能可以更容易地更新和重用。当几个应用程序同时使用相同的功能时,它们还有助于减少内存开销,因为虽然每个应用程序都得到自己的数据副本,但它们可以共享代码。
Windows 应用程序编程接口(API)是以一组动态链接库的形式实现的,因此任何使用 Windows API 的进程都会使用动态链接。动态链接允许一个模块在加载时或运行时只包含定位导出的 DLL 函数所需的信息。动态链接不同于我们更熟悉的静态链接,在静态链接中,链接器将库函数的代码复制到每个调用它的模块中。
动态链接的类型
有两种方法可以调用 DLL 中的函数。
- 在加载时动态链接中,一个模块(应用程序或其他模块)对导出的 DLL 函数进行显式调用,就像它们是本地函数一样。这需要将模块与包含函数的 DLL 的导入库进行链接。导入库为系统提供加载 DLL 所需的信息,并在应用程序加载时定位导出的 DLL 函数。
- 在运行时动态链接中,模块(应用程序或另一个模块)在运行时使用 LoadLibrary()或 LoadLibraryEx()函数加载 DLL。DLL 加载完毕后,模块调用 GetProcAddress()函数来获取导出的 DLL 函数的地址。模块使用 GetProcAddress()返回的函数指针调用导出的 DLL 函数。这样就不需要导入库了。
加载时动态链接
当系统启动一个使用加载时动态链接的程序时,会利用链接器放在文件中的信息来定位进程所使用的 DLL 的名称。然后系统依次在以下位置搜索 DLLs。
- 应用程序加载的目录
- 当前目录。
- 系统目录。使用 GetSystemDirectory()函数来获取该目录的路径。
- 16 位系统目录。没有获取该目录路径的函数,但会搜索该目录。对于 Windows Me/98/95:该目录不存在。
- Windows 目录。使用 GetWindowsDirectory()函数来获取该目录的路径。
- PATH 环境变量中列出的目录。在命令提示符下输入 PATH 命令,可确认目录。
对于 Windows Server 2003,Windows XP SP1:HKLM/System/CurrentControlSet/Control/Session Manager/SafeDllSearchMode 的默认值为 1(在系统和 Windows 目录之后搜索当前目录)。对于 Windows XP。 如果 HKLMSystem/CurrentControlSet/Control/Session Manager/SafeDllSearchMode 为 1,则在系统和 Windows 目录之后搜索当前目录,但在 PATH 环境变量中的目录之前搜索。默认值是 0(在系统和 Windows 目录之前搜索当前目录)。请注意,这个值在加载时是以每个进程为基础进行缓存的。
如果系统无法找到所需的 DLL,则会终止进程,并显示一个向用户报告错误的对话框。否则,系统会将 DLL 映射到进程的虚拟地址空间,并递增 DLL 引用计数。系统调用入口点函数。该函数接收到一个代码,表示进程正在加载 DLL。如果切入点函数没有返回 TRUE,系统将终止进程并报告错误。最后,系统用导入的 DLL 函数的起始地址修改函数地址表。DLL 在初始化过程中被映射到进程的虚拟地址空间中,只有在需要时才加载到物理内存中。
运行时动态链接
当应用程序调用 LoadLibrary()或 LoadLibraryEx()函数时,系统尝试使用加载时动态链接中使用的相同搜索序列来定位 DLL。如果搜索成功,系统将 DLL 模块映射到进程的虚拟地址空间,并递增引用计数。如果对 LoadLibrary()或 LoadLibraryEx()的调用指定了一个 DLL,其代码已经映射到调用进程的虚拟地址空间中,该函数只是返回一个 DLL 的句柄,并递增 DLL 的引用计数。请注意,两个基本文件名和扩展名相同但在不同目录下的 DLL 不被认为是同一个 DLL。
系统在调用 LoadLibrary()或 LoadLibraryEx()的线程的上下文中调用入口点函数。如果 DLL 已经被进程通过调用 LoadLibrary()或 LoadLibraryEx()加载,而没有相应的 FreeLibrary()函数的调用,则不调用入口点函数。
如果系统找不到 DLL,或者入口点函数返回 FALSE,LoadLibrary()或 LoadLibraryEx()返回 NULL。如果 LoadLibrary()或 LoadLibraryEx()成功,它将返回一个 DLL 模块的句柄。进程可以在调用 GetProcAddress()、FreeLibrary()或 FreeLibraryAndExitThread()函数时使用这个句柄来识别 DLL。
GetModuleHandle()函数返回一个在 GetProcAddress()、FreeLibrary()或 FreeLibraryAndExitThread()中使用的句柄。GetModuleHandle()函数只有在 DLL 模块已经通过加载时链接或通过之前对 LoadLibrary()或 LoadLibraryEx()的调用映射到进程的地址空间时才会成功。与 LoadLibrary()或 LoadLibraryEx()不同的是,GetModuleHandle()不会递增模块引用计数。GetModuleFileName()函数检索与 GetModuleHandle()、LoadLibrary()或 LoadLibraryEx()返回的句柄相关联的模块的完整路径。
进程可以使用 GetProcAddress()来获取 DLL 中使用 LoadLibrary()或 LoadLibraryEx()、GetModuleHandle()返回的 DLL 模块句柄的导出函数的地址。当不再需要 DLL 模块时,进程可以调用 FreeLibrary()或 FreeLibraryAndExitThread()。这些函数会递减模块引用计数,如果引用计数为零,则从进程的虚拟地址空间中解映射 DLL 代码。运行时动态链接使进程能够继续运行,即使 DLL 不可用。然后,进程可以使用另一种方法来完成其目标。例如,如果一个进程无法找到一个 DLL,它可以尝试使用另一个 DLL,或者它可以通知用户一个错误。如果用户能够提供缺失的 DLL 的完整路径,即使 DLL 不在正常的搜索路径中,进程也可以利用这个信息来加载该 DLL。这种情况与加载时链接形成了鲜明的对比,在加载时链接中,如果系统找不到 DLL,就会简单地终止进程。如果 DLL 使用 DllMain()函数为进程的每个线程执行初始化,运行时动态链接可能会引起问题,因为在调用 LoadLibrary()或 LoadLibraryEx()之前存在的线程不会调用入口点。
DLLs 和内存管理
每个加载 DLL 的进程都会将其映射到它的虚拟地址空间中。进程将 DLL 加载到其虚拟地址后,就可以调用导出的 DLL 函数。系统为每个 DLL 维护一个每线程的引用计数。当一个线程加载 DLL 时,引用计数会递增 1。当进程终止时,或者当参考计数变为零时(仅运行时动态链接),DLL 将从进程的虚拟地址空间中卸载。像任何其他函数一样,导出的 DLL 函数在调用它的线程的上下文中运行。因此,以下条件适用。
- 调用 DLL 的进程的线程可以使用 DLL 函数打开的句柄。同样,调用进程的任何线程打开的句柄也可以在 DLL 函数中使用。
- DLL 使用调用线程的栈和调用进程的虚拟地址空间。
- DLL 从调用进程的虚拟地址空间中分配内存。
动态链接的优点
与静态链接相比,动态链接具有以下优点。
- 在同一基址加载同一 DLL 的多个进程在物理内存中共享一个 DLL 的副本。这样做可以节省系统内存,减少交换。
- 当 DLL 中的函数发生变化时,只要函数参数、调用约定和返回值不发生变化,使用这些函数的应用程序就不需要重新编译或重新链接。而静态链接的对象代码则需要在函数改变时重新链接应用程序。
- DLL 可以提供后市场支持。例如,一个显示驱动 DLL 可以被修改为支持应用程序最初出厂时不可用的显示器。
- 用不同编程语言编写的程序可以调用相同的 DLL 函数,只要这些程序遵循函数使用的相同调用约定。调用惯例(如 C、Pascal 或标准调用)控制了调用函数必须将参数推到堆栈上的顺序,是函数还是调用函数负责清理堆栈,以及是否有任何参数在寄存器中传递。更多的信息,请参见编译器附带的文档。
使用 DLL 的一个潜在的缺点是应用程序不是自足的,它依赖于一个单独的 DLL 模块的存在。如果使用加载时动态链接的进程需要一个在进程启动时没有找到的 DLL,系统就会终止该进程,并给用户一个错误信息。在这种情况下,系统不会终止使用运行时动态链接的进程,但缺失的 DLL 所导出的函数对程序不可用。
动态链接库入口点函数
DLL 可以选择性地指定一个切入点函数,如果存在,系统会在进程或线程加载或卸载时调用切入点函数。如果存在,每当一个进程或线程加载或卸载 DLL 时,系统就会调用该入口点函数。它可以用来执行简单的初始化和清理任务。例如,它可以在创建新线程时设置线程本地存储,并在线程终止时清理它。
如果你将你的 DLL 与 C 运行时库链接,它可能会为你提供一个入口点函数,并允许你提供一个单独的初始化函数。查看你的运行时库的文档以了解更多信息。如果您提供了自己的入口点,请参见下一节中的 DllMain()函数。DllMain()这个名字是用户定义函数的占位符。您必须在构建 DLL 时指定实际使用的名称。
调用入口点函数
每当发生以下任何一个事件时,系统就会调用入口点函数。
- 进程加载 DLL。对于使用加载时动态链接的进程,DLL 在进程初始化期间被加载。对于使用运行时链接的进程,DLL 在 LoadLibrary()或 LoadLibraryEx()返回之前被加载。
- 进程会卸载 DLL。当进程终止或调用 FreeLibrary()函数,并且引用计数为零时,DLL 被卸载。如果进程因 TerminateProcess()或 TerminateThread()函数而终止,系统不会调用 DLL 入口点函数。
- 在加载了 DLL 的进程中会创建一个新的线程。你可以使用 DisableThreadLibraryCalls()函数来禁用线程创建时的通知。
- 已加载 DLL 的进程的线程正常终止,不使用 TerminateThread()或 TerminateProcess()。当进程卸载 DLL 时,入口点函数只对整个进程调用一次,而不是对进程的每个现有线程调用一次。你可以使用 DisableThreadLibraryCalls()来禁止线程终止时的通知。
每次只有一个线程可以调用入口点函数。系统在引起调用函数的进程或线程的上下文中调用切入点函数,这允许 DLL 使用其切入点函数在调用进程的虚拟地址空间中分配内存或打开进程可访问的句柄。这使得 DLL 可以使用它的切入点函数在调用进程的虚拟地址空间中分配内存,或者打开进程可以访问的句柄。切入点函数还可以通过使用线程本地存储(TLS)为新线程分配私有的内存。
入门点函数定义
DLL 入口点函数必须用标准调用约定(__stdcall
)来声明。若没有正确声明 DLL 入口点,则 DLL 不会被加载,系统会显示一条消息,表明必须用 WINAPI
声明 DLL 入口点。
对于 Windows Me/98/95:若 DLL 入口点声明不正确,则 DLL 未被加载,系统显示一条名为 "Error starting program"
的消息,指示用户检查文件以确定问题所在。
在函数的主体中,可以处理以下 DLL 入口点被调用的任意组合情况:
- 一个进程加载 DLL(
DLL_PROCESS_ATTACH
)。 - 当前进程创建了一个新的线程(
DLL_THREAD_ATTACH
)。 - 一个线程正常退出(
DLL_THREAD_DETACH
)。 - 一个进程卸载 DLL(
DLL_PROCESS_DETACH
)。
入口点函数应该只执行简单的初始化任务。它不能调用 LoadLibrary()
或 LoadLibraryEx()
函数(或调用这些函数的函数),因为这可能会在 DLL 加载顺序中产生依赖性循环,这可能导致 DLL 在系统执行其初始化代码之前就被使用。
同样,入口点函数不能调用 FreeLibrary()
函数(或调用 FreeLibrary()
的函数),因为这可能会导致在系统执行其终止代码后使用 DLL。
调用 Kernel32.dll
中的其他函数是安全的,因为当调用切入点函数时,保证这个 DLL 会被加载到进程地址空间中。通常情况下,entry-point
函数会创建同步对象,如关键部分和 mutexes,并使用 TLS。
不要调用注册表函数,因为它们位于 Advapi32.dll
中。
如果你正在与 C 运行时库进行动态链接,不要调用 malloc()
,而是调用 HeapAlloc()
。
调用 Kernel32.dll
以外的导入函数可能会导致难以诊断的问题。例如,调用 User
、Shell
和 COM
函数可能会导致访问违规错误,因为它们的 DLL 中的一些函数会调用 LoadLibrary()
来加载其他系统组件。
下面的例子演示了如何构造 DLL 入口点函数。
#include <windows.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, //handle to DLL module
DWORD fdwReason, //reason for calling function
LPVOID lpReserved) //reserved
{
// Perform actions based on the reason for calling.
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
// Initialize once for each new process.
// Return FALSE to fail DLL load.
break;
case DLL_THREAD_ATTACH:
// Do thread-specific initialization.
break;
case DLL_THREAD_DETACH:
// Do thread-specific cleanup.
break;
case DLL_PROCESS_DETACH:
// Perform any necessary cleanup.
break;
}
// Successful DLL_PROCESS_ATTACH.
return TRUE;
}
输入点函数返回值
当因为进程正在加载而调用 DLL 入口点函数时,该函数返回 TRUE 表示成功。对于使用加载时链接的进程,返回值为 FALSE 会导致进程初始化失败,进程终止。对于使用运行时链接的进程,返回值为 FALSE 会导致 LoadLibrary()
或 LoadLibraryEx()
函数返回 NULL,表示失败。系统会立即用 DLL_PROCESS_DETACH
调用你的入口点函数,并卸载 DLL。当该函数因其他原因被调用时,入口点函数的返回值将被忽略。
动态链接库的创建
要创建一个动态链接库(DLL),你必须创建一个或多个源代码文件,也可能创建一个用于导出函数的链接器文件。如果您计划允许使用您的 DLL 的应用程序使用加载时动态链接,您还必须创建一个导入库。
创建源代码文件
DLL 的源文件包含导出的函数和数据,内部函数和数据,以及 DLL 的可选入口点函数。您可以使用任何支持创建基于 Windows 的 DLL 的开发工具。如果你的 DLL 可能被一个多线程的应用程序使用,你应该使你的 DLL 成为 “线程安全”。你必须同步访问 DLL 的所有全局数据以避免数据损坏。你还必须确保你只链接到线程安全的库。例如,Microsoft 速 Visual C++ 速包含了多个版本的 C 运行时库,一个是不线程安全的,两个是线程安全的。更多细节请参考模块 A。
导出函数
如何指定 DLL 中的哪些函数应该被导出取决于你所使用的开发工具。一些编译器允许你通过在函数声明中使用修饰符直接在源代码中导出函数。其他时候,你必须在传递给链接器的文件中指定导出。例如,使用 Visual C++,有两种可能的方法来导出 DLL 函数。
- 用
__declspec
修饰符或者是… - 用一个.def 文件。
如果你使用 __declspec
修饰符,就没有必要使用 .def 文件。
使用 __declspec(dllexport)
从 DLL 中导出。
.DLL 文件的布局与.EXE 文件非常相似,但有一个重要的区别:DLL 文件包含一个导出表。导出表包含 DLL 向其他可执行文件导出的每个函数的名称。这些函数是进入 DLL 的入口点;只有导出表中的函数可以被其他可执行文件访问。DLL 中的任何其他函数都是 DLL 的私有函数。可以通过 DUMPBIN 工具的 / EXPORTS 选项来查看 DLL 的导出表。您可以使用两种方法从 DLL 中导出函数。
- 创建一个模块定义(.DEF)文件,并在构建 DLL 时使用该.DEF 文件。如果你想从 DLL 中按序号而不是按名称导出函数,请使用这种方法。
- 在函数的定义中使用关键字__declspec(dllexport)。
当用这两种方法导出函数时,一定要使用 __stdcall
的调用约定。微软在 Visual C++ 的 16 位编译器版本中引入了 __export
,允许编译器自动生成导出名,并将它们放在一个.LIB 文件中。然后,这个.LIB 文件可以像静态.LIB 一样与 DLL 链接使用。
在 32 位编译器版本中,你可以使用 __declspec(dllexport)
关键字从 DLL 中导出数据、函数、类或类成员函数。__declspec(dllexport)
将导出指令添加到对象文件中,因此你不需要使用一个.DEF 文件。
这种便利性在试图导出装饰的 C++ 函数名时最为明显。没有标准的名称装饰规范,所以导出的函数名可能会在不同的编译器版本之间发生变化。如果你使用 __declspec(dllexport)
,重新编译 DLL 和依赖的 .EXE 文件是必要的,只是为了考虑到任何命名约定的变化。
许多导出指令,例如 ordinals、NONAME 和 PRIVATE,只能在 .DEF 文件中进行,没有 .DEF 文件就无法指定这些属性。然而,除了使用 .DEF 文件之外,使用 __declspec(dllexport)
不会导致构建错误。要导出函数,__declspec(dllexport)
关键字必须出现在调用约定关键字的左边,如果有指定关键字的话。例如:
__declspec(dllexport)void __cdecl FunctionName(void);
要导出一个类中所有的公共数据成员和成员函数,关键字必须出现在类名的左边,如下所示。
class __declspec(dllexport)CExampleExport : public CObject
{ ... class definition ... };
当构建你的 DLL 时,你通常会创建一个头文件,其中包含你要导出的函数原型和 / 或类,并在头文件的声明中添加 __declspec(dllexport)
。为了使你的代码更易读,为 __declspec(dllexport)
定义一个宏,并对你要导出的每个符号使用这个宏。例如:
#define DllExport __declspec(dllexport)
__declspec(dllexport)
在 DLL 的导出表中存储函数名。当把 DLL 源代码从 Win16 移植到 Win32 时,用 __declspec(dllexport)
替换 __export
的每个实例。作为参考,在 Win32 WINBASE.H 头文件中搜索 __declspec(dllimport)
的使用实例。
导出 C 函数在 C 或 C++ 语言可执行文件中的用途
如果你在用 C 语言编写的 DLL 中有一些函数,而你想从 C 语言或 C++ 语言的模块中访问这些函数,你应该使用 __cplusplus
预处理程序宏来确定正在编译哪种语言,如果是从 C++ 语言的模块中使用这些函数,则用 C 链接来声明这些函数。如果你使用这种技术,并为你的 DLL 提供头文件,这些函数可以被 C 和 C++ 用户使用,而不会有任何改变。
下面的代码显示了一个可以被 C 和 C++ 客户端应用程序使用的头文件。
// MyCFuncs.h
#ifdef __cplusplus
extern "C" { //only need to export C interface if used by C++ source code
#endif
__declspec(dllimport)void MyCFunc();
__declspec(dllimport)void AnotherCFunc();
#ifdef __cplusplus
}
#endif
如果你需要将 C 函数链接到你的 C++ 可执行文件中,而函数声明头文件没有使用上述技术,在 C++ 源文件中,做如下操作,以防止编译器装饰 C 函数名。
extern "C" {
#include "MyCHeader.h"
}
创建一个导入库
一个导入库(.lib)文件包含了链接器所需的信息,以解析对导出的 DLL 函数的外部引用,因此系统可以在运行时定位指定的 DLL 和导出的 DLL 函数。例如,要调用 CreateWindow()函数,你必须将你的代码与导入库 User32.lib 链接。原因是 CreateWindow()驻留在一个名为 User32.dll 的系统 DLL 中,而 User32.lib 是用于解析你的代码中对 User32.lib 中导出函数的调用的导入库。链接器会创建一个包含每个函数调用地址的表。当加载 DLL 时,对 DLL 中函数的调用将被固定起来。当系统在初始化进程时,它会加载 User32.dll,因为进程依赖于该 DLL 中的导出函数,它更新函数地址表中的条目。所有对 CreateWindow()的调用都会调用 User32.dll 中导出的函数。警告。 在 DLL 中调用 ExitProcess()函数可能导致意外的应用程序或系统错误。只有当您知道哪些应用程序或系统组件将加载 DLL,并且在此上下文中调用 ExitProcess()是安全的,才能确保从 DLL 中调用 ExitProcess()。
动态链接库更新
有时需要用较新的版本替换 DLL。在替换 DLL 之前,请执行版本检查,以确保您是在用较新的版本替换旧版本。可以替换正在使用的 DLL。替换正在使用的 DLL 的方法取决于您使用的操作系统。在 Windows XP 和更高版本上,应用程序应使用隔离应用程序和并排装配体。如果执行以下步骤,则无需重新启动计算机。
- 使用 MoveFileEx()函数重命名被替换的 DLL。不要指定 MOVEFILE_ALLOWED,并确保重命名的文件在包含原始文件的同一卷上。你也可以简单地重命名同一目录下的文件,给它一个不同的扩展名。
- 将新的 DLL 复制到包含重命名 DLL 的目录中。现在所有的应用程序都将使用新的 DLL。
- 使用带有 MOVEFILE_DELAY_UNTIL_REBOOT 的 MoveFileEx()来删除重命名的 DLL。
在你进行这个替换之前,应用程序将使用原来的 DLL,直到它被卸载。在你进行替换之后,应用程序将使用新的 DLL。当你编写一个 DLL 时,你必须注意确保它已经为这种情况做好了准备,特别是当 DLL 维护全局状态信息或与其他服务通信时。如果 DLL 没有为全局状态信息或通信协议的更改做好准备,更新 DLL 将需要您重新启动计算机,以确保所有应用程序都使用相同版本的 DLL。对于 Windows Me/98/95:因为不支持 MoveFileEx(),所以需要重新启动计算机。
动态链接库重定向
当应用程序加载的 DLL 版本与出厂时的版本不同时,可能会出现问题。从 Windows 2000 开始,您可以通过创建一个重定向文件来确保您的应用程序使用 DLL 的正确版本。重定向文件的内容会被忽略,但它的存在会强制应用程序目录中的所有 DLL 从该目录加载。
重定向文件必须命名为:appname.local。
例如,如果应用程序的名称是 editor.exe,重定向文件就命名为 editor.exe.local。你必须将 itor.exe.local 安装在包含 editor.exe 的同一目录中。你也必须在同一目录下安装 DLLs。如果存在重定向文件,LoadLibrary()和 LoadLibraryEx()函数会改变其搜索顺序。如果指定了路径,并且存在应用程序的重定向文件,这些函数就会在应用程序的目录中搜索 DLL。如果 DLL 存在于应用程序的目录中,这些函数忽略指定的路径,从应用程序的目录中加载 DLL。如果模块不在应用程序的目录中,这些函数从指定的目录中加载 DLL。例如,一个应用程序 c:\myapp\myapp.exe 使用以下路径调用 LoadLibrary()。
c:program files/common files/system/mydll.dll
如果 c:\myapp\myapp.exe.local 和 c:\myapp\mydll.dll 存在,LoadLibrary()将加载 c:\myapp\mydll.dll。否则,LoadLibrary()将加载 c:program files\common files\system\mydll.dll。在包含应用程序的同一目录下安装应用程序的 DLLs 是一个很好的做法,即使你不使用重定向。它可以确保安装你的应用程序不会覆盖 DLL 的其他副本并导致其他应用程序失败。此外,其他应用程序也不会覆盖您的 DLL 副本而导致应用程序失败。
动态链接库数据
动态链接库(DLL)可以包含全局数据或本地数据。
变量范围
DLL 变量的默认范围与应用程序中声明的变量的范围相同。DLL 源代码文件中的全局变量对使用 DLL 的每个进程都是全局的。静态变量的作用域限于它们被声明的块。因此,默认情况下,每个进程都有自己的 DLL 全局变量和静态变量实例。您的开发工具可能允许您覆盖全局变量和静态变量的默认范围。
动态内存分配
当 DLL 使用任何一个内存分配函数(GlobalAlloc()、LocalAlloc()、HeapAlloc()和 VirtualAlloc())分配内存时,内存是在调用进程的虚拟地址空间中分配的,并且只有该进程的线程可以访问。DLL 可以使用文件映射来分配可以在进程间共享的内存。
线程本地存储
线程本地存储(TLS)函数使 DLL 能够为多线程进程的每个线程分配一个索引,用于存储和检索不同的值。例如,一个电子表格应用程序可以在用户每次打开一个新的电子表格时创建一个新的同一线程实例。为各种电子表格操作提供函数的 DLL 可以使用 TLS 来保存每个电子表格的当前状态信息(行、列等)。
警告。 Visual C++ 编译器支持一种语法,可以让你声明线程本地变量。__declspec(thread)
。如果你在一个 DLL 中使用这个语法,你将不能使用 LoadLibrary()或 LoadLibraryEx()显式加载 DLL。如果你的 DLL 将被显式加载,你必须使用线程本地存储函数来代替 __declspec(thread)
。
Windows 动态链接库 - DLL 第三部分:方案实例
Sunbreak
2021-02-23
这篇文章主要介绍了 Windows 动态链接库(DLL)的相关内容,包括创建简单的 DLL 程序、使用加载时动态链接和运行时动态链接,还列举了相关代码示例及可能出现的问题,最后提供了进一步阅读和挖掘的参考资料。
关联问题:DLL 方案有哪些 DLL 实例怎样 DLL 如何应用
原文地址:www.tenouk/ModuleCC1.h…
原文作者:www.tenouk/
发布时间:约 2004 年前后
本模块有哪些内容?
- 创建一个简单的动态链接库程序
- 使用加载时动态链接
- 使用运行时动态链接
我的训练时间: aa 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
Win32 编程技能。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的动态链接库。
- 能够创建 DLL 的导出和导入函数。
创建一个简单的动态链接库程序
下面快照演示如何创建和使用 DLL。首先我们来看看如何创建 DLL 项目的步骤。
选择 Win32 控制台项目,把项目名称。
选择 DLL 单选按钮并勾选 “空项目”。
然后照常添加 C++ 源文件。
复制并粘贴下面的源代码。构建即可,不要运行。稍后,我们要创建一个应用程序,在我们的 DLL 程序中使用 mydll()函数。接下来我们就可以构建我们的 DLL 程序了。在这个例子中,我们构建的是 Release 版本。
示例相关说明
下面的例子 mysrcdll.cpp
是创建一个简单的 DLL
程序 mydllpro.dll
所需要的源代码。
文件 mysrcdll.cpp
包含一个简单的字符串打印函数,叫做 mydll()
。
mydllpro.dll
并没有定义一个入口点函数,因为它是与 C
运行时库链接在一起的,没有自己的初始化或清理功能要执行。
// Project name: mydllpro, File name: mysrcdll.cpp, generating mydllpro.dll, mydllpro.lib...
// The mydll function writes a null-terminated string to
//the standard output device...
// The export mechanism used here is the __declspec(export)
//method supported by Microsoft Visual Studio, but any
//other export method supported by your development
//environment may be substituted.
// For WinXp, don't forget to add
// Advapi32.lib library if needed...
#define _WIN32_WINNT 0x0501
#include <windows.h>
#include <stdio.h>
#define EOF(-1)
#ifdef __cplusplus // If used by C++ code,
extern "C" { //we need to export the C interface
#endif
__declspec(dllexport)int mydll(LPTSTR lpszMsg)
{
DWORD cchWritten;
HANDLE hStdout;
BOOL fRet;
printf("-This is mydll.dll file lol!-\n");
// Get a handle to the standard output device.
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if(INVALID_HANDLE_VALUE == hStdout)
{
//failed to get the handle, give some message, get error code, just exit...
printf("GetStdHandle(), invalid handle, error: GetLastError().\n");
return EOF;
}
else
printf("GetStdHandle(), standard handle is OK.\n");
// Write a null-terminated string to the standard output device.
while(*lpszMsg != '\0')
{
fRet = WriteFile(hStdout, lpszMsg, 1, &cchWritten, NULL);
if((fRet == FALSE)||(cchWritten != 1))
// If something wrong just exit or provide meaningful message/error code...
return EOF;
//else, write more...
lpszMsg++;
}
printf("\n");
return 1;
}
#ifdef __cplusplus
}
#endif
设置发布版本。
或者使用配置管理器…
好了,最后让建立程序。然后,验证一下准备使用的 DLL 文件(mydllpro.dll)的创建。注意,mydllpro.lib 文件将用于隐式链接(加载)。
使用加载时动态链接
创建了一个 DLL 之后,就可以在应用程序中使用它了。下面的文件 mydllloadtime.cpp 是一个空的 Win32 控制台应用程序的源代码,它使用从 mydll.dll 导出的 mydll()函数。请注意,我们之前创建的 DLL 程序中没有任何用户定义的头文件。
// File: mydllloadtime.cpp
// A simple program that uses mydll()from mydllpro.dll.
// For WinXp, don't forget to add
#define _WIN32_WINNT 0x0501
#include <windows.h>
//call to a function in the mydllpro.dll
__declspec(dllimport)int mydll(LPTSTR);
// Another form: int mydll(LPTSTR);
int main()
{
int Ret = 1;
Ret = mydll("This message was printed using the DLL function");
return Ret;
}
因为 mydllloadtime.cpp 隐式调用 DLL 函数,所以应用程序的模块必须与导入的 librarymydllpro.lib 链接。在本例中,mydllpro.lib 和 mydllpro.dll 被复制到项目目录下。
不幸的是,尽管已经遵循了所有必要的步骤,但当调用 mydll()时,这个程序失败了。错误如下所示。
...
...
Loaded kernel32.lib(KERNEL32.dll)
Found __NULL_IMPORT_DESCRIPTOR
Referenced in kernel32.lib(KERNEL32.dll)
Loaded kernel32.lib(KERNEL32.dll)
Found KERNEL32_NULL_THUNK_DATA
Referenced in kernel32.lib(KERNEL32.dll)
...
...
testdll.obj : error LNK2019: unresolved external symbol "int __cdecl mydll(char *)"(?mydll@@YAHPAD@Z)referenced in function _main
Release/mydlltest.exe : fatal error LNK1120: 1 unresolved externals
从构建输出来看,库文件已经被搜索,但没有被加载 / 处理。Tenouk 没有在 Visual C++ 编译器上尝试这个例子。让我们尝试使用同样的代码来实现 Run-Time Dynamic Linking。
使用 Run-Time Dynamic Linking
你可以在加载时和运行时动态链接中使用同一个 DLL。下面的源代码产生了与上一节中加载时示例相同的输出。程序使用 LoadLibrary()函数来获取 mydll.dll 的句柄。如果 LoadLibrary()成功,程序在 GetProcAddress()函数中使用返回的句柄来获取 DLL 的 mydll()函数的地址。
调用 DLL 函数后,程序调用 FreeLibrary()函数来卸载 DLL。下面的例子说明了运行时和加载时动态链接之间的重要区别。如果 themydll.dll 文件不可用,使用加载时动态链接的应用程序就会简单地终止。然而,运行时动态链接的例子可以对错误做出响应。
这个项目是一个空的控制台模式的应用程序。将 mydllpro.dll 复制到项目目录或系统目录或前面提到的任何其他目录序列中。在本例中,mydllpro.dll 已被复制到项目目录下。
// File: testmydllruntime.cpp
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access mydll()in mydllpro.dll.
// For WinXp, don't forget to add
#define _WIN32_WINNT 0x0501
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module created in the previous example.
hinstLib = LoadLibrary("mydllpro");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The dll handle is valid...\n");
ProzAdd =(MYPROC)GetProcAddress(hinstLib, "mydll");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Pass some text, mydll()will display it on the standard output...
(ProcAdd)("\nThis message is via DLL function...\n");
}
else
printf("\nThe function address is NOT valid, error: % d.\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK, error: % d.\n", GetLastError());
}
else
printf("The dll handle is not valid, error: % d.\n", GetLastError());
// If unable to call the DLL function, use an alternative.
if(!fRunTimeLinkSuccess)
printf("This message via alternative method...\n");
return 0;
}
一个示例输出。
好了,它工作了!该程序使用运行时动态链接;在创建该程序时,你不应该像我们的 Load-Time 动态链接示例那样与导入库链接。让我们继续看更多的故事。
Windows 动态链接库 - DLL 第四部分:方案实例
Sunbreak
2021-02-23
文章主要介绍了动态链接库(DLL)的相关内容,包括在 DLL 中使用共享内存、测试 MainDll()、函数调用公约、使用线程本地存储技术、测试 DLL 程序、DLL 参考及过时功能等,还提供了相关的代码示例和函数说明。
关联问题:DLL 方案怎样 DLL 实例有哪些 DLL 作用是什么
原文地址:www.tenouk/ModuleCC2.h…
原文作者:www.tenouk/
发布时间:约 2004 年前后
在这个模块中我们有什么?
- 在动态链接库中使用共享内存
- 测试我们的 MainDll()
- 函数调用公约
- 在动态链接库中使用线程本地存储技术
- 测试 DLL 程序
- 动态链接库参考
- 职能
- 过时的功能
我的训练时间: xyz 小时。在你开始之前,请阅读这里的 一些说明。DLL 的 Windows MFC 编程(GUI 编程)可以在 MFC GUI 编程步骤教程中找到。
Win32 的编程技巧。
- 能够理解、构建和运行动态链接库程序。
- 能够区分静态链接和动态链接。
- 能够识别不同类型的动态链接库。
- 能够创建 DLL 的导出和导入函数。
动态链接库中共享内存的使用。
本节介绍了 DLL 入口点函数如何使用文件映射对象来设置内存,使加载 DLL 的进程可以共享内存。共享的 DLL 内存只在 DLL 被加载时才会持续存在。本例使用文件映射将一个命名的共享内存块映射到每个加载 DLL 的进程的虚拟地址空间中。要做到这一点,入口点函数必须。
- 调用 CreateFileMapping()函数获得一个文件映射对象的句柄。第一个加载 DLL 的进程创建文件映射对象。随后的进程打开现有对象的句柄。
- 调用 MapViewOfFile()函数将一个视图映射到虚拟地址空间。这样,进程就可以访问共享内存。
请注意,虽然您可以通过为 CreateFileMapping()的 lpAttributes 参数传递一个 NULL 值来指定默认的安全属性,但您可以选择使用 SECURITY_ATTRIBUTES 结构来提供额外的安全性。
这是一个空的 DLL 工程(程序),你需要将你的工程设置为使用__stdcall(使用 WINAPI)约定的 dllmain()。以下是 Visual C++ .Net 的设置。
// Project name: moredll, File name: dllentryfunc.cpp generating moredll.dll
//but no moredll.lib! The DLL entry-point function sets up shared memory using
//a named file-mapping object.
#include <windows.h>
#include <stdio.h>
#include <memory.h>
#define SHMEMSIZE 4096
static LPVOID lpvMem = NULL; //pointer to shared memory
static HANDLE hMapObject = NULL; //handle to file mapping
BOOL DllMain(HINSTANCE hinstDLL, // DLL module handle
DWORD fdwReason, //reason called
LPVOID lpvReserved) //reserved
{
BOOL fInit, fIgnore;
switch(fdwReason)
{
// The DLL is loading due to process
//initialization or a call to LoadLibrary.
case DLL_PROCESS_ATTACH:
printf("The DLL is loading...from moredll.dll.\n");
// Create a named file mapping object.
hMapObject = CreateFileMapping(
INVALID_HANDLE_VALUE, //use paging file
NULL, //default security attributes
PAGE_READWRITE, //read/write access
0, //size: high 32-bits
SHMEMSIZE, //size: low 32-bits
"dllmemfilemap"); //name of map object
if(hMapObject == NULL)
return FALSE;
else
printf("CreateFileMapping()is OK.\n");
// The first process to attach initializes memory.
fInit =(GetLastError()!= ERROR_ALREADY_EXISTS);
// Get a pointer to the file-mapped shared memory.
lpvMem = MapViewOfFile(
hMapObject, //object to map view of
FILE_MAP_WRITE, //read/write access
0, //high offset: map from
0, //low offset: beginning
0); //default: map entire file
if(lpvMem == NULL)
return FALSE;
else
printf("MapViewOfFile()is OK.\n");
// Initialize memory if this is the first process.
if(fInit)
memset(lpvMem, '\0', SHMEMSIZE);
break;
// The attached process creates a new thread.
case DLL_THREAD_ATTACH:
printf("The attached process creates a new thread...from moredll.dll.\n");
break;
// The thread of the attached process terminates.
case DLL_THREAD_DETACH:
printf("The thread of the attached process terminates... from moredll.dll.\n");
break;
// The DLL is unloading from a process due to
//process termination or a call to FreeLibrary().
case DLL_PROCESS_DETACH:
printf("The DLL is unloading from a process... from moredll.dll.\n");
// Unmap shared memory from the process's address space.
fIgnore = UnmapViewOfFile(lpvMem);
// Close the process's handle to the file-mapping object.
fIgnore = CloseHandle(hMapObject);
break;
default:
printf("Reason called not matched, error if any: % d... from moredll.dll.\n", GetLastError());
break;
}
return TRUE;
UNREFERENCED_PARAMETER(hinstDLL);
UNREFERENCED_PARAMETER(lpvReserved);
}
// Can be commented out for this example...
// SetSharedMem()sets the contents of shared memory.
VOID SetSharedMem(LPTSTR lpszBuf)
{
LPTSTR lpszTmp = "Testing some string";
// Get the address of the shared memory block.
lpszTmp =(LPTSTR)lpvMem;
// Copy the null-terminated string into shared memory.
while(*lpszBuf)
*lpszTmp++ = *lpszBuf++;
*lpszTmp = '\0';
printf("The content: % s.\n", lpszTmp);
}
// Can be commented out for this example...
// GetSharedMem()gets the contents of shared memory.
VOID GetSharedMem(LPTSTR lpszBuf, DWORD cchSize)
{
LPTSTR lpszTmp;
// Get the address of the shared memory block.
lpszTmp =(LPTSTR)lpvMem;
// Copy from shared memory into the caller's buffer.
while(*lpszTmp && --cchSize)
*lpszBuf++ = *lpszTmp++;
*lpszBuf = '\0';
printf("The caller buffer: % s.\n", lpszBuf);
}
一个示例输出。
将你的项目改为 Release 版本,并重建 DLL 程序。 如果没有错误,将 DLL 文件(本例中为 moredll.dll)复制到 Windows 系统目录。我们将在下一节测试这个 DLL 文件。
测试我们的 MainDll()
让我们通过执行下面的简单程序来测试生成的 moredll.dll。 这里没有函数调用(导出)。moredll.dll 已经被复制到 C:\WINDOWS/System32 目录下(Windows Xp Pro)。
// File: testdll.cpp, using moredll.dll that uses Dllmain()
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access Dllmain()of moredll.dll.
// No function to be exported, just testing...
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module, moredll.dll...this module has been copied
//to C:\WINDOWS\System32 directory...
hinstLib = LoadLibrary("moredll");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The DLL handle is valid...\n");
ProcAdd =(MYPROC)GetProcAddress(hinstLib, "Anonymfunction");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Ready to execute DLLmain()...
}
else
printf("The function address is not valid, error: % d.\n", GetLastError());
}
else
printf("\nThe DLL handle is NOT valid, error: % d\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK.\n");
return 0;
}
一个示例输出。
错误。127 - 找不到指定的过程。(ERROR_PROC_NOT_FOUND)应该是预期的,因为我们没有在 DLL 程序中定义任何函数。注意,共享内存可以在每个进程中映射到不同的地址。因此,每个进程都有自己的 lpvMem 参数实例,它被声明为一个全局变量,因此它对所有 DLL 函数都可用。本例假设 DLL 全局数据是不共享的,所以每个加载 DLL 的进程都有自己的 lpvMem 实例。
在这个例子中,当最后一个文件映射对象的句柄关闭时,共享内存就会被释放。为了创建持久的共享内存,DLL 可以在第一次加载 DLL 时创建一个分离的进程。如果这个分离的进程使用 DLL 并且没有终止,它就有一个文件映射对象的句柄,防止共享内存被释放。
函数调用公约
每一次函数调用都会有一个栈帧的创建。 如果我们能研究一下函数调用的操作,以及函数的堆栈框架是如何构造和销毁的,是非常有用的。 对于函数的调用,编译器有一些用于调用的约定。 惯例是指一种标准化的,但不是文档化的标准的做事方式。例如,C/C++ 的函数调用惯例告诉编译器这样的事情。
- 函数参数被推到堆栈上的顺序。
- 不管是调用函数还是被调用函数(callee)的责任,在调用结束后将参数从栈中删除,这就是栈清理过程。
- 编译器用来标识各个函数的命名约定。
调用约定的例子有 __stdcall
,__pascal
,__cdecl
和 __fastcall
(对于 Microsoft Visual C++)。 调用约定属于一个函数的签名,因此具有不同调用约定的函数之间是不兼容的。 目前 C/C++ 编译器厂商之间,甚至一个编译器的不同版本之间,对于函数调用方案的命名都没有标准。这就是为什么如果你链接了其他编译器编译的对象文件,可能会产生不一样的命名方案,从而导致外挂无法解决。 对于 Borland 和 Microsoft 编译器,你可以在返回类型和函数名称之间明确指定一个特定的调用约定,如下所示。
void __cdecl TestFunc(float a, char b, char c); // Borland and Microsoft
或者如前面的例子所示,你可以通过 Visual C++/.Net 的设置来实现。对于 GNU GCC 来说,你可以使用__attribute__关键字,在写函数定义时,后面加上关键字__attribute__,然后用双括号说明调用约定,如下图所示。
或者如前面的例子所示,你可以通过 Visual C++/.Net 的设置来实现。对于 GNU GCC 来说,你可以使用__attribute__关键字,在写函数定义的时候,后面跟着关键字__attribute__,然后用双括号说明调用约定,如下所示。
void TestFunc(float a, char b, char c)__attribute__((cdecl)); // GNU GCC
以微软 Visual C++ 编译器为例,其使用的函数调用约定有三种,如下表所示。
关键字 | 堆栈清理 ~~~~~~~~ | 参数传递 |
---|---|---|
__cdecl | 调用者 | 按相反的顺序(从右到左)推送堆栈中的参数。 调用者清理堆栈。 这是支持变量函数(参数数量可变或类型列表,如 printf())的 C 语言以及 C++ 程序的默认调用惯例。 cdecl 调用约定比 __stdcall 创建更大的可执行文件,因为它要求每个函数调用都包含堆栈清理代码。 |
__stdcall | 被调用者 | 也称为 __pascal 。 在堆栈中以相反的顺序(从右到左)推送参数。 使用这种调用惯例的函数需要一个函数原型。 Callee 清理堆栈。 这是 Win32 API 函数中使用的标准约定。 |
__fastcall | 被调用者 | 参数存储在寄存器中,然后推到堆栈上。 __fastcall 调用惯例规定,函数的参数尽可能在寄存器中传递。 Callee 清理堆栈。 |
基本上,C 函数调用时,调用者会将一些参数推送到堆栈上,调用函数,然后弹出堆栈,清理这些推送的参数。 下面以汇编语言中的 __cdecl
为例进行说明。
/*example of __cdecl*/
push arg1
push arg2
call function
add ebp, 12 ;stack cleanup
而对于 __stdcall
的例子。
/*example of __stdcall*/
push arg1
push arg2
call function
/* No stack cleanup, it will be done by caller */
在动态链接库中使用线程本地存储技术
本节介绍了使用 DLL 入口点函数来设置线程本地存储(TLS)索引,为多线程进程的每个线程提供私有存储。入口点函数使用 TlsAlloc()
函数在进程加载 DLL 时分配一个 TLS 索引。每个线程都可以使用这个索引来存储指向自己内存块的指针。当用 DLL_PROCESS_ATTACH
值调用入口点函数时,代码会执行以下操作。
- 使用
TlsAlloc()
函数分配一个 TLS 索引。 - 分配一个内存块给进程的初始线程使用。
- 在调用
TlsSetValue()
函数时使用 TLS 索引来存储分配到内存的指针。
每当进程创建一个新的线程时,都会用 DLL_THREAD_ATTACH 值调用入口点函数。然后,entry-point 函数为新线程分配一个内存块,并通过使用 TLS 索引来存储一个指向它的指针。每个线程都可以在调用 TlsGetValue()
时使用 TLS 索引来检索自己内存块的指针。
当一个线程终止时,用 DLL_THREAD_DETACH 值调用入口点函数,该线程的内存被释放。当一个进程终止时,使用 DLL_PROCESS_DETACH 值调用入口点函数,TLS 索引中指针引用的内存被释放。
TLS 索引被存储在一个全局变量中,使得所有的 DLL 函数都可以使用它。下面的例子假定 DLL 的全局数据是不共享的,因为 TLS 索引对于每个加载 DLL 的进程来说不一定是相同的。这是一个空的 DLL 工程(程序)。
// Project name: moredll, File name: dllntls.cpp, generating moredll.dll
// Using Thread Local Storage in a Dynamic Link Library
#include <windows.h>
#include <stdio.h>
static DWORD dwTlsIndex; //address of shared memory
// DllMain()is the entry-point function for this DLL.
BOOL DllMain(HINSTANCE hinstDLL, // DLL module handle
DWORD fdwReason, //reason called
LPVOID lpvReserved) //reserved
{
LPVOID lpvData;
BOOL fIgnore;
switch(fdwReason)
{
// The DLL is loading due to process
//initialization or a call to LoadLibrary.
case DLL_PROCESS_ATTACH:
printf("Loading the DLL...\n");
// Allocate a TLS index.
if((dwTlsIndex = TlsAlloc())== 0xFFFFFFFF)
return FALSE;
// No break: Initialize the index for first thread.
// The attached process creates a new thread.
case DLL_THREAD_ATTACH:
printf("The attached process creating a new thread...\n");
// Initialize the TLS index for this thread.
lpvData =(LPVOID)LocalAlloc(LPTR, 256);
if(lpvData != NULL)
fIgnore = TlsSetValue(dwTlsIndex, lpvData);
break;
// The thread of the attached process terminates.
case DLL_THREAD_DETACH:
printf("The thread of the attached process terminates...\n");
// Release the allocated memory for this thread.
lpvData = TlsGetValue(dwTlsIndex);
if(lpvData != NULL)
LocalFree((HLOCAL)lpvData);
break;
// DLL unload due to process termination or FreeLibrary.
case DLL_PROCESS_DETACH:
printf("DLL unloading...\n");
// Release the allocated memory for this thread.
lpvData = TlsGetValue(dwTlsIndex);
if(lpvData != NULL)
LocalFree((HLOCAL)lpvData);
// Release the TLS index.
TlsFree(dwTlsIndex);
break;
default:
printf("Reason called not matched, error if any: % d...\n", GetLastError());
break;
}
return TRUE;
UNREFERENCED_PARAMETER(hinstDLL);
UNREFERENCED_PARAMETER(lpvReserved);
}
当一个进程使用该 DLL 的加载时链接时,切入点函数足以管理线程的本地存储。使用运行时链接的进程可能会出现问题,因为在调用 LoadLibrary()
函数之前存在的线程没有调用 entry-point 函数,所以没有为这些线程分配 TLS 内存。下面的例子解决了这个问题,检查 TlsGetValue()
函数返回的值,如果该值表明该线程的 TLS 槽没有设置,则分配内存。
LPVOID lpvData;
// Retrieve a data pointer for the current thread.
lpvData = TlsGetValue(dwTlsIndex);
// If NULL, allocate memory for this thread.
if(lpvData == NULL)
{
lpvData =(LPVOID)LocalAlloc(LPTR, 256);
if(lpvData != NULL)
TlsSetValue(dwTlsIndex, lpvData);
}
将你的项目改为 Release 模式,然后重建 DLL 程序。 如果没有错误,将 DLL 文件(本例中为 moredll.dll)复制到 Windows 系统目录下。我们将在下一节测试该 DLL 文件。
测试 DLL 程序
以前面的测试程序为例,让我们测试一下 DLL 程序。
// File: testdll.cpp, using moredll.dll that uses Dllmain()
// Using Run-Time Dynamic Linking
// A simple program that uses LoadLibrary()and
// GetProcAddress()to access Dllmain()of moredll.dll.
// No function to be exported/imported, just testing...
#include <stdio.h>
#include <windows.h>
typedefvoid(*MYPROC)(LPTSTR);
int main()
{
HINSTANCE hinstLib;
MYPROC ProcAdd;
BOOL fFreeResult, fRunTimeLinkSuccess = FALSE;
// Get a handle to our DLL module, moredll.dll...this module has been copied
//to C:\WINDOWS\System32 directory...
hinstLib = LoadLibrary("moredll");
// If the handle is valid, try to get the function address.
if(hinstLib != NULL)
{
printf("The DLL handle is valid...\n");
ProcAdd =(MYPROC)GetProcAddress(hinstLib, "Anonymfunction");
// If the function address is valid, call the function.
if(ProcAdd != NULL)
{
printf("The function address is valid...\n\n");
fRunTimeLinkSuccess = TRUE;
// Ready to execute DllMain()...
}
else
printf("The function address is not valid, error: % d.\n", GetLastError());
}
else
printf("\nThe DLL handle is NOT valid, error: % d\n", GetLastError());
// Free the DLL module.
fFreeResult = FreeLibrary(hinstLib);
if(fFreeResult != 0)
printf("FreeLibrary()is OK.\n");
else
printf("FreeLibrary()is not OK.\n");
return 0;
}
一个输出示例。
好吧,我们的 DLL 程序看起来不错。这一切的人。享受你的 C / C++ 之旅吧
动态链接库参考
以下元素用于动态链接。
功能 | 说明 |
---|---|
DisableThreadLibraryCalls() | 禁用指定 DLL 的线程附加和线程分离通知。 |
DllMain() | 一个进入 DLL 的可选入口点。 |
FreeLibrary() | 减少加载的 DLL 的引用次数。当引用数达到零时,模块将从调用进程的地址空间中解映射。 |
FreeLibraryAndExitThread() | 将加载的 DLL 的引用数减一,然后调用 ExitThread()来终止调用线程。 |
GetDllDirectory() | 检索用于查找应用程序的 DLLs 的搜索路径的应用程序特定部分。 |
GetModuleFileName() | 检索包含指定模块的文件的完全限定路径。 |
GetModuleFileNameEx() | 检索包含指定模块的文件的完全限定路径。 |
GetModuleHandle() | 读取指定模块的模块句柄。 |
GetModuleHandleEx() | 读取指定模块的模块句柄。 |
GetProcAddress() | 从指定的 DLL 中检索导出的函数或变量的地址。 |
LoadLibrary() | 将指定的可执行模块映射到调用进程的地址空间。 |
LoadLibraryEx() | 将指定的可执行模块映射到调用进程的地址空间。 |
SetDllDirectory() | 修改用于查找应用程序的 DLL 的搜索路径。 |
过时的功能
LoadModule()
函数只是为了兼容 16 位版本的 Windows 而提供的。
进一步的阅读和挖掘
- Microsoft Visual C++,在线 MSDN。
- 结构、枚举、联合和 typedef 故事可以参考 C/C++ 结构、枚举、联合和 typedef。
- 多字节、Unicode 字符和本地化请参考 Locale、宽字符和 Unicode(Story)和 Windows 用户与组编程教程(Implementation)。
- Windows 数据类型是 Windows 数据类型。
- 查看 Amazon 上最畅销的 C / C++ 和 Windows 书籍。
链接库(动态链接库,静态链接库)
红渐
2022-03-20
阅读并整理了一些网上的资料,做个记录
库最基本的概述:是共享代码的方法
链接库是什么
库: 专门存储可重复使用的代码块的文件
但是一般不会直接分享源代码,而是分享库的二进制版本 ——链接库
一个目标文件中使用的函数或变量,可能定义在其他的目标文件中,也可能定义在某个链接库文件中。链接器完成完成链接工作的方式有两种,分别是:
- 无论缺失的地址位于其它目标文件还是链接库,链接库都会逐个找到各目标文件中缺失的地址。采用此链接方式生成的可执行文件,可以独立载入内存运行;
- 链接器先从所有目标文件中找到部分缺失的地址,然后将所有目标文件组织成一个可执行文件。如此生成的可执行文件,仍缺失部分函数和变量的地址,待文件执行时,需连同所有的链接库文件一起载入内存,再由链接器完成剩余的地址修复工作,才能正常执行。
第一种链接方式为静态链接;第二种,链接所有目标文件也为静态链接,但是在内存中链接属于动态链接
静态链接库
有很多缺点:
-
文件体积很大
-
如果存在多个可执行文件,都链接同一个静态库,那么每个可执行文件都要全量链接该静态库,造成资源浪费
-
一旦程序中有模块更新,那么都要重新链接才能运行
动态链接库
在 Linux 上习惯称为共享库、共享对象文件
动态链接:将链接的时机推迟到程序运行时再进行
1.动态链接库和可执行文件是分别载入内存的
2.多个程序用同一个动态链接库时,所有程序可共享一份动态链接库,避免空间浪费
- 方便了程序的更新,如果程序有一个模块更新,那么只需要替换旧的模块,然后动态链接在一起
但是,每次程序运行都需重新链接,比静态链接库相比损失了大概 5% 的性能
后缀:linux 中为 .so,windows 中为 .dll
gcc -shared -fpic -o libvector.so addvec.c mulvec.c
gcc -o prog2 main.c ./libvector.so// 通过共享库构建可执行文件
-shared: 指定创建共享库
-fpic: 共享库可以加载到内存任意位置
libvector.so
的代码没有真的被复制到 prog2 中,而静态库就会。
执行过程中,加载器会发现 prog2 中存在一个 名为 .interp
的 section,其包含了动态链接器的路径名
实际上,动态链接器也是一个 so 文件,ld-linux.so。被加载器加载到内存中运行,然后动态链接器执行重定位代码和数据的工作
具体的执行过程:
-
重定位 libc.so(C 语言函数库) 到一个位置
-
重定位 libvector.so到另一个位置
-
重定位 prog2 中的那些 定义于 libc.so libvector.so 的引用符号
如何使用(动态)共享库
-dlopen:可以打开一个共享库文件,RTLD_LAZY 代表,当共享库中函数执行时再进行符号解析
-dlsym:handle 是共享库文件的句柄,symbol 是符号名。返回符号的地址
-dlclose:卸载共享库
via:
-
Windows 动态链接库 - DLL 第一部分:方案实例 - 掘金
https://juejin/post/6932276742551765005 -
Windows 动态链接库 - DLL 第二部分:方案实例 - 掘金
https://juejin/post/6932333618983337992 -
Windows 动态链接库 - DLL 第三部分:方案实例 - 掘金
https://juejin/post/6932337818014482439 -
Windows 动态链接库 - DLL 第四部分:方案实例 - 掘金
https://juejin/post/6932342156229279757 -
链接库(动态链接库,静态链接库)库最基本的概述:是共享代码的方法 链接库是什么 库 - 掘金 2022-03-20
https://juejin/post/7077090970159808543