首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

C 语言内存问题,难在于定位,定位到了就好解决了。

这篇笔记我们来聊聊踩内存。踩内存,通过字面理解即可。本来是操作这一块内存,因为设计失误操作到了相邻内存,篡改了相邻内存的数据。

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

内存,粗略地分:

静态存储区

动态存储区

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

静态存储区踩内存

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

在Linux中,一个进程默认可以打开的文件数为1024个,fd的范围为0~1023。

项目中使用了串口,串口fd为static全局变量,某次这个fd突然变为一个超范围得值,显然被踩了。

出问题的代码如:

float?arr[5];

int?count?=?8;

for?(size_t?i?=?0;?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")

动态存储区踩内存

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

#include?

#include?

#include?

#include?

int?main?(void)

{

char?*str?=?"hello";

int?str_len?=?strlen(str);

///

printf("str_len?=?%d\n",?str_len);

///

char?*ptr?=?(char*)malloc(str_len);

if?(NULL?==?ptr)

{

printf("malloc?error\n");

exit(EXIT_FAILURE);

}

///

char?*p_a?=?ptr?+?5;

*p_a?=?20;

printf("*p_a?=?%d\n",?*p_a);

///

strcpy(ptr,?str);

///

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的简单使用可阅读往期笔记:工具 | Valgrind仿真调试工具的使用

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

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

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

此处,我们定义:

长度区(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)

{

///

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?

#include?

#include?

#include?

#include?

#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)

{

///

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);

///

printf("str_len?=?%d\n",?str_len);

///

char?*ptr?=?(char*)Malloc(str_len);????///

if?(NULL?==?ptr)

{

printf("malloc?error\n");

exit(EXIT_FAILURE);

}

///

char?*p_a?=?ptr?+?5;

*p_a?=?20;

printf("*p_a?=?%d\n",?*p_a);

///

strcpy(ptr,?str);

///

CheckMem(ptr,?str_len);

///

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

注意

  • 发表于:
  • 原文链接https://page.om.qq.com/page/Olr5buQGxWr0XQTmOP1QMr4Q0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券
http://www.vxiaotou.com