第19章 动态链接库基础

19.1 DLL和进程的地址空间

(1)DLL的优缺点

  ①有利于节省内存,多个进程能同时使用一个DLL,即在内存中共享DLL的单个拷贝,这节省了内存并减少了文件交换

  ②促进了资源的共享,DLL里能够包含诸如对话框模版、字符串、图标以及位图之烊的资源。多个应用程序可以使用DLL来共享这些资源。

  ③只要函数的参数和返回值不变,DLL函数实现可以改变而不必重新编译和链接整个应用。(这对于导出的C++类发生改变时一般行不能,这个问题被称为DLL Hell问题)

  ④只要程序遵循函数的调用约定,用不同语言编写的程序都能调用同一个DLL。

  ⑤可以用于特殊目的,Windows提供的某些特性只有DLL才能使用。如钩子、ActiveX控件都必须被存放在DLL中。

  ⑥使用DLL的一个缺点是应用程序不是自包含的(即该应用程序不是自身包含所需的所有代码(或组件)),而是依赖于一个分离的DLL模块,若该DLL不存在,那么进程就无法执行。

(2)地址空间

  ①可以通过隐式或显式链接将DLL文件映射到调用进程的地址空间。

  ②一旦映射成功,进程中的所有线程就可以调用该DLL中的函数了。当线程调用DLL中的一个函数的时候,该函数的参数和局部变量会被存放在线程栈中。

  ③DLL被不同的进程所加载时,DLL中的全局变量和局部变量,当被不同的进程所加载,会与同一个可执行文件的多个实例一样,通过写时复制来保证变量的实例为进程私有的数据

  ④在DLL中的函数创建的任何对象都为调用线程或调用进程(而不是DLL)所拥有。如在DLL中调用VirtualAlloc,系统会从调用进程的地址空间预订区域。如果稍后DLL被撤销映射,那么这块地址空间仍为预订状态(即该区域虽为DLL中的函数所预订,但却为进程所拥有)。只有当线程调用了VirtualFree或进程终止时,该区域才会释放。

(3)常见的错误及分析

①错误代码

