当前位置:   article > 正文

C 踩内存问题!

内核堆内存被踩

大家好,我是杂烩君。

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

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

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

内存,粗略地分:

  • 静态存储区

  • 动态存储区

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

静态存储区踩内存

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

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

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

出问题的代码如:

  1. float arr[5];
  2. int count = 8;
  3. for (size_t i = 0; i < count; i++)
  4. {
  5.     arr[i] = xxx;
  6. }
e66018ad89199eb7321b3a433f3e800a.png

操作同属于静态存储区的arr数组出现了数组越界操作,踩了后面几个连续变量,fd也踩了。

实际中,纯靠log打印调试很难定位fd的相邻变量,需要花比较多的时间。

在Linux中,这个问题我们可以通过生成生成map文件来查看,在CMakeLists.txt中生成map文件的代码如:

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

动态存储区踩内存

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

  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <unistd.h>
  4. #include <string.h>
  5. int main (void)
  6. {
  7.     char *str = "hello";
  8.     int str_len = strlen(str);
  9.     ///< 此时str_len = 5
  10.     printf("str_len = %d\n", str_len);
  11.     ///< 申请5字节的堆内存
  12.     char *ptr = (char*)malloc(str_len);
  13.     if (NULL == ptr)
  14.     {
  15.         printf("malloc error\n");
  16.         exit(EXIT_FAILURE);
  17.     }
  18.     ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20
  19.     char *p_a = ptr + 5;
  20.     *p_a = 20;
  21.     printf("*p_a = %d\n", *p_a);
  22.     ///< 拷贝字符串str到ptr指向的地址
  23.     strcpy(ptr, str);
  24.     ///< 打印结果:a指向的地方被踩了
  25.     printf("ptr = %s\n", ptr);
  26.     printf("*p_a = %d\n", *p_a);
  27.     ///< 释放对应内存
  28.     if (ptr)
  29.     {
  30.         free(ptr);
  31.         ptr = NULL;
  32.     }
  33.     return 0;
  34. }

运行结果:

db8920f954e1fa148dad1e912f589260.png

显然,经过strcpy操作之后,数据a的值被篡改了。

原因:忽略了strcpy操作会把字符串结束符一同拷贝到目的缓冲区。

3714e95ea53d802cf3ad36fd059f6620.png

如果相邻的空间里没有存放其它业务数据,那么踩了也不会出现问题,如果正好存放了重要数据,这时候可能会出现大bug,而且可能是偶现的,不好复现定位。

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

  • dmalloc

  • valgrind

valgrind的简单使用可阅读往期笔记:工具 | Valgrind仿真调试工具的使用

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

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

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

此处,我们定义:

  • 前红区(before_ red_area):4字节。写入固定数据0x11223344。

  • 后红区(after_ red_area):4字节。写入固定数据0x55667788。

  • 长度区(len_area):4字节。存储数据存储区的长度。

0565fef3546d78f2de06ea2932d505d9.png


自定义申请内存函数

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

void *malloc(size_t __size);

自定义申请内存的函数:

void *Malloc(size_t __size);

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

  1. #define BEFORE_RED_AREA_LEN  (4)            ///< 前红区长度
  2. #define AFTER_RED_AREA_LEN   (4)            ///< 后红区长度
  3. #define LEN_AREA_LEN         (4)            ///< 长度区长度
  4. #define BEFORE_RED_AREA_DATA (0x11223344u)  ///< 前红区数据
  5. #define AFTER_RED_AREA_DATA  (0x55667788u)  ///< 后红区数据
  6. void *Malloc(size_t __size)
  7. {
  8.     ///< 申请内存:4 + 4 + __size + 4
  9.     void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
  10.     if (NULL == ptr)
  11.     {
  12.         printf("[%s]malloc error\n", __FUNCTION__);
  13.         return NULL;
  14.     }
  15.     ///< 往前红区地址写入固定值
  16.     *((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;     
  17.     ///< 往长度区地址写入长度     
  18.     *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;  
  19.     ///< 往后红区地址写入固定值
  20.     *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;  
  21.     ///< 返回数据区地址
  22.     void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
  23.     return data_area_ptr;
  24. }

自定义检测内存函数

申请完内存并往内存里写入数据后,检测本该写入到数据存储区的数据有没有写到红区。这种内存检测方法我们是用在开发调试阶段的,所以检测内存,我们可以使用断言,一旦触发断言,直接终止程序报错。

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

  1. void CheckMem(void *ptr, size_t __size)
  2. {
  3.     void *data_area_ptr = ptr;
  4.     ///< 检测是否踩了前红区
  5.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
  6.     assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);
  7.     ///< 检测是否踩了长度区
  8.     printf("[%s]len_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
  9.     assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size); 
  10.     ///< 检测是否踩了后红区
  11.     printf("[%s]after_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
  12.     assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA); 
  13. }

自定义释放内存函数

要释放所有前面申请内存。释放前同样要进行检测:

  1. void Free(void *ptr)
  2. {
  3.     void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;
  4.     ///< 检测是否踩了前红区
  5.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
  6.     assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);
  7.     ///< 读取长度区内容
  8.     size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));
  9.     ///< 检测是否踩了后红区
  10.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
  11.     assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);
  12.     ///< 释放所有区域内存
  13.     free(all_area_ptr);
  14. }

