C语言(14) 谈谈嵌入式 C 语言踩内存问题!

news2024/12/28 3:56:48

1 概述

C 语言内存问题,难在于定位,定位到了就好解决了。
这篇笔记我们来聊聊踩内存。踩内存,通过字面理解即可。本来是操作这一块内存,因为设计失误操作到了相邻内存,篡改了相邻内存的数据。

踩内存,轻则导致功能异常,重则导致程序崩溃死机。
内存,粗略地分:

静态存储区
动态存储区

存储于相同存储区的变量才有互踩内存的可能。

2 静态存储区踩内存

分享一个之前在实际项目中遇到的问题。

在Linux中,一个进程默认可以打开的文件数为1024个,fd的范围为0~1023。
项目中使用了串口,串口fd为static全局变量,某次这个fd突然变为一个超范围得值,显然被踩了。
出问题的代码如:

float arr[5];
int count = 8;
for (size_t i = 0; i < count; i++)
{
    arr[i] = xxx;
}

在这里插入图片描述
操作同属于静态存储区的arr数组出现了数组越界操作,踩了后面几个连续变量,fd也踩了。
实际中,纯靠log打印调试很难定位fd的相邻变量,需要花比较多的时间。
在Linux中,这个问题我们可以通过生成生成map文件来查看,在CMakeLists.txt中生成map文件的代码如:

set(CMAKE_EXE_LINKER_FLAGS "-Wl,-Map=output.map")  # 生成map文件
set(CMAKE_C_FLAGS "-fdata-sections")               # 把static变量地址输出到map文件
set(CMAKE_CXX_FLAGS "-fdata-sections")

3 动态存储区踩内存

动态堆内存踩内存典型例子:malloc与strcpy搭配使用不当导致缓冲区溢出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main (void)
{
    char *str = "hello";
    int str_len = strlen(str);

    ///< 此时str_len = 5
    printf("str_len = %d\n", str_len);

    ///< 申请5字节的堆内存
    char *ptr = (char*)malloc(str_len);
    if (NULL == ptr)
    {
        printf("malloc error\n");
        exit(EXIT_FAILURE);
    }

    ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20
    char *p_a = ptr + 5;
    *p_a = 20;
    printf("*p_a = %d\n", *p_a);

    ///< 拷贝字符串str到ptr指向的地址
    strcpy(ptr, str);

    ///< 打印结果:a指向的地方被踩了
    printf("ptr = %s\n", ptr);
    printf("*p_a = %d\n", *p_a);

    ///< 释放对应内存
    if (ptr)
    {
        free(ptr);
        ptr = NULL;
    }

    return 0;
}

运行结果:
在这里插入图片描述
显然,经过strcpy操作之后,数据a的值被篡改了。
原因:忽略了strcpy操作会把字符串结束符一同拷贝到目的缓冲区。
在这里插入图片描述
如果相邻的空间里没有存放其它业务数据,那么踩了也不会出现问题,如果正好存放了重要数据,这时候可能会出现大bug,而且可能是偶现的,不好复现定位。

针对这种情况,我们可以借助一些工具来定位问题,比如:

dmalloc
valgrind
valgrind的简单使用可阅读往期笔记:链接

当然,我们也可以在我们的代码里进行一些尝试。针对这类问题,分享一个检测思路:

我们在申请内存时,在申请内存的前后增加两块标识区(红区),里面写入固定数据。申请、释放内存的时候去检测这两块标识区有没有被破坏(检测操作堆内存时是否踩到高压红区)。

为了能定位到后面的标识区,在增加一块len区用来存储实际申请的空间的长度。
此处,我们定义:

前红区(before_ red_area):4字节。写入固定数据0x11223344。
后红区(after_ red_area):4字节。写入固定数据0x55667788。
长度区(len_area):4字节。存储数据存储区的长度。

在这里插入图片描述

自定义申请内存函数

除了数据存储区之外,多申请12个字节。自定义申请内存的函数自然是要兼容malloc的使用方法。malloc原型:

void *malloc(size_t __size);

自定义申请内存的函数:

void *Malloc(size_t __size);

返回值自然要返回数据存储区的地址。具体实现:

#define BEFORE_RED_AREA_LEN  (4)            ///< 前红区长度
#define AFTER_RED_AREA_LEN   (4)            ///< 后红区长度
#define LEN_AREA_LEN         (4)            ///< 长度区长度