VOID EXEFunc(){
    PVOID pv = DLLFunc();
//访问pv内存块的内容 //假定pv是从EXE的C/C++运行时堆创建的 free(pv); } PVOID DLLFunc(){ //从Dll的C/C++运行时堆中分配内存块 return (malloc(100)); }

②问题:DLL中的函数分配的内存块,能否为EXE函数所释放?

  A、如果EXE和DLL都链接到C/C++运行库的DLL版本,则代码正常工作

原因:如果C/C++运行库是静态版的,那么代码会被分别链接进EXE和DLL模块中,成为这两个模块内部一份单独的运行库代码(注意,本质上在内存中是两份代码,所以这些代码不能为EXE和DLL所共享)。这时如果在EXE和DLL中分别调用malloc函数,实际上是在两个不同的堆中分配内存。因此,如果在EXE中调用free函数并传入的是DLL中那个堆的内存地址,就会失败

  B、如果两个模块或其中一个链接到C/C++运行库的静态版本,则Free调用失败

原因:如果两个模块都是加载C/C++运行库的DLL版本,由于一个进程不同模块共享运行库代码,这里不管在EXE模块还是DLL模块调用malloc或free函数,都是在同一个堆中操作内存的,所以调用会成功。

③改进:在DLL中提供分配内存和释放内存的操作

BOOL DLLFreeFunc(PVOID pv){

    //从DLL的C/C++运行时堆中释放内存块

    return (free(pv));
}

19.2 纵观全局

19.2.1 构建DLL模块

(1)构建DLL模块的说明

  ①在构建DLL模块时,编译器为每个源文件产生一个.obj模块。当所有.obj模块都创建完毕后,链接器会将所有的.obj模块合并起来并产生一个单独的DLL文件。

  ②如果链接器检测到DLL的源文件输出了至少一个函数或变量,那么链接器还会生成一个.lib文件,这个文件并不包含任何函数或变量,只是列出了所有被导出的函数或变量的的名称——为构建可执行模块时,隐式链接DLL时使用。

  ③加载程序为新的进程创建虚拟地址空间,并将可执行模块映射到新进程的地址空间。加载程序接着解析可执行模块的导入段。对导入段列出的每个DLL,加载程序会对其进行定位,并将其映射到进程的地址空间。如果DLL模块还有自己的导入段,则会将它所需的DLL模块映射到进程的地址空间。

(2)DLL导出定义的方法及导出的3类接口

  ①扩展语法方法:使用关键字__declspec(dllexport)——适合导出变量、函数、C++类

接口类型

导出语法(DLL开发者)

导入语法(DLL调用者)

变量

__declspec(dllexport) int g_iVal

__declspec(dllimport) int g_iVal

函数

__declspec(dllexport) int Fun(int)

__declspec(dllimport) int Fun(int)

class __declspec(dllexport)

CObject{…};

class __declspec(dllimport)

CObject{…};

备注

①导出函数默认是C风格的,若需要在C++代码中使用,则需要使用extern "C"{...};

②导出类,不会改变类成员的访问属性,如原来是protected的成员仍然是protected成员。之所以这些成员还要导出,是为了方便从DLL导出类来派生自己的类。

可以导出类的某几个成员,而不是导出全部成员,此时将导出扩展关键字放到相应的成员前即可,规则同普通的变量和函数导出一样(但不提倡这样做,因为如果是非静态成员,会因只导出成员而类没被导出,所以不能使用。如果是静态成员,这与导出一个普通函数毫无区别,所而会使调用方代码书写会过于啰嗦)

  【ExportClassDLL】演示导出类和函数的例子

 //ExportClassDll.h

//下列的ifdef块是创建一个宏,是使从DLL导出更简单的一种标准的方法
//在使用此DLL的任何其他项目上不应定义此符号。这样,源文件包含此文件的
//任何其他项目都会将被SAMLPLEDLL_API修饰的函数视为从DLL中导入的,而此DLL
//则将用此宏定义符号视为被导出的
#pragma once

#ifdef SAMPLEDLL_EXPORTS
#define SAMPLEDLL_API  __declspec(dllexport)
#else
#define SAMPLEDLL_API  __declspec(dllimport)
#endif

//导出类
class SAMPLEDLL_API CSampleDll{
public:
    CSampleDll(void); //构造函数

public:
    int Sum(int a, int b);
};

//导出变量(应尽量避免!)
extern  SAMPLEDLL_API int  nSampleDll;

//导出函数
SAMPLEDLL_API int Multiply(int, int);

//ExportClassDll.cpp

//DLL模块中必须先定义此宏,以便将SAMPLEDLL_API 定义为dllexport
#define SAMPLEDLL_EXPORTS

#include <windows.h>
#include "ExportClassDll.h"

//导出变量
int nSampleDll = 10;

//导出函数
int Multiply(int a, int b)
{
    return a * b;
}

//导出类
CSampleDll::CSampleDll(void){
    return;
}

int CSampleDll::Sum(int a, int b)
{
    return a + b;
}

//DLL的入口函数
BOOL APIENTRY DllMain(HMODULE HMODULE, DWORD dwReason, LPVOID lpReserved){
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

//ExportClassExe:DLL测试程序

#include <stdio.h>
#include <tchar.h>
#include "../19_ExportClassDll/ExportClassDll.h"

#pragma  comment(lib,"../../Debug/19_ExportClassDll.lib")

int _tmain()
{
    int a = 8;
    int b = 5;

    _tprintf(_T("a = %d, b = %d
"), a,b);

    //使用DLL中导出的类
    CSampleDll sample;
    _tprintf(_T("CSampleDll::Sum(a,b) = %d
"), sample.Sum(a, b));


    //使用DLL中导出的函数
    _tprintf(_T("Multiply(a,b) = %d
"), Multiply(a, b));
    
    //使用DLL中的变量
    //注意:此处的nSampleDll被声明为extern __declspec(dllimport) int  nSampleDll,所
    //以导入的nSampleDll是变量本身,如果被声明为extern int nSampleDll,则导入的是变量的
    //地址,这里需要特别注意,当导入变量地址时,则下列语句须用*(int*)nSampleDll来访问这
    //个变量。
    _tprintf(_T("old 'nSampleDll' value = %d
"),nSampleDll);

    nSampleDll = 100;
    _tprintf(_T("new 'nSampleDll' value = %d
"), nSampleDll);

    _tsystem(_T("PAUSE"));

    return 0;
}

  ②模块定义文件方法:定义一个扩展名为def的文件,在其中说明DLL中要导出的接口(但这种方法只能导出变量和函数)

  A、编译器使用模块定义文件建立输入库(*.lib)文件和输出库(*.exp)文件

  B、链接程序使用DLL的输入库(*.lib)产生所有使用DLL的可执行模块(隐式链接时)

  C、链接器使用输出文件(*.exp)产生最终的.dll文件

  D、设置链接时依赖文件的方法:“项目属性”→“配置属性”→“链接器”→“输入”→“模块定义文件”→输入“XXX.def”(不包含引号)

【文件格式】

;模块定义文件里的注释是用分号的

; 必须包含一条LIBRARY和EXPORTS语句

LIBRARY "ExportUseDefFile"      ; ExportUseDefFile为动态链接库的名称

EXPORTS

   Func_A @1             ;列出要导出的函数和序号,从1开始。@1为序号

   Func_B @2

   Func_C @3

 【ExportUseDef程序】测试使用模块定义文件导出DLL中的函数

//ExportUseDef.cpp

#include <windows.h>
#include <strsafe.h>

BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
{
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

int Func_A(int iVal){
    return iVal;
}

int Func_B(int iVal1, int iVal2)
{
    return iVal1 + iVal2;
}

int Func_C(int iVal1, int iVal2, int iVal3)
{
    return iVal1 + iVal2 + iVal3;
}

//ExportUseDef.def文件

;模块定义文件里的注释是用分号的
; 必须包含一条LIBRARY和EXPORTS语句

LIBRARY "19_ExportUseDef"  ; 19_ExportUseDef为动态链接库的名称

EXPORTS

   Func_A @1             ;列出要导出的函数和序号,从1开始。@1为序号

   Func_B @2

   Func_C @3

//TestExportUseDef:测试文件

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>

#pragma  comment(lib,"../../Debug/19_ExportUseDef.lib")
__declspec(dllimport) int Func_A(int iVal1);
__declspec(dllimport) int Func_B(int iVal1, int iVal2);
__declspec(dllimport) int Func_C(int iVal1, int iVal2, int iVal3);

int _tmain(){
    int iVal1 = 10;
    int iVal2 = 20;
    int iVal3 = 30;
    _tprintf(_T("iVal1=%d, iVal2=%d, iVal3=%d
"), iVal1, iVal2, iVal3);
    _tprintf(_T("Func_A(iVal1)=%d
"), Func_A(iVal1));
    _tprintf(_T("Func_B(iVal1,iVal2)=%d
"), Func_B(iVal1, iVal2));
    _tprintf(_T("Func_C(iVal1,iVal2,iVal3)=%d
"), Func_C(iVal1, iVal2, iVal3));

    _tsystem(_T("PAUSE"));
    return 0;
}

 (3)动态链接库导出函数名称问题

  ①DLL导出函数名称的关系图

 

  ②extern "C":C++编译器会对函数名和变量名进行改编(mangle)。使用这个修饰符用来告诉编译器不要进行改编而是以C方式来导出函数或变量名。这个修饰符一般用在混合使用C和C++进行编程的时候。

  ③由于名称的改编,即使完全使用C来编程,但因VC 中默认调用是 __cdecl 方式,而Windows API 使用 __stdcall 调用方式。如果在 DLL 导出函数中,为了跟 Windows API 保持一致而使用 __stdcall方式的话,则函数的名称依然会被改编(即使函数前用extern  "C"修饰,注意一般不这样用,因为这个修饰符是用在C++中的)。这时有两种处理方法:一种是通过模块定义文件;还有一种方法是通过#pragma comment(linker, "/export:MyFunc=_MyFunc@08")之类的链接器指示符。

  ④__declspect(dllexport):当VS的C/C++编译器看到被这个修饰符修饰的变量、函数或C++类时,会在生成的.obj中嵌入一些额外信息,当链接器在链接DLL所有的.obj时,会根据这些信息,生成一个.lib文件,这个文件列出该DLL导出的符号。除了生成这个.lib文件外,链接器还会在生成的DLL文件中嵌入一个导出符号表(导出段),其中列出了导出的变量、函数和类的符号名,并保存其相对虚拟地址。

19.2.2 构建可执行模块

(1)隐式链接时,必须包含DLL的头文件,如#include "MyLib.h",要从DLL中导入的符号(变量、函数或C++类)前要用__declspec(dllimport)关键字修饰。但也可以直接用标准C语言的extern关键字。但如果编译器知道我们要引号的符号是来自一个DLL的.lib文件,那么使用__declspec(dllimport)编译时,效率会略高一点)。

(2)隐式链接时,要将编译DLL生成的.lib链接到可执行模块中,可以使用#pragma comment(lib,"../../Debug/19_ExportUseDef.lib")之类的链接指示符

(3)链接器会在最终的EXE模块中嵌入一个“导入段”,该段列出中所需的DLL模块,以及它从每个DLL模块中引用的符号。

19.2.3 运行可执行模块

(1)导入段只包含DLL的名称,而不包含DLL路径,加载时的搜索顺序如下:

  ①包含本执行文件(EXE)的目录

  ②Windows的系统目录,可通过GetSystemDirectory得到。如C:windowssystem32。

  ③16位的系统目录,即Windows目录中的System子目录。

  ④Windows目录,该目录可通过GetWindowsDirectory得到。如C:windows

  ⑤进程的当前目录(注意,这个目录搜索顺序位于Windows目录之后,是为了防止加载程序在应用程序的当前目录中找到伪造的系统DLL并将它们载入,从而保证系统DLL始终都是从它们的Windows目录中的正确位置载入的。)

  ⑥PATH环境变量中所列出的目录

(2)加载程序将DLL模块映射到进程的地址空间中,它会同时检查每个DLL的导入段,如果一个DLL有导入段,那么加载程序会继续将所需的额外的DLL模块映射到进程的地址空间。(如果多个模块用到了同一个DLL,该模块只会被载入和映射一次

(3)当所有的DLL模块被载入并映射到进程的地址空间中后。加载程序会检查每个DLL模块导入段中的每个符号(变量、函数名、C++类)在其对应的DLL的导出段中是否存在,如果不存在,就会报错。

(4)如果该符号存在,加载程序会取得该符号的RVA并加上DLL模块被载入的虚拟地址,并将把保存在该模块的导入段中。正是因为加载程序会在进程初始化时导入这些DLL模块,并修复每个模块的导入段,所以加载过程可能需要较长时间。为了减少加载时间,可对可执行模块和DLL模块进行基地址重定位和绑定(请参考第20章的基地址重定位和绑定技术)。

【MyLib程序】课本例子,演示导出函数和变量的方法

//Mylib.h

/************************************************************************
Module: MyLib.h
************************************************************************/
#pragma  once

#ifdef MYLIB_EXPORT
//MYLIB_EXPORT必须在Dll源文件包含该头件前被定义
#define MYLIBAPI extern "C" __declspec(dllexport)
//本例中所有的函数和变量都会被导出
#else
#define MYLIBAPI extern "C" __declspec(dllimport)
#endif

//导出变量(应避免导出变量!)
MYLIBAPI extern int g_nResult;

//定义要导出的函数的原型
MYLIBAPI int Add(int nLeft, int nRight);

//MyLib.cpp

/************************************************************************
Module: MyLibFile1.cpp
************************************************************************/

//包含标准的Windows头文件和C运行库头文件
#include <windows.h>

//////////////////////////////////////////////////////////////////////////
//在这个DLL源文件定义要导出的函数和变量
#define MYLIB_EXPORT   //这个源文件中须定义这个宏,以告诉编译器函数要
                   //__declspect(dllexport),这个宏须在包含MyLib.h
                   //之前被定义

#include "MyLib.h"

//////////////////////////////////////////////////////////////////////////
//在.cpp文件中,函数或变量前不必再加__declspect(...)关键字,因为头文件中己经说明了
int g_nResult;

int Add(int nLeft, int nRight){
    g_nResult = nLeft + nRight;
    return (g_nResult);
}

//////////////////////////////////////////////////////////////////////////
//以下DllMain不是必须的
//BOOL APIENTRY DllMain(HMODULE hModule, DWORD dwReason, LPVOID lpReserved)
//{
//    switch (dwReason)
//    {
//    case DLL_PROCESS_ATTACH:
//    case DLL_THREAD_ATTACH:
//    case DLL_THREAD_DETACH:
//    case DLL_PROCESS_DETACH:
//        break;
//    }
//    return TRUE;
//}
///////////////////////////////文件结束////////////////////////////////////

//MyExeFile1.cpp:测试程序

/************************************************************************
Module: MyExeFile1.cpp
************************************************************************/

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>


//包含DLL头文件
#include "../19_MyLib/MyLib.h"

#pragma comment(lib,"../../Debug/19_Mylib.lib")

//////////////////////////////////////////////////////////////////////////
int WINAPI _tWinMain(HINSTANCE,HINSTANCE,LPTSTR,int)
{
    int nSum,nLeft = 10, nRight = 25;
    nSum = Add(nLeft, nRight);
    TCHAR sz[100];
    
    StringCchPrintf(sz, _countof(sz), TEXT("%d + %d = %d
 g_nResult = %d"), 
                     nLeft, nRight, nSum,g_nResult);
    MessageBox(NULL, sz, TEXT("计算"), MB_OK);

    return 0;
}
原文地址:https://www.cnblogs.com/5iedu/p/4984519.html