当前位置 博文首页 > 昆兰:关于显示加载动态链接库模块及卸载的问题

    昆兰:关于显示加载动态链接库模块及卸载的问题

    作者:昆兰 时间:2021-02-05 18:24

    问题起因是,在一次模块卸载后,程序运行异常。遂对动态链接库做一些测试。

    动态库加载方式有两种,隐式加载和显示加载,隐式加载包含xxx.lib导入库,在程序执行之前由动态加载器完成所有加载;显示加载则使用LoadLibrary方式;具体数据可参考《程序员的自我修养:链接,装载与库》一书。

    动态库头文件:

     1 #ifdef DYNAMICLIBRARYTEST_EXPORTS
     2 #define DYNAMICLIBRARYTEST_API __declspec(dllexport)
     3 #else
     4 #define DYNAMICLIBRARYTEST_API __declspec(dllimport)
     5 #endif
     6 
     7 // 此类是从 dll 导出的
     8 class DYNAMICLIBRARYTEST_API Base {
     9 public:
    10     Base(void);
    11     
    12     virtual int* virtualFunc();
    13     virtual ~Base();
    14     
    15 
    16     int a = 8;
    17     int b = 9;
    18     char c[10] = {'H','e', 'l', 'l', 'o', 'W', 'o', 'r', 'l', 'd' };
    19     // TODO: 在此处添加方法。
    20 };
    21 
    22 class DYNAMICLIBRARYTEST_API Derive  : public Base
    23 {
    24 public:
    25     Derive(void);
    26     int* normalFunc()
    27     {
    28         return nullptr;
    29     }
    30 
    31     int* virtualFunc() override;
    32      ~Derive();
    33     // TODO: 在此处添加方法。
    34 };
    35 
    36 extern "C" DYNAMICLIBRARYTEST_API int i_global;
    37 
    38 extern "C" DYNAMICLIBRARYTEST_API double d_global;
    39 
    40 extern "C" DYNAMICLIBRARYTEST_API char c_global[6];
    41 
    42 extern "C" DYNAMICLIBRARYTEST_API int func1(void);
    43 extern "C" DYNAMICLIBRARYTEST_API Derive* createDerive();
    View Code

     动态库实现文件:

     1 // DynamicLibraryTest.cpp : 定义 DLL 的导出函数。
     2 //
     3 
     4 #include "DynamicLibraryTest.h"
     5 // 这是导出变量的一个示例
     6 DYNAMICLIBRARYTEST_API int i_global = 1;
     7 int i_global_1 = 9;
     8 DYNAMICLIBRARYTEST_API double d_global = 2 ;
     9 DYNAMICLIBRARYTEST_API char c_global[6] = {'G', 'l','o', 'b', 'a', 'l'};
    10 
    11 // 这是导出函数的一个示例。
    12 DYNAMICLIBRARYTEST_API int func1(void)
    13 {
    14     return -1;
    15 }
    16 
    17 Derive * createDerive()
    18 {
    19     return new Derive;
    20 }
    21 
    22 Base::Base()
    23 {
    24     return;
    25 }
    26 
    27 
    28 int* Base::virtualFunc()
    29 {
    30     return nullptr;
    31 }
    32 
    33 Base::~Base()
    34 {
    35 }
    36 
    37 Derive::Derive(void)
    38 {
    39 }
    40 
    41 int* Derive::virtualFunc()
    42 {
    43     int c = a + b;
    44     c--;
    45     return new int[10];
    46 }
    47 
    48 Derive::~Derive()
    49 {
    50 }
    View Code

    查看导出符号:

     

     可以看到导出的变量命名比较正常,这是因为是以C风格导出的。不然就是C++的诡异风格修饰。

    主程序实现:project.cpp

     1 // project.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
     2 //
     3 
     4 #include <iostream>
     5 #include "DynamicLibraryTest.h"
     6 #include <Windows.h>
     7 
     8 #define LIBNAME "C:/Users/Admin/source/repos/DynamicLibraryTest/Release/DLL_1.dll"
     9 
    10 typedef int*(*NormalFunc)();
    11 typedef Derive*(*CreateDerive)();
    12 int main()
    13 {
    14     const char* szStr = LIBNAME;
    15     WCHAR wszClassName[256]; 
    16     memset(wszClassName, 0, sizeof(wszClassName));
    17     MultiByteToWideChar(CP_ACP, 0, szStr, strlen(szStr) + 1, wszClassName, sizeof(wszClassName) / sizeof(wszClassName[0])); 
    18     HMODULE hmodule = ::LoadLibrary(wszClassName);
    19     if (NULL == hmodule) 
    20     {
    21         printf("LoadLibrary failed/n"); 
    22         return -1; 
    23     }
    24 
    25     CreateDerive funcDerive = (CreateDerive)GetProcAddress(hmodule, "createDerive");
    26     NormalFunc nor = (NormalFunc)GetProcAddress(hmodule, "?normalFunc@Derive@@QAEPAHXZ");
    27     Derive* d = funcDerive();//分配在堆上
    28     Derive* d2 = funcDerive();
    29     //d->normalFunc();//不能直接调用非虚函数
    30     //本模块保存了一份虚表地址在堆上,每次访问虚函数,通过堆上的保存的虚表地址查找真正的虚表,
    31     //而虚表保存在映射区域(dll模块的全局常量区,不过映射的数据区域为备份),随着模块的卸载,该映射区域也会消失,导致访问异常。
    32     //至于为什么显示加载dll的方式不能调用非虚函数,是因为调用这种函数不需要查虚表,直接调函数地址,但该函数导出名字经过修饰,
    33     //会造成无法解析的引用; 子类和父类都有一套虚表,存的是各自的函数地址。
    34     int* vb = d->virtualFunc();//ecx寄存器保存的是this指针,即d;
    35     d2->a = 2;
    36     _asm
    37     {
    38         mov ecx, dword ptr[d2];
    39     }
    40     nor();//此时调用的是d2的成员函数。
    41     delete d;
    42     int *local = new int[10];
    43     vb[0] = 1;
    44     local[0] = 2;
    45     int c = vb[0] + local[0];
    46 
    47     ::FreeLibrary(hmodule);
    48     //int* va = d->virtualFunc();//报错
    49     return 0;
    50 }

     显示加载后,得到类对象d,是不能直接通过该对象调用其非虚成员函数的(链接不通过),但是能直接调用虚函数。问题是因为调用虚函数是要查虚表的。下图是project.obj的main部分反汇编代码:

     

     可以看到对于一般的函数调用会生成函数符号,相当于一个占位标记,该符号地址在链接前,用默认地址00 00 00 00 代替(32位机器下),在执行链接后,该默认地址会修改为正确的位置。

    链接后的main部分反编译代码:

     

     回到之前的那个问题,为什么一般的成员函数不能直接调用,因为找不到符号(无法解析的引用符号),会导致链接不过。

     第一,导出该符号(整个类都是导出的话,该成员函数自然也是导出的)。第二,该符号的名字要写对;

    NormalFunc nor = (NormalFunc)GetProcAddress(hmodule, "?normalFunc@Derive@@QAEPAHXZ");

    强行获取该方法。那么又有一个问题,这个函数该怎么调用?对于任意一个成员函数来讲,调用会存在一个this指针。直接调用会出现奇怪的现象。其实通常调用成员函数,从汇编的角度,会将this指针赋值给ecx寄存器。接着调用该函数。

     

     

     

     上图可以看到ecx与this的关系。通过证实nor()执行的确实是d2的成员函数。

    接着下一个问题,卸载模块后,在该模块申请的堆内存数据还在不在?以及能不能继续调用该模块的成员函数。

    下图先给出该进程的内存布局(x64Dbg反编译工具):

     

     执行完LoadLibrary后的内存布局:

     

     可以看到dll_1映射到了某个内存地址。

     查看dll中normalFunc的函数地址:

     对应于dll的代码段映射区域。

    查看d和d2的内存区域:

     

     可以看到这两个变量所对应的首4字节值是一样的,这就是虚表地址。

    转到虚表地址:

     

     发现该虚表存储在DLL_1的内存区域“.rdata ”段(从前面的内存布局看出)。

    那么当真个DLL被卸载时发生了什么?执行完Freelibrary后:

     

     

     

     

     

     那么显而易见,卸载dll模块后,变量d2是不能调用任何函数的,因为此时地址都清空了,包括虚函数,虚表不存在。而d2这个变量所对应的内存空间依然存在。

    其实,在dll申请的内存,最好在该dll里释放,不然会出现奇怪的现象。

    。。。待续

     

    bk