动态链接库(DLL)
什么叫库?
库是写好的,现有的,成熟的,可以复用的代码。本质上来说库是一种可执行代码的二进制形式,可以被操作系统装载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。
所谓静态、动态是指链接过程 。将一个程序编译成可执行程序的步骤:
静态库
【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o/.obj与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库特点总结:
静态库对函数库的链接是放在编译时期完成的。
程序在运行时与函数库再无瓜葛,移植方便。
浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
对程序的更新、部署和发布困难。如果静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于客户来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库
而针对静态库自身的缺点:内存和磁盘浪费及程序更新和发布困难,便有了动态链接库,其主要思想:简单的讲,就是不对那些组成程序的目标文件进行链接,而是等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到了运行时再进行。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
Windows下动态库
动态链接库(dynamic-link library,DLL),Windows应用程序编程接口(API)提供的所有函数都包含在DLL中。其中三个最重要的DLL分别是:Kernel32.dll,包含的函数用来管理内存、进程以及线程;User32.dll,包含的函数用来执行与用户界面相关的任务,如创建窗口和发送消息;GDI32.dll,包含的函数用来绘制图像和显示文字。著名的ActiveX技术也是基于这种运行时加载机制实现的。
Winows下的DLL文件和EXE文件实际上是一个概念,它们都是PE格式的二进制文件,稍微有些不同的是PE文件头部中有个符号位表示该文件是EXE或是DLL。需要通过在链接开关进行控制,来告诉编译器编译成DLL或者EXE。
DLL通常由一组可供任何应用程序使用的独立函数组成。在DLL中,通常没有用来处理消息循环或创建窗口的代码。DLL只不过是一组源代码模块,每个模块包含一些可供应用程序(可执行文件)或其他DLL调用的函数。
构建DLL需要的步骤
(1) 必须先创建一个头文件,其中包含我们想要在DLL中导出的函数原型、结构以及符号。在构建可执行文件的时候需要用到同一个头文件。
需要显示地“告诉”编译器我们要导出某个符号,否则编译器默认所有符号都不导出。Microsoft Visual C++(MSVC)编译器提供了一些列C/C++的扩展来指定符号的导入和导出。我们可以通过”__declspec”属性关键字来修饰某个函数或者变量,当使用”__declspec(dllexport)”时表示该符号是从本DLL导出的符号,“__declspec(dllimport)”表示该符号是从别的DLL导入的符号。
在C++中,如果你希望导出或导入的符号符合C语言的符号修饰规范,那么必须在这个符号的定义之前加上extern “C”,以防止C++编译器进行符号修饰。
除了使用”__declspec”扩展关键字指定导入导出符号外,我们也可以使用“.def”文件来声明导入导出符号。“.def”文件中的IMPORT或者EXPORTS段可以用来声明导入导出符号。
(2)创建C/C++源文件来实现想要在DLL模块中导出的函数和变量。由于在构建可执行模块的时候不需要这些源文件,因此创建该DLL的公司可以将这些源代码作为公司的机密。
(3)在构建该DLL模块的时候,编译器会对每个源文件进行处理并产生一个.obj模块(每个源文件对应一个.obj模块)。
(4)当所有.obj模块都创建完毕后,链接器会将所有.obj模块的内容合并起来,产生一个单独的DLL映像文件。为了执行可执行模块,这个文件时必须的。
(5)如果链接器检测到DLL的源文件输出了至少一个函数或变量,那么链接器还会产生一个.lib文件。这个.lib文件非常小,它只是列出了所有被导出的函数和变量的符号名。为了构建可执行模块,这个文件时必须的。
.lib文件只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
一旦创建了DLL模块,我们将可以通过下列步骤来构建可执行模块:
(1)在所有引用了导出的函数、变量、数据结构或符号的源文件中,必须包含由DLL的开发人员所创建的头文件。
(2)创建C/C++源文件来实现想要包含在可执行模块中的函数和变量。当然,代码可以引用在DLL的头文件中定义的函数和变量。
(3)在构建可执行模块的时候,编译器会对每个源文件进行处理并产生一个.obj模块(每个源文件对应一个.obj模块)。
(4)链接器将所有.obj模块的内容合并起来,产生一个单独的可执行映像文件。这个映像文件包含了可执行文件中所有的二进制代码以及全局/静态变量。该可执行模块还包含一个导入段(import section),其中列出了所有它需要的DLL模块的名称。
一旦DLL和可执行模块都已构建完毕,进程就可以执行了。当我们试图运行可执行模块的时候,操作系统的加载程序会执行下面的步骤。
(5)加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行模块的导入段。对导入段中列出的每个DLL,加载程序会在用户的系统中对该DLL模块进行定位,并将该DLL映射到进程的地址空间中。注意,由于DLL模块可以从其它DLL模块中导入函数和变量,因此DLL模块可能有自己的导入段并需要将它所需的DLL模块映射到进程的地址空间中。
创建\使用动态链接库
在应用程序(或其他DLL)能够调用一个DLL中的函数之前,必须将该DLL的文件映像映射到调用进程的地址空间中。一般的两种方式是:隐式载入时连接 (implicit load-time linking)或显示运行时链接 (explicit run-time linking)。
代码如下:
1. DLL文件:
/*********************************************************************
MyDLLTest.h *************************************************************************/
#pragma once
#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);
MYLIBAPI int Sub(int a, int b);
MYLIBAPI int Mul(int a, int b);
/*********************************************************************
MyDLLTest.cpp
*************************************************************************/
#include <windows.h>
#define _EXPORTDLL //需要在#include "MyDLLTest.h" 前面定义
#include "MyDLLTest.h"
int g_nResult;
int Add(int a, int b)
{
g_nResult = a + b;
return g_nResult;
}
int Sub(int a, int b)
{
g_nResult = a - b;
return g_nResult;
}
int Mul(int a, int b)
{
g_nResult = a * b;
return g_nResult;
}
DLL文件编译完成后可以利用VS的DUMPBIN工具查看导出函数:
查看.Lib文件的相关信息
查看.dll文件的相关信息
- EXE文件
- 隐式载入时连接 (implicit load-time linking)
静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时DLL的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该DLL,则Windows对DLL的应用记录减,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。
静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就像调用程序内部的其他函数一样。
(必须把DLL工程的.h, .lib, .dll文件放在EXE工程文件夹下):
#include "stdafx.h"
#include <windows.h>
#include <iostream>
#include "MyDLLTest.h" //包含dll的头文件
//在应用程序里,.lib文件将作为DLL的替代文件参与编译
//告诉编译器与DLL相对应的.lib文件所在的路径及文件名
#pragma comment(lib, "../Debug/DLLTest_NOT_MFC.lib")//链接.lib文件(显示调用不需要连接.lib文件)
// 设置lib文件包含路径
// 工程上右击,选择“属性”,打开工作属性页
// A“配置属性”-->“连接器”-->“常规”-->“附加库目录”, 注意这只是添加了目录而已
// B“配置属性”-->“连接器”-->“输入”-->“附加依赖项”,
// 这里指定了本工程要链接的库,等同于#pragma comment(lib, "xx.lib")
int main(void)
{
int a = Add(1,2);
//可以像平常一样调用函数和变量
std::cout << "Add() =" << a << std::endl;
std::cout << "g_nResult" << g_nResult << std::endl;
Sleep(10000);
}
EXE文件运行结果:
- 显示运行时链接 (explicit run-time linking)
可以让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。当程序需要用到某个插件或者驱动的时候,才将相应的模块装载进来,而不需要从一开始就将他们全部装载进来,从而减少了程序启动时间和内存的使用。
#include <windows.h>
#include <iostream>
//显示调用DLL (运行时加载)
//需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。
typedef int(*lpAddFun)(int, int); //宏定义函数指针类型
int main(int argc, char *argv[])
{
HINSTANCE hDll; //DLL句柄
lpAddFun addFun; //函数指针
//进程中的每个DLL模块被全局唯一的字节的HINSTANCE句柄标识,只有
//在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地址
hDll = LoadLibrary("../Debug/DLLTest_NOT_MFC.dll");
//(用来装载一个DLL到进程的地址空间)
if (hDll != NULL)
{
//(用来查找某个符号的地址)
//addFun = (lpAddFun)GetProcAddress(hDll, "Add");
//MAKEINTRESOURCE通过序号取得函数名的宏
//直接使用导出文件中的序号
addFun = (lpAddFun)GetProcAddress(hDll,
MAKEINTRESOURCE(1));
if (addFun != NULL)
{
int result = addFun(2, 3);
std::cout << "addFun(2, 3) = " << result << std::endl;
Sleep(10000);
}
FreeLibrary(hDll); //(用来卸载某个已加载的模块)
}
return 0;
}