打印
[APM32F4]

嵌入式c语言开发:关于“踩内存”,你知道多少?

[复制链接]
172|0
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
DKENNY|  楼主 | 2024-12-12 20:22 | 只看该作者 |只看大图 回帖奖励 |倒序浏览 |阅读模式
本帖最后由 DKENNY 于 2024-12-12 20:21 编辑

#申请原创# #技术资源# @21小跑堂

前言

      在编程中,尤其是使用C语言时,内存错误常常令人头痛。一旦找出这些错误,问题通常容易解决。今天我们聚焦于“踩内存”的现象做个简单的分析。
      “踩内存”意味着由于程序设计或实现的问题,意外修改了相邻的内存区域。原本应该处理特定内存块,但误操作使得相邻内存数据被篡改。此问题可能导致程序功能失常,甚至使程序崩溃或系统挂起。
      内存可以大致分为两个部分:
      l 静态存储区
      l 动态存储区
      “踩内存”通常发生在同一存储区内的数据之间。

一、静态存储区互踩
      静态存储区互踩通常是指在静态存储区域中,多个变量之间由于不当的内存管理或对同一内存区域的错误引用而导致的数据覆盖和错误。
      在C语言中,静态存储区互踩的情况主要可以归结为以下几种常见的情况。
        l 同名静态变量互踩
        l 数组越界
        l 静态结构体中变量互踩
        l 静态变量在循环中越界
        l 静态变量的指针互踩

1. 同名静态变量互踩
#include <stdio.h>

void func1()
{
    static int value = 0; // 静态变量 value
    value++;
    printf("func1: value = %d\n", value);
}

void func2()
{
    static int value = 10; // 静态变量 value,和 func1 中的 value 名称相同
    value++;
    printf("func2: value = %d\n", value);
}

int main()
{
    func1(); // 输出: 1
    func2(); // 输出: 11
    func1(); // 输出: 2
    func2(); // 输出: 12
    return 0;
}
     分析:在这个例子中,func1 和 func2 中都有同名的静态变量 value。虽然它们在不同的作用域中,但由于同名而导致互相影响。每次调用时,func1 和 func2 分别引用同名的静态变量,可能导致逻辑混乱。
      解决方案:使用更具描述性的变量名,避免同名。

2. 数组越界
#include <stdio.h>

void func()
{
    static int arr[3]; // 静态数组 arr
    static int b = 10; // 静态变量 b

    // 越界赋值
    for (int i = 0; i <= 3; i++)
    { // 注意这里的循环条件是 <= 3,导致越界
        arr[i] = i * 2; // 尝试赋值
    }

    printf("arr[0] = %d, arr[1] = %d, arr[2] = %d, b = %d\n", arr[0], arr[1], arr[2], b);
}

int main()
{
    func(); // 调用 func
    return 0;
}
     分析:在这个示例中,数组 arr 被越界访问,arr[3] 可能覆盖静态变量 b 的值。这会导致未定义行为及错误的输出。
      解决方案:确保数组访问时索引在合法范围内。

3. 静态结构体中变量互踩
#include <stdio.h>

struct Data
{
    static int count; // 静态结构体成员
};

int Data::count = 0; // 初始化

void func1()
{
    Data::count++;
    printf("func1: count = %d\n", Data::count);
}

void func2()
{
    Data::count += 10; // 同一静态成员
    printf("func2: count = %d\n", Data::count);
}

int main()
{
    func1(); // 输出: 1
    func2(); // 输出: 11
    func1(); // 输出: 2
    func2(); // 输出: 12
    return 0;
}
     分析:在这个例子中,静态结构体成员 count 被 func1 和 func2 同时修改,导致它们之间的值互相影响。
      解决方案:避免使用同一结构体的静态成员,使用实例化对象。

4. 静态变量在循环中越界
#include <stdio.h>

void func()
{
    static int arr[3]; // 静态数组 arr
    for (int i = 0; i < 5; i++)
    { // 越界访问
        arr[i] = i; // 越界写入
    }
}

int main()
{
    func(); // 调用 func
    return 0;
}
     分析:这里传入的数组大小是3,但在循环中却写入了5个元素。arr[3] 和 arr[4] 可能会覆盖其他静态或全局变量,导致数据损坏。
      解决方案:确保循环条件不超过数组大小。

5. 静态变量的指针互踩
#include <stdio.h>

void func()
{
    static int *ptr1 = NULL; // 指向静态变量的指针
    static int *ptr2 = NULL; // 另一个指针

    static int a = 5; // 静态变量
    static int b = 10; // 另一个静态变量

    ptr1 = &a; // 指向 a
    ptr2 = &b; // 指向 b

    *ptr1 = 20; // 修改 a
    *ptr2 = 30; // 修改 b

    printf("a = %d, b = %d\n", a, b);
}

