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

2023-11-20

    在嵌入式应用开发过程中,踩内存的问题常常让人束手无策。使用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   4096

class 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之后对虚函数表进行写操作。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

随机推荐

  • Android客户端apk自动检测更新自动下载自动安装的实现方法

    改进了一个可以检测版本更新自动下载自动安装的客户端升级方案 在下载之前删除之前的历史下载文件 减少垃圾数据 先给出核心类 public class DownloadService extends Service private Downlo
  • airpodspro没有弹窗_安卓党能用 AirPods Pro ?这样操作可以完美适配

    AirPods Pro一出现就引爆了市场 高达2000元的售价没能阻拦人们对它的狂热 目前AirPods已经在官网全面缺货 发货等待期甚至多达一个月 这是因为AirPods Pro的产品力实在太过出色 比它降噪强的没它小 比它小的没它降噪
  • C# A potentially dangerous 问题解决

    A potentially dangerous Request Path value was detected from the client 如果在页面中出现上述错误 只需在页面的头文件添加validateRequest false 即可
  • 【Vue基础】Vuex全局管理的基本代码结构及其使用

    Vuex全局管理的基本代码结构及其使用 一 代码结构及其使用 1 state 提供唯一的公共数据源 组件访问 state 中数据的第一种方式 this store state 全局数据名称 组件访问 state 中数据的第二种方式 mapS
  • UE4 使蓝图连接线出现小箭头标志

    当我们写的蓝图杂乱无章 甚至不能看清执行顺序的时候 只需要在偏好设置里设置一下就可以清晰的看清蓝图的执行顺序
  • 区块链+物联网 BOT

    不可否认 我们的一只脚已经迈入万物智联时代 但另一只脚迈入还存在一定的阻碍 区块链技术的出现将会促进这一进程的发展 智能音响 主人你好 我是小Q 现在是早上08点29分 上班时间要到咯 智能门锁 主人你摔疼我了 你总是这样匆忙 下次赶紧麻溜
  • 安装Python第三方库

    视频版教程 Python3零基础7天入门实战视频教程 在Python的标准安装中 包含了一组自带的模块 这些模块被成为 标准库 比如常用的math random datetime os json等等 此外 还有很多的第三方模块 或者叫做库也
  • 若以框架 vue富文本字符串中获取图片的存储路径

    若依的富文本 字符串是上述格式 我们要获取以下格式 就是图片的存储路径 有几个步骤 第一步 main js中添加两行代码 import axios from axios Vue prototype axios axios 第二步 Edit包
  • Python 笔记 — 单例模式和魔术方法

    目录 一 单例模式 1 概念 2 优点 3 使用 4 实现方法 5 运用 6 内存地址 7 通过 new 实现 8 通过 classmethod 9 通过装饰器实现 10 通过导入模块时实现 11 hasattr 函数 12 getattr
  • 内存管理——分页分段

    一 分页存储管理 1 页面与页框 1 页面 将一个进程的逻辑地址空间分成若干个大小相等的片 称为页面或页 并为各页加以编号 2 页框 相应于页面 把内存空间分成和页面相同大小的若干个存储块 称为 物理 块或页框 frame 3 页内碎片 在
  • Android中项目中各个文件夹的含义和用途详解

    1 src 存放所有的 java源程序 2 gen 为ADT插件自动生成的代码文件保存路径 里面的R java将保存所有的资源ID 3 assets 可以存放项目一些较大的资源文件 例如 图片 音乐 字体等 4 res 可以存放项目中所有的
  • IntelliJ IDEA 2022.3 版本 IDAE 的Java 编译器错误问题解决方法

    2022 2 3 版本 IDAE 的Java 编译器错误问题解决方法 如果这是按照以前的办法就是去更改下面 的内容就可以OK了 但是在最新版中无论怎么修改都是无用 不知道为啥 随后我就在这里 是的就是上面 的第二张图中 将11 versio
  • 设计模式——State(状态)模式

    目录 前言 1 定义 2 适用性 3 结构 3 1 结构图 3 2 参与者 4 应用举例 4 1 State TcpState 4 2 Context TcpConnection 4 3 ConcreteState ListeningTcp
  • 生产环境的域名地址与后端给的接口地址不一样时的配置(vue)

    1 找到 config dev env js 文件 module exports merge prodEnv NODE ENV development API ROOT http com 本地开发时用的域名 2 找到 config prod
  • 不在傻傻for循环!完美解决JPA批量插入问题

    前言 jpa在简单的增删改查方面确实帮助我们节省了大部分时间 但是面对复杂的情况就显得心有余而力不足了 最近遇到一个批量插入的情况 jpa虽然提供了saveAll方法 但是底层还是for循环save 如果遇到大量数据插入频繁与数据库交互必然
  • WIN11

    WIN11 安装WSL2 在WSL2上跑pytorch gpu 远程连接WSL2 个人电脑的关键配置 显卡是RTX3090 系统是WIN11 由于买的是整机 所以刚开机就装好了驱动 本人没有再进行显卡驱动 同时包含了wsl中的驱动 的安装
  • Android Log系统介绍 (基于Android N)

    原文使用有道云笔记创作 看这个 http note youdao com noteshare id 82f88b1c82652b80c27d54aad55af035 引言 gt Android 的log 从操作系统分层上来讲 可以分为 Ke
  • Linux内核驱动之GPIO子系统(一)GPIO的使用

    分类 Linux内核驱动 2012 10 31 21 12 162人阅读 评论 1 收藏 举报 目录 一 概述 Linux内核中gpio是最简单 最常用的资源 和 interrupt dma timer一样 驱动程序 应用程序都能够通过相应
  • Subwords Tokenizer方法介绍: BPE, Byte-level BPE, WordPiece, Unigram, SentencePiece

    参考于transformers tokenizer的文档 目录 Byte Pair Encoding BPE 1 出发点 原理 应用 Byte level BPE 2 出发点 原理 应用 WordPiece 3 原理 应用 Unigram
  • 一种踩内存的定位方法(C++)

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