0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

C语言内存问题如何解决

科技绿洲 来源:嵌入式大杂烩 作者:嵌入式大杂烩 2023-06-22 11:37 次阅读

大家好,我是杂烩君。

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

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

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

内存,粗略地分:

  • 静态存储区
  • 动态存储区

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

静态存储区踩内存

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

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

动态存储区踩内存

动态堆内存踩内存典型例子: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的简单使用可阅读往期笔记:工具 | 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应该就比较好复现也比较好定位。看代码应该就比较容易定位了,比较难定位的往往是那种踩了一小块的。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内存
    +关注

    关注

    8

    文章

    2996

    浏览量

    73868
  • 文件
    +关注

    关注

    1

    文章

    561

    浏览量

    24692
收藏 人收藏

    评论

    相关推荐

    关于C语言结构体内存对齐

    今天给大家带来一道经典、易错的关于C语言结构体内存对齐的题目:
    的头像 发表于 04-14 12:51 6851次阅读

    关于C语言结构体内存对齐

    今天给大家带来一道经典、易错的关于C语言结构体内存对齐的题目。
    发表于 09-08 11:54 468次阅读

    一文详解C语言内存管理

    C语言内存管理指对系统内存的分配、创建、使用这一系列操作。
    发表于 07-26 16:04 678次阅读
    一文详解<b class='flag-5'>C</b><b class='flag-5'>语言</b><b class='flag-5'>内存</b>管理

    C语言中数组和结构体的内存表示和布局

    C语言中,数组和结构体都可以代表一块内存,但为什么结构体可以直接赋值,而数组不可以?这个问题涉及到C语言的设计哲学、语法规则以及
    发表于 08-28 10:54 1448次阅读

    C语言程序设计中动态内存分配如何实现

    C语言程序设计中,动态内存分配如何实现,需要注意哪些问题?
    发表于 09-28 16:53 1274次阅读

    C语言教程之获取BIOS常规内存容量

    C语言教程之获取BIOS常规内存容量,很好的C语言资料,快来学习吧。
    发表于 04-25 16:43 0次下载

    C语言使用中指针和内存泄漏的问题和解决方案

    引言对于任何使用 C 语言的人,如果问他们 C 语言的最大烦恼是什么,其中许多人可能会回答说是指针和内存泄漏
    的头像 发表于 07-17 16:33 4158次阅读

    C语言内存堆与栈的笔记资料说明

    本文档的主要内容详细介绍的是C语言内存堆与栈的笔记资料说明说明了C语言中堆与栈的区别,哪些数据存放在堆,哪些存放在栈。
    发表于 02-14 08:00 3次下载
    <b class='flag-5'>C</b><b class='flag-5'>语言</b><b class='flag-5'>内存</b>堆与栈的笔记资料说明

    干货 | 嵌入式C语言内存管理

    很多工程师都知道,C/C++语言与其他语言不同,它需要开发者自己管理内存资源,动态内存使用不当,
    的头像 发表于 07-23 14:32 4879次阅读

    C语言中数组在内存中如何表示

    C语言中数组在内存中是怎样表示的,今天就给大家聊聊这个话题。
    的头像 发表于 02-15 14:35 894次阅读
    <b class='flag-5'>C</b><b class='flag-5'>语言</b>中数组在<b class='flag-5'>内存</b>中如何表示

    C语言怎么建立内存的动态分配

    C语言中,全局变量是分配在内存中的静态存储区的,非静态的局部变量,包括形参是分配在内存中的动态存储区的,这个存储区是一个“栈”的区域。
    的头像 发表于 03-10 15:30 824次阅读

    聊聊嵌入式C语言内存的问题

    C 语言内存问题,难在于定位,定位到了就好解决了。
    发表于 06-25 08:59 1577次阅读
    聊聊嵌入式<b class='flag-5'>C</b><b class='flag-5'>语言</b>踩<b class='flag-5'>内存</b>的问题

    何解C语言中的“访问权限冲突”异常?C语言引发异常原因分析

    何解C语言中的“访问权限冲突”异常?C语言引发异常原因分析  在C
    的头像 发表于 01-12 16:03 5016次阅读

    C语言中的动态内存管理讲解

    本章将讲解 C 中的动态内存管理。C 语言内存的分配和管理提供了几个函数。这些函数可以在 头文件中找到。
    的头像 发表于 02-23 14:03 365次阅读
    <b class='flag-5'>C</b><b class='flag-5'>语言</b>中的动态<b class='flag-5'>内存</b>管理讲解

    C语言内存泄漏问题原理

    内存泄漏问题只有在使用堆内存的时候才会出现,栈内存不存在内存泄漏问题,因为栈内存会自动分配和释放。C
    发表于 03-19 11:38 477次阅读
    <b class='flag-5'>C</b><b class='flag-5'>语言</b><b class='flag-5'>内存</b>泄漏问题原理