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

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 #include #include #include int main (void){    char *str = hello;    int str_len = strlen(str);    ///< 此时str_len = 5    printf(str_len = %d, str_len);    ///< 申请5字节的堆内存    char *ptr = (char*)malloc(str_len);    if (null == ptr)    {        printf(malloc error);        exit(exit_failure);    }    ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20    char *p_a = ptr + 5;    *p_a = 20;    printf(*p_a = %d, *p_a);    ///< 拷贝字符串str到ptr指向的地址    strcpy(ptr, str);    ///< 打印结果:a指向的地方被踩了    printf(ptr = %s, ptr);    printf(*p_a = %d, *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, __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, __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, __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, __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, __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, __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){    ///< 申请内存: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, __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, __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, __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, __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, __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, __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, str_len);    ///< 申请5字节的堆内存    char *ptr = (char*)malloc(str_len);    ///< 自定义的malloc    if (null == ptr)    {        printf(malloc error);        exit(exit_failure);    }    ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20    char *p_a = ptr + 5;    *p_a = 20;    printf(*p_a = %d, *p_a);    ///< 拷贝字符串str到ptr指向的地址    strcpy(ptr, str);    ///< 操作完堆内存之后,要检测写入操作有没有踩到红区    checkmem(ptr, str_len);    ///< 打印结果:a指向的地方被踩了    printf(ptr = %s, ptr);    printf(*p_a = %d, *p_a);    ///< 释放对应内存    if (ptr)    {        free(ptr);        ptr = null;    }    return 0;}  
没有踩内存的情况:
本例只是简单分享了检测堆内存踩数据的一种检测思路,例子代码不具备通用性。比如,万一踩的内存不只是相邻的几个字节,而是踩了相邻的一大片,这时候就跨过了红区,而不是踩在红区上。
红区大小由我们自己设定,我们可以设得大些。如果设得很大了都能跨过,这种情况bug应该就比较好复现也比较好定位。看代码应该就比较容易定位了,比较难定位的往往是那种踩了一小块的。


iOS 12.3.1正式版“意外”推送,以修复BUG和优化系统为主
拉普拉斯变换的本质意义
科学家受昆虫的“外骨骼”启发 使用3D打印机制作出柔性骨架机器人
10nm工艺+6GB LPDDR4内存!三星Galaxy Note 6实力爆表
使用最新硬件推动工厂的物流自动化
聊聊嵌入式C语言踩内存的问题
芯华章:让面向未来的EDA技术诞生在中国
5G基站开始建成了吗
LDO的类型及其工作原理
中车电动为新能源汽车产业注强“芯”剂
用于ECU的传感器模块、执行器等BOM风险评估
iPhone14来了!苹果官宣发布会时间
ST STP4CMP带电荷泵四路LED驱动解决方案
小型空气监测系统是什么
无功功率补偿介绍
移动在四省完成SDN单层控制器纳管多厂商设备现网试点
详解ChatGPT数据集之谜
太阳能光热发电控制技术研究
5G时代,运营商的日子不好过
SiC功率器件的主要特点