Lenguaje C (14) ¡Hablemos del problema de memoria del lenguaje C integrado!

1. Información general

La dificultad del problema de la memoria del lenguaje C radica en localizarlo, una vez localizado se puede solucionar fácilmente.
En esta nota hablemos de pisar la memoria. Pisa el recuerdo y entiéndelo literalmente. Originalmente, se operó este bloque de memoria, pero debido a un error de diseño, se operó la memoria adyacente y se manipularon los datos en la memoria adyacente.

Pisotear la memoria puede causar, en el mejor de los casos, anomalías funcionales o, en el peor, provocar fallos del programa.
Memoria, aproximadamente dividida en:

Área de almacenamiento estático
Área de almacenamiento dinámico

Sólo las variables almacenadas en la misma área de almacenamiento pueden interferir con la memoria de otras.

2 El área de almacenamiento estático se utiliza en la memoria.

Comparta un problema que haya encontrado antes en un proyecto real.

En Linux, la cantidad predeterminada de archivos que puede abrir un proceso es 1024 y el rango de fd es 0 ~ 1023.
El puerto serie se utiliza en el proyecto y el puerto serie fd es una variable global estática. Una vez, el fd cambió repentinamente a un valor fuera de rango, lo que obviamente fue pisado.
El código problemático es el siguiente:

float arr[5];
int count = 8;
for (size_t i = 0; i < count; i++)
{
    
    
    arr[i] = xxx;
}

Insertar descripción de la imagen aquí
La operación en la matriz arr, que también pertenece al área de almacenamiento estático, ocurrió una operación de matriz fuera de los límites, se pisaron las siguientes variables continuas y también se pisó fd.
En la práctica, es difícil localizar las variables adyacentes de fd simplemente mediante la impresión y depuración de registros, y lleva mucho tiempo.
En Linux, podemos comprobar este problema generando un archivo de mapa. El código para generar el archivo de mapa en CMakeLists.txt es el siguiente:

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

3 pasos del área de almacenamiento dinámico en la memoria

Ejemplo típico de memoria dinámica que sobrecarga la memoria: el uso inadecuado de malloc y strcpy provoca un desbordamiento del búfer.

#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;
}

Resultados de la ejecución:
Insertar descripción de la imagen aquí
obviamente, después de la operación strcpy, el valor de los datos a ha sido manipulado.
Motivo: se ignora que la operación strcpy copiará el terminador de cadena al búfer de destino.
Insertar descripción de la imagen aquí
Si no hay otros datos comerciales almacenados en el espacio adyacente, no habrá ningún problema si lo pisa. Si se almacenan datos importantes, puede aparecer un gran error en este momento, y puede aparecer ocasionalmente, lo que dificulta reproducir y localizar.

En este caso, podemos utilizar algunas herramientas para localizar el problema, como por ejemplo:

Uso simple de dmalloc
valgrind
valgrind puede leer notas anteriores: enlace

Por supuesto, también podemos probar algo en nuestro código. Para este tipo de problema, compartamos una idea de detección:

Cuando solicitamos memoria, agregamos dos áreas de identificación (áreas rojas) antes y después de la solicitud de memoria, y escribimos datos fijos en ellas. Al solicitar y liberar memoria, verifique si las dos áreas de identificación están dañadas (verifique si se pisa la zona roja de alto voltaje cuando se opera la memoria de montón).

Para ubicar el área de identificación posterior, se agrega un área de longitud para almacenar la longitud del espacio solicitado real.
Aquí definimos:

Antes_red_area: 4 bytes. Escribe datos fijos 0x11223344.
Después_red_area: 4 bytes. Escribe datos fijos 0x55667788.
Área de longitud (len_area): 4 bytes. Almacena la longitud del almacén de datos.

Insertar descripción de la imagen aquí

Función de memoria de aplicación personalizada

Además del área de almacenamiento de datos, solicite 12 bytes más. Naturalmente, la función de la aplicación de memoria personalizada debe ser compatible con el uso de malloc. prototipo de malloc:

void *malloc(size_t __size);

Personaliza la función para solicitar memoria:

vacío *Malloc(tamaño_t __tamaño);

El valor de retorno, naturalmente, devuelve la dirección del área de almacenamiento de datos. Implementación:

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

Personalice la función de liberación de memoria
para liberar toda la memoria asignada previamente. También se requieren pruebas antes del lanzamiento:

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

Usamos este método para detectar el ejemplo anterior de desbordamiento del búfer causado por el uso inadecuado de malloc y strcpy:
Insertar descripción de la imagen aquí
Como puede ver, este ejemplo pasa por la zona roja trasera y cambia los datos de la zona roja trasera a 0x55667700, lo que activa la finalización del programa de aserción. .

Código de prueba:

#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;
}

No hay violación de memoria:
Insertar descripción de la imagen aquí
este ejemplo simplemente comparte una idea de detección para detectar violaciones de datos en la memoria del montón. El código de ejemplo no es universal. Por ejemplo, si la memoria pisada no son solo unos pocos bytes adyacentes, sino un área adyacente grande, entonces se ha cruzado la zona roja en lugar de pisarse.

El tamaño de la zona roja lo establecemos nosotros mismos y podemos configurarlo más grande. Si se establece en un valor grande y se puede solucionar, el error debería ser más fácil de reproducir y localizar. Debería ser más fácil de localizar mirando el código, los que son más difíciles de localizar suelen ser aquellos que han pisado un trozo pequeño.

Informacion relevante:

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

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

Oficina de reimpresión: haga clic

Supongo que te gusta

Origin blog.csdn.net/zhi_Alanwu/article/details/131260391
Recomendado
Clasificación