• 第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;
    }
  • 相关阅读:
    设计模式--总结
    设计模式--行为型模式--解释器模式
    设计模式--行为型模式--备忘录模式
    设计模式--行为型模式--访问者模式(Visitor模式)
    设计模式--行为型模式--迭代器模式
    设计模式--行为型模式--中介者模式
    js常用方法集合
    CSS 每隔4行显示不同样式的表格
    常用正则验证
    wIndow 强制关闭被占用的端口
  • 原文地址:https://www.cnblogs.com/5iedu/p/4984519.html
Copyright © 2020-2023  润新知