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

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

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

#include <new>
#include <cstdio>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <sys/types.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <malloc.h>#define MEM_PAGESIZE   4096class MemBase {
public:MemBase(int x_ = 1, int y_ = 9);virtual void testFunc0();virtual void testFunc1();virtual ~MemBase();protected:void memProtect(bool readonly);protected:unsigned char area[MEM_PAGESIZE * 3];int x, y;
};class MemDeriv : public MemBase {
public:MemDeriv(int x_ = 2, int y_ = 8);virtual void testFunc0();virtual void testFunc1();virtual ~MemDeriv();protected:int z;
};int main(int argc, char *argv[])
{MemDeriv * obj0;obj0 = new MemDeriv(3, 6);obj0->testFunc0();obj0->testFunc1();delete obj0;obj0 = NULL;return 0;
}void MemBase::memProtect(bool readonly)
{int ret;size_t msize;unsigned long pa;pa  = (unsigned long) area;pa -= sizeof(void *); /* sizeof the TAB, what ever it is */if (pa & (MEM_PAGESIZE - 1)) {pa &= ~(MEM_PAGESIZE - 1);pa += MEM_PAGESIZE;msize = MEM_PAGESIZE * 2;} else {msize = MEM_PAGESIZE * 3;}if (readonly) {/* initialize the memory */memset(area, 0xA5, sizeof(area));}ret = mprotect((void *) pa, msize,readonly ? PROT_READ /* or PROT_NONE */ : PROT_READ | PROT_WRITE);if (ret != 0) {fprintf(stderr, "Error, failed to mprotect(%p): %s\n",(void *) pa, strerror(errno));fflush(stderr);}
}MemBase::MemBase(int x_, int y_)
{x = x_;y = y_;fprintf(stdout, "Constructing MemBase, this: %p, area: %p, x: %d (%p), y: %d (%p)\n",(void *) this, (void *) area, x, &x, y, &y);fflush(stdout);
}void MemBase::testFunc0()
{fprintf(stdout, "In function [%s], x: %d\n", __FUNCTION__, x);fflush(stdout);
}void MemBase::testFunc1()
{fprintf(stdout, "In function [%s], y: %d\n", __FUNCTION__, y);fflush(stdout);
}MemBase::~MemBase()
{fprintf(stdout, "Deconstructing MemBase, this: %p\n", (void *) this);fflush(stdout);
}MemDeriv::MemDeriv(int x_, int y_) : MemBase(x_, y_)
{z = x + y;fprintf(stdout, "Construction MemDeriv, this: %p, z: %d (%p)\n",(void *) this, z, &z);fflush(stdout);memProtect(true);
}void MemDeriv::testFunc0()
{fprintf(stdout, "In function [%s], z: %d\n", __FUNCTION__, z);fflush(stdout);
}void MemDeriv::testFunc1()
{fprintf(stdout, "In function [%s], z: %d\n", __FUNCTION__, z);fflush(stdout);
}MemDeriv::~MemDeriv()
{memProtect(false);fprintf(stdout, "Deconstructing MemDeriv, this: %p\n", (void *) this);fflush(stdout);
}

由于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保护了:

int main(int argc, char *argv[])
{void * palign;MemDeriv * obj0;palign = memalign(MEM_PAGESIZE, sizeof(MemDeriv));if (palign == NULL) {fprintf(stderr, "malloc(%#x) has failed: %s\n",(unsigned int) sizeof(MemDeriv), strerror(errno));fflush(stderr);exit(1);}obj0 = new(palign) MemDeriv(4, 5);obj0->testFunc0();obj0->testFunc1();obj0->~MemDeriv();free(obj0);obj0 = NULL;return 0;
}

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

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

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

MemDeriv::~MemDeriv()
{asm volatile ("\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n" : : : "memory");memProtect(false);asm volatile ("\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n""\tnop\n" : : : "memory");fprintf(stdout, "Deconstructing MemDeriv, this: %p\n", (void *) this);fflush(stdout);
}

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

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

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

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

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

  1. 需要对基类增加页大小整数倍的area缓存成员变量,且需要是第一个成员变量(这样在area与this之间,不存在其他成员变量);
  2. 由子类的构造函数和析构函数调用memProtect,分别设置area(及其之前的虚函数表指针)的只读、可读可写属性;
  3. 若修改源码,每次编译生成的可执行文件或动态库,都需重新构造hed指令,修改子类析构函数,在调用memProtect之后对虚函数表进行写操作。

