1.DLL的两种链接方式:
在使用DLL前,程序必先加载DLL程序(将DLL映射到进程的地址空间)。有如下两种方式加载DLL程序:
(1)隐式链接
(2)显示链接
2.静态与动态CRT库
一个进程的地址空间是由一个可执行程序模块和多个DLL模块组成的。
这些模块有的链接的是CRT静态库,有的链接的是CRT动态库(DLL),有的没有链接CRT.
这将导致在进程地址空间中存在多个CRT的副本(由CRT静态库导致)
在这种情况下,程序运行可能会产生问题,如下:
//DLL A中的函数
VOID EXEFunc()
{
PVOID pv = AllocMem();//调用DLL B中的函数
free(pv);//这里可能会出问题
}
//DLL B中的函数
PVOID AllocMem()
{
return(malloc(100));
}
DLL B中提供了一个函数AllocMem,该函数功能是分配一块内存,并将所得内存的地址返回。
DLL A调用DLL B中的AllocMem函数获取内存,然后调用free去释放所得内存。在free的时候可能会产生问题。
解决办法如下有以下两种:
(1)所有DLL模块(不引用CRT的DLL模块除外)和可执行程序模块都只引用动态库。
(2)DLL B再提供一个释放内存的函数FreeMem,让DLL A调用FreeMem来释放内存。
即保证同一模块中若提供了内存分配的函数的同时也要提供释放内存的函数
//修改后的DLL A
VOID EXEFunc()
{
PVOID pv = AllocMem ();
FreeMem (pv);
}
//在DLL B中新加一个函数
BOOL FreeMem(PVOID pv)
{
return(free(pv));
}
3.构造DLL
(1)仅导出函数
DLL可以导出全局变量和类,但我们不建议这么做,建议导出函数。
(2).lib
每个DLL都有与之相对应的.lib文件,该文件中列出了DLL中导出的函数和变量的符号名
(3)指定要导出的函数名
因为不同编译器的Name mangle规则不同,这就导致DLL不能跨编译器使用。
有以下两种方法可以解决这个问题:
1.在.def文件中指定要导出的函数名
2.在编译指中指定要导出的函数名:
#pragma comment(linker, "/export:MyFunc=_MyFunc@8")
4.DLL加载路径
当需要加载一个DLL时,系统会依照下面的顺序去寻找所需DLL直到找到为止,然后加载,否则加载失败。
(1)当前可执行文件路径
(2)GetWindowsDirectory返回的Windows系统路径
(3)16位系统的路径 windows"system
(4)GetSystemDirectory返回的Windows系统路径
(5)当前进程所在路径
(6)PATH环境中所指定的路径
5.Common API:
LoadLibrary LoadLibraryEx
GetModuleFileName GetModuleHandle GetProcAddress
FreeLibrary FreeLibraryAndExitThread
1. Kernel32.dll-管理内存、线程、进程。User32.dll-窗口和消息。Gdi32.dll-绘制图像文字。ComDlg32.dll-常用对话框。ComCtl32.dll-常用控件。
2. DLL函数分配的内存应该由DLL自己提供的函数释放:主要是针对通过C/C++函数(malloc、new)分配的内存,因为当DLL和DLL的使用者都在引用静态库版本的CRT时(或有一方在引用静态库CRT),多个静态库版CRT中有多份CRT堆的管理数据(全局变量),如果从一个管理器分配资源交给另一个管理器释放,显然会错误。因此,如果所有模块都使用DLL版CRT就不会有错(因为只有一份全局CRT堆管理数据),或者改用HeapAlloc(GetProcessHeap(),…)也不会错(显然DLL中和EXE中访问到的默认堆是同一个),当然最佳做法还是DLL同时提供匹配的释放函数。
3. .lib文件中只包含函数、变量和类型的符号名。由于模块中只包含要引用的模块名而没有路径,所以主模块被载入后需要按一定的搜索顺序搜索被引用模块再载入,同时这也意味着修改.lib中的符号名,搜索DLL时也会搜索新名称。
4. DLL的导出段中按符号名顺序列出了导出项,每一项包括符号名和RVA(Relative Virtual Address,用于指出该符号在DLL模块中相对于模块基址的地址)。模块可以包含多个导入段,每个导入段指出该段要依赖的DLL名以及需要的符号,导入符号对应的实际地址在DLL被载入后填充,其值为DLL基址+RVA。
5. 在为DLL的导出函数指定名称的时候,最好使用.def文件,其次可以选择链接选项#pragma comment(linker, “/export:MyFunc=_MyFunc@”)。
6. dumpbin.exe的/exports能够查看导出段,/imports能够查看导入段。
7. 关于MSVC编译器对符号改名的策略:C语言下默认不改变函数名,因此C++下使用了extern “C”的__cdecl也不会改名。
.DLL和进程的地址空间
创建DLL通常比创建应用程序容易,因为DLL通常由一组可供任何应用程序使用的独立函数组成。在DLL中,通常没有用来处理消息循环或创建窗口的代码。 DLL只不过是一组源代码模块,每个模块包含一些可供应用程序(可执行文件)或其他DLL调用的函数。
在应用程序(或其他DLL)能够调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中。我们可以通过两种方法来达到这一目的:显示运行时链接 (explicit run-time linking)。
一旦系统将一个DLL的文件映像映射到调用进程的地址空间中之后,进程中的所有线程就可以调用该DLL种的函数了。事实上,该DLL几乎完全丧失了它的DLL身份:对进程中的线程来说,该DLL中的代码和数据就像是一些附加的代码和数据,碰巧被放在进程地址空间中。
注意:当一个模块提供一个内存分配函数的时候,它必须同时提供另一个用来释放内存的函数。
例如:
VOID EXEFunc()
{
PVOID pv = DLLFunc();
……
DLLFreeFunc(pv);
}
PVOID DLLFunc()
{
PVOID pv = malloc(100);
return (pv);
}
BOOL DLLFreeFunc(PVOID pv)
{
return(free(pv));
}
构建DLL需要的步骤
(1)必须先创建一个头文件,在其中包含我们想要在DLL中导出的函数原型、结构以及符号。在构建可执行文件的时候需要用到同一个头文件。
(2)创建C/C++源文件来实现想要在DLL模块中导出的函数和变量。由于在构建可执行模块的时候不需要这些源文件,因此创建该DLL得公司可以将这些源代码作为公司的机密。
(3)在构建该DLL模块的时候,编译器会对每个源文件进行处理并产生一个.obj模块(每个源文件对应一个.obj模块)。
(4)当所有.obj模块都创建完毕后,连接器会将所有.obj模块的内容合并起来,产生一个单独的DLL映像文件。这个映像文件包含DLL中所有的二进制代码以及全局静态变量。为了执行可执行模块,这个文件时必须的。
(5)如果链接器检测到DLL的源文件输出了至少一个函数或变量,那么链接器还会产生一个.lib文件。这个.lib文件非常小,这是因为它并不包含任何函数或变量。它只是列出了所有被导出的函数和变量的符号名。为了构建可执行模块,这个文件时必须的。
模块,我们将可以通过下列步骤来构建可执行模块:
(1)在所有引用了导出的函数、变量、数据结构或符号的源文件中,必须包含由DLL的开发人员所创建的头文件。
(2)创建C/C++源文件来实现想要包含在可执行模块中的函数和变量。当然,代码可以引用在DLL的头文件中定义的函数和变量。
(3)在构建可执行模块的时候,编译器会对每个源文件进行处理并产生一个.obj模块(每个源文件对应一个.obj模块)。
(4)链接器将所有.obj模块的内容合并起来,产生一个单独的可执行映像文件。这个映像文件包含了可执行文件中所有的二进制代码以及全局/静态变量。该可执行模块还包含一个导入段(import section),其中列出了所有它需要的DLL模块的名称。
一旦DLL和可执行模块都已构建完毕,进程就可以执行了。当我们试图运行可执行模块的时候,操作系统的加载程序会执行下面的步骤。
(5)加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行模块的导入段。对导入段中列出的每个DLL,加载程序会在用户的系统中对该DLL模块进行定位,并将该DLL映射到进程的地址空间中。注意,由于DLL模块可以从其它DLL模块中导入函数和变量,因此DLL模块可能有自己的导入段并需要将它所需的DLL模块映射到进程的地址空间中。
隐式调用DLL
代码如下:
1. DLL文件:
/*********************************************************************
Mylib.h
*************************************************************************/
#ifdef _EXPORTDLL
#define MYLIBAPI extern "C" __declspec(dllexport)
#else
#define MYLIBAPI extern "C" __declspec(dllimport)
#endif
MYLIBAPI int g_nResult;
MYLIBAPI int Add(int a, int b);
/*********************************************************************
Mylib.cpp
*************************************************************************/
#include <windows.h>
#define _EXPORTDLL //需要在#include "Mylib.h" 前面定义
#include "Mylib.h"
int g_nResult;
int Add(int a, int b)
{
g_nResult = a + b;
return g_nResult;
}
DLL文件编译完成后可以利用VS的DUMPBIN工具查看导出函数:
2. EXE文件(必须把DLL工程的.h, .lib, .dll文件放在EXE工程文件夹下):
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include "Mylib.h" //包含Dll的头文件
#pragma comment(lib,"Mylib.lib") //链接.lib文件
int main(void)
{
int a = Add(1,2);
printf("Add() = %d\n", a); //可以像平常一样调用函数和变量
printf("g_nResult = %d\n", g_nResult);
Sleep(10000);
}
EXE文件运行结果:
由于导入段只包含DLL的名称,不包含DLL的路径,因此加载程序必须在用户磁盘上搜索DLL。下面是夹在程序的搜索顺序:
- 包含可执行文件的目录;
- Windows的系统目录,该目录可以通过GetSystemDirectory得到;
- 16位的系统目录,即Windows目录中的System子目录;
- Windows目录,该目录可以通过GetWindowsDirectory得到;
- 进程的当前目录;
- PATH环境变量中所列出的目录。