当前位置:   article > 正文

一种踩内存的定位方法(C++)_c踩内存定位手段

c踩内存定位手段

    在嵌入式应用开发过程中,踩内存的问题常常让人束手无策。使用gdb调试工具,可以大幅加快问题的定位。不过,对于某些踩内存的问题,它的表现是间接的,应用崩溃的位置也是不固定的,这就给问题定位带来了更大的困难。

    笔者见过带有虚函数C++的类对象在试图调用虚函数时,因指向虚函数的表指针被踩了,导致获取虚函数的地址是错识的,从而应用崩溃。此问题的表现就是间接的:在踩内存发生时,应用没有崩溃;当应用崩溃时,执行的代码是踩内存的“历史遗迹”。为了让应用在踩内存时就发生崩溃(这样可以使用gdb调试,或分析其coredump),一种方法是将C++类对象配置成只读属性;可用的系统调用为mprotect,它可以配置一段对页对齐的内存区域内存的读写属性。

下面笔者对此问题进行了抽象和简化,完整的代码如下(memory-test.cpp):

 

  1. #include <new>
  2. #include <cstdio>
  3. #include <cerrno>
  4. #include <cstring>
  5. #include <cstdlib>
  6. #include <sys/types.h>
  7. #include <sys/mman.h>
  8. #include <fcntl.h>
  9. #include <unistd.h>
  10. #include <malloc.h>
  11. #define MEM_PAGESIZE 4096
  12. class MemBase {
  13. public:
  14. MemBase(int x_ = 1, int y_ = 9);
  15. virtual void testFunc0();
  16. virtual void testFunc1();
  17. virtual ~MemBase();
  18. protected:
  19. void memProtect(bool readonly);
  20. protected:
  21. unsigned char area[MEM_PAGESIZE * 3];
  22. int x, y;
  23. };
  24. class MemDeriv : public MemBase {
  25. public:
  26. MemDeriv(int x_ = 2, int y_ = 8);
  27. virtual void testFunc0();
  28. virtual void testFunc1();
  29. virtual ~MemDeriv();
  30. protected:
  31. int z;
  32. };
  33. int main(int argc, char *argv[])
  34. {
  35. MemDeriv * obj0;
  36. obj0 = new MemDeriv(3, 6);
  37. obj0->testFunc0();
  38. obj0->testFunc1();
  39. delete obj0;
  40. obj0 = NULL;
  41. return 0;
  42. }
  43. void MemBase::memProtect(bool readonly)
  44. {
  45. int ret;
  46. size_t msize;
  47. unsigned long pa;
  48. pa = (unsigned long) area;
  49. pa -= sizeof(void *); /* sizeof the TAB, what ever it is */
  50. if (pa & (MEM_PAGESIZE - 1)) {
  51. pa &= ~(MEM_PAGESIZE - 1);
  52. pa += MEM_PAGESIZE;
  53. msize = MEM_PAGESIZE * 2;
  54. } else {
  55. msize = MEM_PAGESIZE * 3;
  56. }
  57. if (readonly) {
  58. /* initialize the memory */
  59. memset(area, 0xA5, sizeof(area));
  60. }
  61. ret = mprotect((void *) pa, msize,
  62. readonly ? PROT_READ /* or PROT_NONE */ : PROT_READ | PROT_WRITE);
  63. if (ret != 0) {
  64. fprintf(stderr, "Error, failed to mprotect(%p): %s\n",
  65. (void *) pa, strerror(errno));
  66. fflush(stderr);
  67. }
  68. }
  69. MemBase::MemBase(int x_, int y_)
  70. {
  71. x = x_;
  72. y = y_;
  73. fprintf(stdout, "Constructing MemBase, this: %p, area: %p, x: %d (%p), y: %d (%p)\n",
  74. (void *) this, (void *) area, x, &x, y, &y);
  75. fflush(stdout);
  76. }
  77. void MemBase::testFunc0()
  78. {
  79. fprintf(stdout, "In function [%s], x: %d\n", __FUNCTION__, x);
  80. fflush(stdout);
  81. }
  82. void MemBase::testFunc1()
  83. {
  84. fprintf(stdout, "In function [%s], y: %d\n", __FUNCTION__, y);
  85. fflush(stdout);
  86. }
  87. MemBase::~MemBase()
  88. {
  89. fprintf(stdout, "Deconstructing MemBase, this: %p\n", (void *) this);
  90. fflush(stdout);
  91. }
  92. MemDeriv::MemDeriv(int x_, int y_) : MemBase(x_, y_)
  93. {
  94. z = x + y;
  95. fprintf(stdout, "Construction MemDeriv, this: %p, z: %d (%p)\n",
  96. (void *) this, z, &z);
  97. fflush(stdout);
  98. memProtect(true);
  99. }
  100. void MemDeriv::testFunc0()
  101. {
  102. fprintf(stdout, "In function [%s], z: %d\n", __FUNCTION__, z);
  103. fflush(stdout);
  104. }
  105. void MemDeriv::testFunc1()
  106. {
  107. fprintf(stdout, "In function [%s], z: %d\n", __FUNCTION__, z);
  108. fflush(stdout);
  109. }
  110. MemDeriv::~MemDeriv()
  111. {
  112. memProtect(false);
  113. fprintf(stdout, "Deconstructing MemDeriv, this: %p\n", (void *) this);
  114. fflush(stdout);
  115. }

    由于mprotect系统调用的限制,我们只能对基类(MemBase)中增加的3个页大小的内存区域area(中的两个页大小的内存区域)设置只读属性(假设设备的内存页大小为4096字节)。不过,在某些情况下,我们也希望对函数表指针进行保护,于是就有了第60行代码:

pa -= sizeof(void *); /* sizeof the TAB, what ever it is */

    这样做是有原因的。当创建C++类对象时,大多数情况下,area缓存的起始地址并不是页对齐的;当类对象的起始地址(即this指针)是页对齐的,那么area就偏移了sizeof(void *)字节,这样一来被配置为只读属性的起始地址就是在this指针偏移了4096字节之后:有时候,被踩内存的大小不足一个页的大小,就不会发生踩内存时的崩溃问题了。这是加入第60行代码的原因。

试着运行一下,此方法确定可行的,测试应用可以正常运行:

    见上图,这里没有测试踩内的异常情况,因此应用可以正常退出。下面让我们来测试一下上面假设的情况,即创建的C++类对象恰好在页对齐的地址上。我们将完整的代码重命名为memory-test1.cpp,将重写main函数,确保创建的类对像this指针是页对齐的,这样就可以对虚函数表指针进行mprotect保护了:

  1. int main(int argc, char *argv[])
  2. {
  3. void * palign;
  4. MemDeriv * obj0;
  5. palign = memalign(MEM_PAGESIZE, sizeof(MemDeriv));
  6. if (palign == NULL) {
  7. fprintf(stderr, "malloc(%#x) has failed: %s\n",
  8. (unsigned int) sizeof(MemDeriv), strerror(errno));
  9. fflush(stderr);
  10. exit(1);
  11. }
  12. obj0 = new(palign) MemDeriv(4, 5);
  13. obj0->testFunc0();
  14. obj0->testFunc1();
  15. obj0->~MemDeriv();
  16. free(obj0);
  17. obj0 = NULL;
  18. return 0;
  19. }

    如果一切顺利,memory-test1.cpp编译得到的test1就能够正常运行:

    结果却是应用崩溃了!使用gdb试一下,发现应用崩溃发生在子类的析构函数中,在调用memProtect成员函数前,就会对虚函数表指针进行写操作。一方面,我们得知mprotect确实能够正常工作,将一段内存设置为只读;另一方面,我们知道,在子类析构函数中,会操作虚函数表,因此该定位踩内存的方法存在严重缺陷——所有的工作都浪费了:

    这样的结果是不可接受的。我们不希望浪费这些工作,需要继续改进此方法;而改进此方法的手段,就是让C++子类的析构函数在调用memProtect成员函数之后再对虚函数表指针进行写操作。这样在memory-test1.cpp的基础上,改成了memory-test2.cpp,对MemDeriv的析构函数增加了很多nop指令:

  1. MemDeriv::~MemDeriv()
  2. {
  3. asm volatile (
  4. "\tnop\n"
  5. "\tnop\n"
  6. "\tnop\n"
  7. "\tnop\n"
  8. "\tnop\n"
  9. "\tnop\n" : : : "memory");
  10. memProtect(false);
  11. asm volatile (
  12. "\tnop\n"
  13. "\tnop\n"
  14. "\tnop\n"
  15. "\tnop\n"
  16. "\tnop\n"
  17. "\tnop\n"
  18. "\tnop\n"
  19. "\tnop\n"
  20. "\tnop\n"
  21. "\tnop\n"
  22. "\tnop\n"
  23. "\tnop\n"
  24. "\tnop\n" : : : "memory");
  25. fprintf(stdout, "Deconstructing MemDeriv, this: %p\n", (void *) this);
  26. fflush(stdout);
  27. }

相应的,析构函数的反汇编如下(反汇编test2):

    有了足够的nop指令填充代码段的空间,就可以对test2进行修改了,编写简单hed操作指令,对test2进行修改(hed的源码在下载页的压缩包中)。修改完成后,对析构函数的反汇编就变成了:

可以看到,test2在修改前后,文件大小未改变,但MD5较验值不同:

    接下来最后一搏,对test2进行测试,就能够正常运行了:

至此,我们的调试C++类被踩内存的方法就成功了:它将我们从踩内存的第二现场带到了案发第一现场。不过在应用崩溃时,还是需要gdb的协助,才得到定位。需要注意的是,该方案有几点要说明一下:

  1. 需要对基类增加页大小整数倍的area缓存成员变量,且需要是第一个成员变量(这样在area与this之间,不存在其他成员变量);
  2. 由子类的构造函数和析构函数调用memProtect,分别设置area(及其之前的虚函数表指针)的只读、可读可写属性;
  3. 若修改源码,每次编译生成的可执行文件或动态库,都需重新构造hed指令,修改子类析构函数,在调用memProtect之后对虚函数表进行写操作。
声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号