int main()
{
    func(); // 调用 func
    return 0;
}
     分析:在这个例子中,静态变量 a 和 b 被指针 ptr1 和 ptr2 修改。如果不小心,指针可能指向错误的内存区域,从而导致互踩的问题。
      解决方案:在使用指针时,确保它们指向正确的内存地址,并避免使用多个指向同一静态变量的指针。

总结
      静态存储区互踩的情况主要由于变量同名、数组越界、指针错误等引起。在编写C代码时,应该注意变量的作用域和生命周期,确保不发生意外的内存覆盖和数据损坏,避免使用同名变量,并严格控制数组的访问范围。

二、动态存储区踩内存
      动态存储区踩内存通常指在使用动态内存时发生的错误,如内存越界、内存泄漏、悬挂指针等。下面列出几种常见的情况,并提供C语言代码示例和详细分析。

1. 内存越界访问
#include <stdio.h>
#include <stdlib.h>

void access_out_of_bounds()
{
    int *arr = (int *)malloc(3 * sizeof(int)); // 动态分配3个整数的内存
    // 越界写入
    for (int i = 0; i <= 3; i++)
    { // 注意这里的循环条件是 <= 3,导致越界
        arr[i] = i; // 尝试访问 arr[3]
    }
    free(arr);
}

int main()
{
    access_out_of_bounds(); // 调用函数
    return 0;
}
    分析:
       l 在这个例子中,malloc 分配了3个整数的内存,但在循环中使用了 i<=3,导致尝试访问 arr[3],这是未分配的内存区域,可能会导致未定义行为。
     后果:
       l 数据损坏:可能会覆盖其他有效内存区域的数据,导致数据完整性问题。
       l 程序崩溃:如果越界访问了操作系统保留的内存或未分配的内存,可能引发段错误(segmentation fault)。

2. 内存泄漏
#include <stdio.h>
#include <stdlib.h>

void memory_leak()
{
    int *ptr = (int *)malloc(5 * sizeof(int)); // 动态分配内存
    // 忘记释放内存
    // free(ptr); // 应该释放内存,但这里被注释掉
}

int main()
{
    memory_leak(); // 调用函数
    return 0;
}
    分析:
       l 在这个示例中,malloc 分配了5个整数的内存,但没有调用 free 来释放它。这将导致内存泄漏。
     后果:
       l 内存耗尽:在长时间运行的程序中,如果频繁发生内存泄漏,可能导致系统内存耗尽,最终导致程序崩溃或系统变得不稳定。
       l 性能降低:内存泄漏会导致可用内存减少,从而影响程序的性能。

3. 悬挂指针
#include <stdio.h>
#include <stdlib.h>

void dangling_pointer()
{
    int *ptr = (int *)malloc(sizeof(int)); // 动态分配内存
    *ptr = 42; // 设置值
    free(ptr); // 释放内存
    // 访问已释放的内存
    printf("Value: %d\n", *ptr); // 悬挂指针
}

int main()
{
    dangling_pointer(); // 调用函数
    return 0;
}
    分析:
       l 在这个例子中,ptr 指向动态分配的内存,在调用 free后,该内存被释放,但 ptr 仍然指向原来的地址。
     后果:
       l 未定义行为:访问已释放的内存将导致未定义行为,程序可能崩溃,或者返回意外的值。
       l 数据损坏:如果其他部分的代码分配了相同的内存,可能会导致 ptr 中的数据被覆盖。

4. 双重释放
#include <stdio.h>
#include <stdlib.h>

void double_free()
{
    int *ptr = (int *)malloc(sizeof(int)); // 动态分配内存
    free(ptr); // 释放内存
    free(ptr); // 再次释放相同的指针,导致双重释放
}

int main()
{
    double_free(); // 调用函数
    return 0;
}
    分析:
       l 在这个示例中,free(ptr) 被调用两次。第一次释放了指针所指向的内存,但第二次释放将导致未定义行为。
     后果:
       l 程序崩溃:双重释放通常会导致程序崩溃(如段错误),因为它试图释放不再有效的内存区域。

5. 未初始化的指针
#include <stdio.h>
#include <stdlib.h>

void uninitialized_pointer()
{
    int *ptr; // 声明,但未初始化
    // 直接使用未初始化的指针
    *ptr = 10; // 访问未定义的内存
}

int main()
{
    uninitialized_pointer(); // 调用函数
    return 0;
}
    分析:
       l 在这个例子中,ptr 被声明但未初始化。使用未初始化的指针访问内存会导致未定义行为。
     后果:
       l 未定义行为:访问未初始化的指针会导致程序崩溃,返回随机值,或读取到不该访问的内存区域。
       l 数据损坏:如果该指针指向随机内存,可能会覆盖有效的数据,导致严重的数据完整性问题。