#define BEFORE_RED_AREA_DATA (0x11223344u)  ///< 前红区数据
#define AFTER_RED_AREA_DATA  (0x55667788u)  ///< 后红区数据

void *Malloc(size_t __size)
{
    ///< 申请内存:4 + 4 + __size + 4
    void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
    if (NULL == ptr)
    {
        printf("[%s]malloc error\n", __FUNCTION__);
        return NULL;
    }

    ///< 往前红区地址写入固定值
    *((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;     

    ///< 往长度区地址写入长度     
    *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;  

    ///< 往后红区地址写入固定值
    *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;  

    ///< 返回数据区地址
    void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
    return data_area_ptr;
}
自定义检测内存函数
申请完内存并往内存里写入数据后,检测本该写入到数据存储区的数据有没有写到红区。这种内存检测方法我们是用在开发调试阶段的,所以检测内存,我们可以使用断言,一旦触发断言,直接终止程序报错。

检测前后红区里的数据有没有被踩:

void CheckMem(void *ptr, size_t __size)
{
    void *data_area_ptr = ptr;

    ///< 检测是否踩了前红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
    assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);

    ///< 检测是否踩了长度区
    printf("[%s]len_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
    assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size); 

    ///< 检测是否踩了后红区
    printf("[%s]after_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
    assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA); 
}

自定义释放内存函数
要释放所有前面申请内存。释放前同样要进行检测:

void Free(void *ptr)
{
    void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;

    ///< 检测是否踩了前红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
    assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);

    ///< 读取长度区内容
    size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));

    ///< 检测是否踩了后红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
    assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);

    ///< 释放所有区域内存
    free(all_area_ptr);
}

我们使用这种方法检测上面的 malloc与strcpy搭配使用不当导致缓冲区溢出 的例子:
在这里插入图片描述
可以看到,这个例子踩了后红区,把后红区数据修改为了 0x55667700 ,触发断言程序终止。

测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>

#define BEFORE_RED_AREA_LEN  (4)            ///< 前红区长度
#define AFTER_RED_AREA_LEN   (4)            ///< 后红区长度
#define LEN_AREA_LEN         (4)            ///< 长度区长度

#define BEFORE_RED_AREA_DATA (0x11223344u)  ///< 前红区数据
#define AFTER_RED_AREA_DATA  (0x55667788u)  ///< 后红区数据

void *Malloc(size_t __size)
{
    ///< 申请内存:4 + 4 + __size + 4
    void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
    if (NULL == ptr)
    {
        printf("[%s]malloc error\n", __FUNCTION__);
        return NULL;
    }

    ///< 往前红区地址写入固定值
    *((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;     

    ///< 往长度区地址写入长度     
    *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;  

    ///< 往后红区地址写入固定值
    *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;  

    ///< 返回数据区地址
    void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
    return data_area_ptr;
}

void CheckMem(void *ptr, size_t __size)
{
    void *data_area_ptr = ptr;

    ///< 检测是否踩了前红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
    assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);

    ///< 检测是否踩了长度区
    printf("[%s]len_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
    assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size); 

    ///< 检测是否踩了后红区
    printf("[%s]after_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
    assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA); 
}

void Free(void *ptr)
{
    void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;

    ///< 检测是否踩了前红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
    assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);

    ///< 读取长度区内容
    size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));

    ///< 检测是否踩了后红区
    printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
    assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);

    ///< 释放所有区域内存
    free(all_area_ptr);
}

int main (void)
{
    char *str = "hello";
    int str_len = strlen(str);

    ///< 此时str_len = 5
    printf("str_len = %d\n", str_len);

    ///< 申请5字节的堆内存
    char *ptr = (char*)Malloc(str_len);    ///< 自定义的Malloc
    if (NULL == ptr)
    {
        printf("malloc error\n");
        exit(EXIT_FAILURE);
    }

    ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20
    char *p_a = ptr + 5;
    *p_a = 20;
    printf("*p_a = %d\n", *p_a);

    ///< 拷贝字符串str到ptr指向的地址
    strcpy(ptr, str);

    ///< 操作完堆内存之后,要检测写入操作有没有踩到红区
    CheckMem(ptr, str_len);

    ///< 打印结果:a指向的地方被踩了
    printf("ptr = %s\n", ptr);
    printf("*p_a = %d\n", *p_a);

    ///< 释放对应内存
    if (ptr)
    {
        Free(ptr);
        ptr = NULL;
    }

    return 0;
}