一种踩内存的定位方法(C++)相关推荐

  1. JVM内存调优原则及几种JVM内存调优方法

    JVM内存调优原则及几种JVM内存调优方法 1.堆大小设置. 2.回收器选择. 1.在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因 ...

  2. windows内存泄露定位方法

    windows内存泄露定位方法 内存泄露(Memory Leak)是C/C++程序经常遇到的一个棘手问题.简单来说,内存泄露就是没有释放本来应该释放的内存. 可以把解决内存泄露问题分成两步,第一步是定 ...

  3. linux踩内存怎么定位,问题定位:内存泄漏,踩内存。

    1.内存泄漏 确定现象: linux 内存泄漏,可以查看slabinfo 和另外一个proc下(貌似meminfo),关于内存的信息,可以看到内存是否在不断减少,以及减少的速度. vxworks系统, ...

  4. Linux下内存泄漏定位方法

    Linux下内存泄漏可分为用户空间的内存泄漏和内核空间的内存泄漏. 用户空间内存泄漏的查找方法: 第一步,查找内存泄漏的应用程序. 首先,写一个简单的内存泄漏程序(每秒钟泄漏4MB)umemleak. ...

  5. c++程序异常定位方法

    文章目录 (一).core.dump (二).dmesg (三).pstack (四).strace (五).valgrind 对于c++程序来说,以segment fault为代表的程序异常行为前奇 ...

  6. python+selenium笔记(一):元素定位方法

    一.环境准备: 1.浏览器选择:Firefox 2.安装插件:Firebug和FirePath(设置>附加组件>搜索:输入插件名称>下载安装后重启浏览器) 3.安装完成后,页面右上角 ...

  7. 导航定位用户向服务器发送位置请求6,跨平台的地理位置定位方法、平台及定位接入服务器...

    1. 一种跨平台的地理位置定位方法,其特征在于,包括: 通过网络服务接口,接收移动终端的应用发送的定位请求: 将所述定位请求转换格式后,向定位导航解算服务器发送: 接收所述定位导航解算服务器针对所述转 ...

  8. 一种定位内存泄露的方法(Linux)

    2019独角兽企业重金招聘Python工程师标准>>> 目的: 本文是<一种定位内存泄露的方法(Solaris)>对应的Linux版本,调试器使用gdb.主要介绍实例部分 ...

  9. linux 定位 踩内存_记录一次用户态踩内存问题

    这几天在做总结,把三年前写的一个定位案例,翻了出来.回想起定位这个问题时的场景,领导催得紧,自己对很多东西又不熟悉,所以当时面临的压力还是很大的.现在回想起来感慨还是很多的,我们在遇到任何一个问题,一 ...

最新文章

  1. DataGrid基于Access的快速分页法
  2. Access和SQL server开启表间关系,并实现更新或删除母表数据自动更新或删除子表数据...
  3. Drectx 3D窗口后台截图
  4. java streaming编程_Spark Streaming编程实战(开发实例)
  5. 绝地求生自定义服务器租用,绝地求生自定义服务器怎么开 自定义服务器设置方法...
  6. 听说你想去大厂看妹子,带你看看字节跳动实习算法岗面试长啥样?
  7. linux和windows文件名称长度限制
  8. 11-散列3 QQ帐户的申请与登陆 (25 分)
  9. 【Leetcode819】最常见的单词
  10. 吴恩达《机器学习》第七章:正则化
  11. 【信号与系统|吴大正】2:连续系统的时域分析
  12. 基于64QAM的LDPC编译码算法
  13. 计算机教室联想系统管理员密码,联想电脑EDU开放模式管理员密码忘记了怎么办...
  14. 不能被编辑的html文档,word不能编辑怎么办 Word文档怎么设置成不可编辑?
  15. 引用动态链接库的原理
  16. 小码哥C++学院-零基础入门C语言
  17. 7-13 寻找大富翁 (25分)
  18. [Bug]: Could not load dynamic library ‘libnvinfer.so.7‘
  19. 领导说给我调岗,是不是不喜欢我?我要怎么办呢?
  20. 从 B 站火到 GitHub,国人开发者又一黑科技面世!

热门文章

  1. SQLsever数据库实例是啥子
  2. bzoj4084【SDOI2015】bigyration
  3. 健康开怀一辈子(转)
  4. 使用OC实现单链表:创建、删除、插入、查询、遍历、反转、合并、判断相交、求成环入口...
  5. HUD1.2.4 Nasty Hacks
  6. 用css3实现图片左右翻转
  7. 2020.9.30 PYTHON 自复习笔记
  8. 计算机科学的一个字节是几位,什么是字节--字节换算
  9. 论文精读2: Ground-to-Aerial Image Geo-LocalizationWith a Hard Exemplar Reweighting Triplet Loss
  10. win7系统打开计算机怎么不显示磁盘分区,大师详解win7系统隐藏磁盘分区不显示的具体步骤...