示例代码分析
      以下是一个类似的动态踩内存示例,我们将创建一个动态数组并故意越界写入,从而引发内存篡改。然后,我们将分析代码,指出篡改的数据,并提供一些检测和防止动态存储区被踩内存的方法。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 10
#define REDZONE_SIZE 4

int main(void)
{
    // 申请 BUFFER_SIZE 字节的堆内存
    char *buffer = (char *)malloc(BUFFER_SIZE + REDZONE_SIZE); // 加上红区
    if (buffer == NULL)
    {
        printf("malloc error\n");
        exit(EXIT_FAILURE);
    }

    // 设置红区的值
    memset(buffer + BUFFER_SIZE, 0xAA, REDZONE_SIZE); // 红区填充为 0xAA

    // 向 buffer 写入数据
    strcpy(buffer, "Hello, World!"); // 这是一个越界写入

    // 打印内容
    printf("buffer = %s\n", buffer);
   
    // 打印红区内容
    for (int i = 0; i < REDZONE_SIZE; i++)
    {
        printf("buffer[%d] = 0x%X\n", BUFFER_SIZE + i, (unsigned char)buffer[BUFFER_SIZE + i]);
    }

    // 释放对应内存
    free(buffer);
    return 0;
}
    分析
     l 变量定义与内存分配:
         ¡ 我们分配了 BUFFER_SIZE 字节的内存来存储字符串,并额外分配了 REDZONE_SIZE 字节作为红区,用于检测内存越界。
     l 越界写入:
         ¡ 使用 strcpy 将超出 buffer 大小的字符串拷贝到动态分配的内存中。这将导致对红区的覆盖,破坏其内容。
     l 输出结果:
         ¡ 程序输出了 buffer 的内容和红区的值。由于越界写入,红区的内容可能被修改,导致其不再是 0xAA。

检测动态存储区被踩内存的方法
      为了解决和检测动态存储区的内存踩踏问题,可以采取以下步骤:
      a. 使用红区:
        l 在动态分配的内存块之后添加一个红区。红区的大小应足够大,可以通过简单的 memset 或其他方法初始化为特定的值(如 0xAA)。
        l 在释放内存前,检查红区的值,以确保其未被修改。
      b. 内存覆盖检测:
        l 在使用内存块之前,检查红区的内容。如果红区的内容被破坏,则表明存在越界写入或其他类型的内存篡改。
      c. 例子中的实现:
        l 在示例代码中,我们对红区进行了填充,并在打印时输出了红区的内容。如果红区的内容变为其它值(如 0x00 或其它),则表示内存被篡改。

设置红区的作用
      设置红区(Red Zone)是一种有效的内存保护技术,通常用于检测动态内存分配中的越界写入和篡改。设置两块红区(前红区和后红区)可以更加全面地保护内存区域。以下是具体分析:
      l 检测越界写入:
        ¡ 红区的主要作用是检测越界写入。如果程序试图写入超过分配的内存块,那么写入的内容可能会覆盖红区的内容。通过监测红区的状态,可以及时发现越界错误。
      l 检测未定义行为:
        ¡ 除了越界写入,红区还可以帮助检测其他未定义行为,例如双重释放或悬挂指针等。通过检查红区的状态,可以判断是否有内存被错误地访问。
      l 提高安全性:
        ¡ 在安全性要求较高的程序中,红区可以帮助防止缓冲区溢出(buffer overflow)攻击,增加程序的安全性。
      l 调试和测试:
        ¡ 在开发和测试阶段,通过在动态分配内存时使用红区,可以更容易地发现和修复内存管理错误。

设置前红区和后红区的好处
     l 双向保护:
       ¡ 设置前红区(在分配的内存之前)和后红区(在分配的内存之后)可以提供更全面的保护。这样无论是对内存块的前面还是后面进行越界写入,都能被有效检测到。
     l 检测不同类型的错误:
       ¡ 前红区可以用于检测前向越界写入,而后红区可以用于检测后向越界写入。这样可以捕获更广泛的错误类型。
     l 栈和堆的整合:
       ¡ 在一些复杂的数据结构中(例如链表、树等),可能会有多个指针相互指向,设置双红区可以减少因指针错误引起的意外内存访问。

红区配置步骤
      a. 定义红区的大小:
        l 通常来说,红区的大小可以设定为几个字节(如4字节或8字节),以便在动态分配内存时能够明显检测内存越界的情况。
      b. 修改动态内存分配函数:
        l 重写 malloc 和 free 函数,以便在分配和释放内存时同时处理红区。
      c. 初始化红区:
        l 在分配内存后,可以使用 memset 函数将红区的内容初始化为特定的值(如0xAA )。
      d. 检测红区:
        l 在释放内存或使用内存前,检查红区的内容,以确保其未被修改。

      以下是一个示例,演示如何在动态分配的内存前后设置红区。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUFFER_SIZE 10