没有踩内存的情况:
在这里插入图片描述
本例只是简单分享了检测堆内存踩数据的一种检测思路,例子代码不具备通用性。比如,万一踩的内存不只是相邻的几个字节,而是踩了相邻的一大片,这时候就跨过了红区,而不是踩在红区上。

红区大小由我们自己设定,我们可以设得大些。如果设得很大了都能跨过,这种情况bug应该就比较好复现也比较好定位。看代码应该就比较容易定位了,比较难定位的往往是那种踩了一小块的。

相关资料:

https://www.packetmania.net/2021/03/28/Memory-overrun-detection/

https://download.csdn.net/download/rrzzzz/8642321

转载处:点击

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/656390.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Shopify股价在暴涨了78%以后,还值得投资吗?

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 今年以来Shopify (SHOP)的股价一直在上涨&#xff0c;迄今为止的涨幅已经超过了78%&#xff0c;并且远远跑赢了美股的所有主要指数。 猛兽财经认为&#xff0c;Shopify的股价之所以能在今年上涨&#xff0c;主要受到以下几…

windows2022证书配置.docx

Windows证书的配置 要求两台主机&#xff0c;一台作为域&#xff0c;一台进入域 按要求来选择角色服务 确认之后安装 安装完以后配置证书服务 选择服务 按要求配置 注&#xff1a;此处不用域用户登陆无法使用企业CA 按要求来 创建新的私钥 这几处检查无误后默认即可 有效期…

实验篇(7.2) 16. 站对站安全隧道 - 通过聚合隧道走对方上网(FortiGate-IPsec) ❀ 远程访问

【简介】前面所有实验基本上是由向导来完成的&#xff0c;只有隧道聚合实验是手动设置的。那么远程访问经常用到的走对方宽带上网功能&#xff0c;需要怎样手动配置呢&#xff1f; 实验要求与环境 OldMei集团深圳总部防火墙现在有三条宽带了&#xff0c;二条普通宽带用来上网及…

SSCMS 内容管理系统介绍

概述 SSCMS 内容管理系统基于微软 .NET Core 平台开发,用于创建在 Windows、Linux、Mac 以及 Docker 上运行的 Web 应用程序和服务。 SSCMS 针对企业级客户开发,完全开源免费,可以用于商业用途不需要支付任何产品或授权费用。 SSCMS 经受了时间考验,1.0 版本在2003年发布…

跨模态检索最新高质量综述《Image-text Retrieval: A Survey on Recent Research and Development》

Image-text Retrieval: A Survey on Recent Research and Development 图像文本检索研究进展综述 2022.03 本文已把文献的引用逐个换成相应的论文标题&#xff0c;方便查找和阅读 摘要 本文从四个方面对ITR方法进行了全面和最新的调查。通过将ITR系统剖析为两个过程&#xff1…

基于Java+Swing+Mysql实现汽车信息管理系统

基于JavaSwingMysql实现汽车信息管理系统 一、系统介绍二、功能展示1.登陆2.车辆信息3.车辆入库4.车辆出库5.车辆查询6、车辆信息修改 三、数据库四、其它1.其他系统实现五.获取源码 一、系统介绍 系统实现汽车入库&#xff0c;汽车出库&#xff0c;汽车查询&#xff0c;汽车信…

2023软件测试八股文最全文档,再也不用担心面试了

前言 6月份已经快过完一半了&#xff0c;马上就要到金九银十的黄金招聘季节了&#xff0c;还在准备面试跳槽涨薪的小伙伴们可以看看本篇文章哟&#xff0c;这里呢笔者就不多说废话了直接上干货&#xff01;答案已整理好&#xff0c;文末拿去即可&#xff01;非常好用&#xff…

day57_Git

今日内容 零、 复习昨日 零、 复习昨日 文章目录 零、 复习昨日一、引言二、介绍三、Git安装3.1 下载Git3.2 安装3.3 基本配置3.4 测试 四、架构五、仓库5.1 新建仓库5.2 工作区5.3 暂存区5.4 分支 六、基本操作6.1 查看仓库状态6.2 暂存文件6.3 提交文件6.4 撤销已经add的文件…

POSTGRESQL PG_GATHER 如何单纯用SQL 就可以产生一个有意思的PG 的分析报告

