经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
关于显示加载动态链接库模块及卸载的问题
来源:cnblogs  作者:青山見我應如是  时间:2021/2/18 15:47:37  对本文有异议

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

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

动态库头文件:

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

 动态库实现文件:

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

查看导出符号:

 

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

主程序实现:project.cpp

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

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

 

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

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

 

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

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

  1. 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里释放,不然会出现奇怪的现象。

。。。待续

 

原文链接:http://www.cnblogs.com/quinlan-space/p/14377806.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号