#define REDZONE_SIZE 4

void check_redzones(char *buffer)
{
    // 检查前红区
    for (int i = 0; i < REDZONE_SIZE; i++)
    {
        if (buffer[i] != 0xAA)
        {
            printf("Memory corruption detected in front redzone!\n");
            return;
        }
    }
    // 检查后红区
    for (int i = 0; i < REDZONE_SIZE; i++)
    {
        if (buffer[BUFFER_SIZE + REDZONE_SIZE + i] != 0xAA)
        {
            printf("Memory corruption detected in back redzone!\n");
            return;
        }
    }
    printf("Both redzones are intact.\n");
}

int main(void)
{
    // 申请 BUFFER_SIZE 字节的堆内存,并加上前后红区
    char *buffer = (char *)malloc(BUFFER_SIZE + 2 * REDZONE_SIZE);
    if (buffer == NULL)
    {
        printf("malloc error\n");
        exit(EXIT_FAILURE);
    }

    // 设置前红区和后红区的值
    memset(buffer, 0xAA, REDZONE_SIZE); // 前红区
    memset(buffer + BUFFER_SIZE + REDZONE_SIZE, 0xAA, REDZONE_SIZE); // 后红区

    // 向 buffer 写入数据
    strcpy(buffer + REDZONE_SIZE, "Hello"); // 向有效内存写入
    // strcpy(buffer + REDZONE_SIZE, "Hello, World!"); // 这会越界写入

    // 检测红区
    check_redzones(buffer);

    // 打印内容
    printf("buffer = %s\n", buffer + REDZONE_SIZE);
   
    // 打印前后红区内容
    for (int i = 0; i < REDZONE_SIZE; i++)
    {
        printf("front redzone[%d] = 0x%X\n", i, (unsigned char)buffer[i]);
        printf("back redzone[%d] = 0x%X\n", i, (unsigned char)buffer[BUFFER_SIZE + REDZONE_SIZE + i]);
    }

    // 释放对应内存
    free(buffer);
    return 0;
}

APM32开发时,怎么配置红区?
      以下是一个简单的示例,演示如何在APM32中设置红区。
      l 前红区:4字节。写入固定数据0xAAAAAAAA。
      l 后红区:4字节。写入固定数据0xAAAAAAAA。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define REDZONE_SIZE 4  // 红区大小
#define BUFFER_SIZE 10  // 动态分配的内存大小

// 自定义内存分配函数
void* my_malloc(size_t size)
{
    // 分配额外的内存用于红区
    char *ptr = (char *)malloc(size + 2 * REDZONE_SIZE);
    if (ptr == NULL)
    {
        return NULL; // 处理内存分配错误
    }
   
    // 初始化前红区
    memset(ptr, 0xAA, REDZONE_SIZE);
    // 初始化后红区
    memset(ptr + size + REDZONE_SIZE, 0xAA, REDZONE_SIZE);
   
    // 返回有效内存的起始地址
    return ptr + REDZONE_SIZE;
}

// 自定义内存释放函数
void my_free(void* ptr)
{
    if (ptr == NULL) return;
   
    // 找到原始指针
    char *original_ptr = (char *)ptr - REDZONE_SIZE;
   
    // 检查红区
    for (int i = 0; i < REDZONE_SIZE; i++)
    {
        if (original_ptr[i] != 0xAA)
        {
            printf("Memory corruption detected in front redzone!\n");
            break;
        }
    }
   
    // 释放内存
    free(original_ptr);
}

int main(void)
{
    // 使用自定义的内存分配函数
    char *buffer = (char *)my_malloc(BUFFER_SIZE);
    if (buffer == NULL)
    {
        printf("Memory allocation failed\n");
        return -1;
    }
   
    // 使用分配的内存
    strcpy(buffer, "Hello");
    printf("Buffer: %s\n", buffer);
   
    // 释放内存
    my_free(buffer);
   
    return 0;
}
     代码分析
      a. 自定义内存分配函数 :
        l 使用分配比请求的大小大的内存,以便为前后红区留出空间。
        l 使用初始化前红区和后红区为特定值(如)。
        l 返回指向有效内存的指针(即跳过前红区)。
      b. 自定义内存释放函数 :
        l 计算原始指针(在红区之前的指针)。
        l 检查前红区的内容,确认是否完整。如果被篡改,输出错误信息。
        l 最后,释放原始指针的内存。
      c. 使用示例:
        l 在函数中,调用分配动态内存,并使用释放。

总结
      l 设置前红区和后红区可以提供双向保护,对于检测动态内存的越界和篡改非常有效。
      l 红区的作用十分重要,不仅可以检测越界、未定义行为,还能提高程序的安全性和调试能力。


使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

37

主题

63

帖子

6

粉丝