开头还是介绍一下群&#xff0c;如果感兴趣polardb ,mongodb ,mysql ,postgresql ,redis 等有问题&#xff0c;有需求都可以加群群内有各大数据库行业大咖&#xff0c;CTO&#xff0c;可以解决你的问题。加群请联系 liuaustin3 &#xff0c;在新加的朋友会分到2群&#xff08;共…

【Android开发基础】Canvas画笔(以刮刮乐为例)

文章目录 一、引言二、设计1、获取图片资源2、获取屏幕信息3、Canvas涂层4、随机内容5、屏幕监听 三、附件1、UI设计2、总代码&#xff08;1&#xff09;控件初始化&#xff08;2&#xff09;图层初始化 3、源代码 一、引言 &#xff08;本篇博客只说明Canvas画笔的使用&#…

STM32单片机(六)TIM定时器 -> 第六节:TIM输入捕获练习(输入捕获模式测频率和PWMI模式测频率占空比)

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

docker inspect

docker inspect 命令用于获取有关 Docker 容器、镜像、网络等的详细信息。它提供了关于指定对象的元数据和配置的完整视图&#xff0c;包括运行状态、网络设置、卷挂载、环境变量等。 以下是一些常见的字段和属性&#xff0c;可以根据需要选择其中一些或全部列出&#xff1a; …

【算法与数据结构】202、LeetCode快乐数

文章目录 一、题目二、解法三、完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 一、题目 二、解法 思路分析&#xff1a;先用一个dowhile循环计算整数各个位数字的平方和&#xff0c;然后在unordered_set里面插入n&#xff0…

C语言学习笔记:函数

✨博文作者&#xff1a;烟雨孤舟 &#x1f496; 喜欢的可以 点赞 收藏 关注哦~~ ✍️ 作者简介: 一个热爱大数据的学习者 ✍️ 笔记简介&#xff1a;作为大数据爱好者&#xff0c;以下是个人总结的学习笔记&#xff0c;如有错误&#xff0c;请多多指教&#xff01; 目录 简介 …

关于OpenCV中minAreaRect角度记录

一、问题引出 最近看到stackflow关于minAreaRect的讨论&#xff1a; MinAreaRect angles - Unsure about the angle returnedOpenCV’s RotatedRect angle does not provide enough information 大概问题是minAreaRect这个接口返回的角度信息不足以反应返回的旋转矩形的旋转…

appium辅助自动化工具-- Appium studio

这里我要给大家介绍一款appium辅助自动化测试工具appium studio&#xff0c;你没看错&#xff0c;不是android studio&#xff0c;也不是appium android studio&#xff0c;就是appium studio&#xff01; 下载地址&#xff1a; Appium Studio | Digital.ai Continuous Test…

架构-计算机体系结构

章节架构 分值约 3 分 #mermaid-svg-nV7dvQlYnuXaOVOf {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-nV7dvQlYnuXaOVOf .error-icon{fill:#552222;}#mermaid-svg-nV7dvQlYnuXaOVOf .error-text{fill:#552222;strok…

如何优雅的使用 React Context

在开始今天的文章之前&#xff0c;大家不妨先想一下触发 React 组件 re-render 的原因有哪些&#xff0c;或者说什么时候 React 组件会发生 re-render 。 先说结论&#xff1a; 状态变化父组件 re-renderContext 变化Hooks 变化 ❝ 这里有个误解&#xff1a;props 变化也会导致…

力扣日记2481

1. 题目 LeetCode 2481. 分割圆的最少切割次数 1.1 题意 可以使用直接或半径切分&#xff0c;管他叫一次切分&#xff0c;求切分圆为n等份的最少次数。 1.2 分析 可以想到&#xff0c;对圆做n等分&#xff0c;然后每个半径看出一次切分&#xff0c;这是最多次数&#xff0c;…

实验篇(7.2) 13. 站对站安全隧道 - 走对方宽带上网(FortiGate-IPsec) ❀ 远程访问

【简介】前面实验已经知道&#xff0c;FortiClient客户端拨号到远端防火墙&#xff0c;包括上网流量等所有流量都可以通过隧道到达远端防火墙&#xff0c;并从对方宽带上网。那么两台防火墙之间连接的安全隧道&#xff0c;可以实现这个功能吗&#xff1f; 实验要求与环境 OldMe…