我们使用这种方法检测上面的 malloc与strcpy搭配使用不当导致缓冲区溢出 的例子:

7c2ae88e973a36d08a86c8df912d6841.png

可以看到,这个例子踩了后红区,把后红区数据修改为了 0x55667700 ,触发断言程序终止。

测试代码:

  1. // 公众号:嵌入式大杂烩
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <unistd.h>
  5. #include <string.h>
  6. #include <assert.h>
  7. #define BEFORE_RED_AREA_LEN  (4)            ///< 前红区长度
  8. #define AFTER_RED_AREA_LEN   (4)            ///< 后红区长度
  9. #define LEN_AREA_LEN         (4)            ///< 长度区长度
  10. #define BEFORE_RED_AREA_DATA (0x11223344u)  ///< 前红区数据
  11. #define AFTER_RED_AREA_DATA  (0x55667788u)  ///< 后红区数据
  12. void *Malloc(size_t __size)
  13. {
  14.     ///< 申请内存:4 + 4 + __size + 4
  15.     void *ptr = malloc(BEFORE_RED_AREA_LEN + AFTER_RED_AREA_LEN + __size + LEN_AREA_LEN);
  16.     if (NULL == ptr)
  17.     {
  18.         printf("[%s]malloc error\n", __FUNCTION__);
  19.         return NULL;
  20.     }
  21.     ///< 往前红区地址写入固定值
  22.     *((unsigned int*)(ptr)) = BEFORE_RED_AREA_DATA;     
  23.     ///< 往长度区地址写入长度     
  24.     *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN)) = __size;  
  25.     ///< 往后红区地址写入固定值
  26.     *((unsigned int*)(ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) = AFTER_RED_AREA_DATA;  
  27.     ///< 返回数据区地址
  28.     void *data_area_ptr = (ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN);
  29.     return data_area_ptr;
  30. }
  31. void CheckMem(void *ptr, size_t __size)
  32. {
  33.     void *data_area_ptr = ptr;
  34.     ///< 检测是否踩了前红区
  35.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)));
  36.     assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN)) == BEFORE_RED_AREA_DATA);
  37.     ///< 检测是否踩了长度区
  38.     printf("[%s]len_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr - LEN_AREA_LEN)));
  39.     assert(*((unsigned int*)(data_area_ptr - LEN_AREA_LEN)) == __size); 
  40.     ///< 检测是否踩了后红区
  41.     printf("[%s]after_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(data_area_ptr + __size)));
  42.     assert(*((unsigned int*)(data_area_ptr + __size)) == AFTER_RED_AREA_DATA); 
  43. }
  44. void Free(void *ptr)
  45. {
  46.     void *all_area_ptr = ptr - LEN_AREA_LEN - BEFORE_RED_AREA_LEN;
  47.     ///< 检测是否踩了前红区
  48.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr)));
  49.     assert(*((unsigned int*)(all_area_ptr)) == BEFORE_RED_AREA_DATA);
  50.     ///< 读取长度区内容
  51.     size_t __size = *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN));
  52.     ///< 检测是否踩了后红区
  53.     printf("[%s]before_red_area_data = 0x%x\n", __FUNCTION__, *((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)));
  54.     assert(*((unsigned int*)(all_area_ptr + BEFORE_RED_AREA_LEN + LEN_AREA_LEN + __size)) == AFTER_RED_AREA_DATA);
  55.     ///< 释放所有区域内存
  56.     free(all_area_ptr);
  57. }
  58. int main (void)
  59. {
  60.     char *str = "hello";
  61.     int str_len = strlen(str);
  62.     ///< 此时str_len = 5
  63.     printf("str_len = %d\n", str_len);
  64.     ///< 申请5字节的堆内存
  65.     char *ptr = (char*)Malloc(str_len);    ///< 自定义的Malloc
  66.     if (NULL == ptr)
  67.     {
  68.         printf("malloc error\n");
  69.         exit(EXIT_FAILURE);
  70.     }
  71.     ///< 定义一个指针p_a指向ptr向后偏移5字节的地址, 并在这个地址里写入整数20
  72.     char *p_a = ptr + 5;
  73.     *p_a = 20;
  74.     printf("*p_a = %d\n", *p_a);
  75.     ///< 拷贝字符串str到ptr指向的地址
  76.     strcpy(ptr, str);
  77.     ///< 操作完堆内存之后,要检测写入操作有没有踩到红区
  78.     CheckMem(ptr, str_len);
  79.     ///< 打印结果:a指向的地方被踩了
  80.     printf("ptr = %s\n", ptr);
  81.     printf("*p_a = %d\n", *p_a);
  82.     ///< 释放对应内存
  83.     if (ptr)
  84.     {
  85.         Free(ptr);
  86.         ptr = NULL;
  87.     }
  88.     return 0;
  89. }

没有踩内存的情况:

24b56eb99b07d2f90206f96ebd28583f.png

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

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

相关资料:

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

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

注意

由于微信公众号近期改变了推送规则,为了防止找不到,可以星标置顶,这样每次推送的文章才会出现在您的订阅列表里。

ae15b2717ed11c0f26b815a10ad9be26.png

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/IT小白/article/detail/152566
推荐阅读
相关标签
  

闽ICP备14008679号