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

news/2024/5/7 11:57:12/文章来源:https://blog.csdn.net/zhi_Alanwu/article/details/131260391

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 = 5printf("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字节的地址, 并在这个地址里写入整数20char *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 + 4void *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 + 4void *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 = 5printf("str_len = %d\n", str_len);///< 申请5字节的堆内存char *ptr = (char*)Malloc(str_len);    ///< 自定义的Mallocif (NULL == ptr){printf("malloc error\n");exit(EXIT_FAILURE);}///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20char *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.luyixian.cn/news_show_319302.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前端开发中遇到的小bug--解决方案

1.在 searchBox 搜索栏中&#xff0c;用到了多级下拉框的筛选条件&#xff0c;样式如下&#xff1a; 这样看起来是没什么问题的&#xff0c;但当我选择时&#xff0c;在框中显示的内容和筛选条件的内容就出错了&#xff1a; 这里其实是选择了 采矿业 -- 石油和天然气开采业 &am…

数据库系统概述——第三章 关系数据库标准语言SQL(知识点复习+练习题)

&#x1f31f;博主&#xff1a;命运之光 &#x1f984;专栏&#xff1a;离散数学考前复习&#xff08;知识点题&#xff09; &#x1f353;专栏&#xff1a;概率论期末速成&#xff08;一套卷&#xff09; &#x1f433;专栏&#xff1a;数字电路考前复习 &#x1f99a;专栏&am…

自动化测试如何区分用例集合?你一定要知道

目录 前言 业务量和复杂度增长现状是什么&#xff1f; 如何区分自动化测试的用例集合&#xff1f; 区分用例集合的过程要注意什么&#xff1f; 总结&#xff1a; 前言 有同学在后台问到&#xff1a;业务比较复杂&#xff0c;有很多串行并行甚至组合的业务场景&#xff0c;执…

Opencv项目实战:23 智能计数和表单信息

目录 0、项目介绍 1、效果展示 2、项目搭建 3、项目代码展示与部分讲解 拍照脚本data_collection.py 图片检测Picdetect.py 摄像头检测Videodetect.py 主函数CountMain.py 自定义模块tally.py 4、项目资源 5、项目总结 0、项目介绍 有一段时间没有更新专栏了&#…

软件测试入门篇

软件测试含义 在规定条件下对程序进行操作&#xff0c;发现软件错误&#xff0c;衡量软件质量&#xff0c;对其是否能满足设计要求进行评估的过程 开发不做测试原因&#xff1a;测试力度&#xff0c;思维方式&#xff0c;关注度 计算机定义 一种可以自动高效进行技术操作的…

vue引入jszip下载多个图片并压缩下载

vue引入jszip下载多个图片并压缩下载 jszip官网地址 先进行jszip下载 npm install jszip然后废话不多说直接上代码 <template><div><button click"downloadImages">下载图片</button></div> </template><script> impo…

Prompt的技巧持续总结

Prompt 有很多网站已经收录了&#xff0c;比如&#xff1a;aimappro 有些直接抄上述网站的作业即可&#xff0c;不过也来看看&#xff0c; 有一些日常提问大概的咒语该怎么写。 1 三种微调下的提示写法 chatgpt时代的创新&#xff1a;LLM的应用模式比较 实际案例说明AI时代大…

《Stable Diffusion WebUI折腾实录》在Windows完成安装, 从社区下载热门模型,批量生成小姐姐图片

环境 操作系统: Windows11 显卡: RTX2060 6GB 显存 安装Python 下载 Python3.10.6 https://www.python.org/ftp/python/3.10.6/python-3.10.6-amd64.exe安装 注意勾选 Add Python 3.10.6 to PATH &#xff0c;然后一路下一步即可 打开powershell&#xff0c; 确认安装成功 …

#10035. 「一本通 2.1 练习 1」Power Strings

Power Strings 题意简述&#xff1a; 求一个字符串由多少个重复的子串连接而成。 例如 ababab 由三个 ab 连接而成&#xff0c;abcd 由 abcd 由一个 abcd 连接而成。 输入格式 本题多组数据。 每一组数据仅有一行&#xff0c;这一行仅有一个字符串 s s s。 输入的结束标…

IT云运维技术分享

1 运维体系 1.1 市场对运维的需求 时代发展到今天&#xff0c;社会的生活方式与生产方式的全面的数字化&#xff0c;无论是传统企业还是互联网企业&#xff0c;都在全面上云&#xff0c;这也意味着企业的关键业务乃至“身家性命”都已经全部放在 IT 系统之上&#xff0c;因此…

VMware Integrated OpenStack 7.3 - 支持 vSphere 8.0U1 和 NSX 4.1 并向下兼容

VMware Integrated OpenStack 7.3 - 支持 vSphere 8.0U1 和 NSX 4.1 并向下兼容 VMware 支持的 OpenStack 发行版&#xff1a;在 VMware 虚拟化技术之上运行企业级 OpenStack 云 请访问原文链接&#xff1a;https://sysin.org/blog/vmware-vio-7/&#xff0c;查看最新版。原创…

android 如何分析应用的内存(八)——Android 7.0以后的malloc debug

android 如何分析应用的内存&#xff08;八&#xff09; 接上文&#xff0c;介绍六大板块中的第三个————malloc调试和libc回调 上一篇文章中&#xff0c;仅仅是在分配和释放的时候&#xff0c;拦截对应的操作。而不能进一步的去检查内存问题。比如&#xff1a;释放之后再…

智能单相电能表

智能单相电能表是一种基于嵌入式系统和电子技术的智能化电能表&#xff0c;具有多种功能和特点&#xff0c;下面是关于智能单相电能表的介绍。 一、工作原理 智能单相电能表采用电子技术和嵌入式系统实现电能测量的智能化和自动化。它包括电压采样装置、电流采样装置、电能计算…

B/S版医院检验科lis系统源码 云lis系统

LIS系统为实验室服务对象提供检验申请、采集标本、结果查询等功能&#xff1b;为实验室工作人员的核收标本、分送标本、传送资料、分析前处理、质量控制、单向或双向通讯、分析后处理、结果审核、打印报告、结果查询等标本检测过程提供全面的技术支持。 .Net Core LIS系统源码…

Git 多账号多仓库配置 SSH

前言 在我们使用 Git 中&#xff0c;有时候会遇到多账号多仓库的情况&#xff0c;比如公司的 GitLab 和 GitHub&#xff0c;以及自己的 GitHub&#xff0c;这时候我们就需要配置多个 SSH 密钥来区分不同的账号和仓库 生成 SSH 密钥 根据你注册仓库的邮箱生成 SSH 密钥&#…

centos下的Nginx的安装

1.Nginx简介 Nginx是一款轻量级的Web 服务器/反向代理服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。其特点是占有内存少&#xff0c;并发能力强。 其他服务器介绍&#xff1a;Apache服务器、Tomcat服务器、Lighttpd服务器 2.nginx依赖安装 yum -y instal…

PSINS工具箱学习(一)下载安装初始化、SINS-GPS组合导航仿真、习惯约定与常用变量符号、数据导入转换、绘图显示

文章目录 一、前言二、相关资源三、下载安装初始化1、下载PSINSyymmdd.rar工具箱文件2、解压文件3、初始化4、启动工具箱导览 四、习惯约定与常用变量符号1、PSINS全局变量结构体 glv2、坐标系定义3、姿态阵/姿态四元数/欧拉角 Cnb/qnb/att4、IMU采样数据 imu5、AVP导航参数 av…

【MySQL 日志管理、备份与恢复】

目录 一、数据库备份的分类1、从物理与逻辑的角度1.1、物理备份: 对数据库操作系统的物理文件&#xff08;如数据文件&#xff0c;日志文件等&#xff09;的备份1.2、逻辑备份 2、从数据库的备份策略角度3、常见的备份方法3.1、物理冷备3.2、专用备份工具mysqldump 或者 mysqlh…

使用Channel的一些业务场景

使用Channel的一些业务场景 首先需要明确的就是&#xff0c;发送方才知道什么时候关闭 channel &#xff0c;这个是比较符合逻辑的。 我们需要知道哪些情况会使 channel 发生 panic 关闭一个 nil 值会引发关闭一个已经关闭的 channel 会引发向一个已经关闭的 channel 发送数据…

破圈丨2023年绿色积分消费返利:云联惠3.0升级版【循环购】商业模式

破圈丨2023年绿色积分消费返利&#xff1a;云联惠3.0升级版【循环购】商业模式 京东供应链商品/自营商品/供应商商品 平台上面产品超过300w款产品&#xff0c;均为京东供应链货品&#xff0c;由京东统一仓储和配送&#xff0c;从源头上面杜绝假冒伪劣产品的存在&#xff0c;然…