Análisis de un artículo: explicación del método de detección de pérdidas de memoria de Linux a través de ejemplos

1. mtrace analiza las pérdidas de memoria

mtrace (rastreo de memoria) es una herramienta de detección de problemas de memoria que viene con GNU Glibc y que puede usarse para ayudar a localizar problemas de pérdida de memoria. Su código fuente de implementación está en el directorio malloc del código fuente glibc. Su principio de diseño básico es diseñar una función void mtrace (). La función rastrea las llamadas de malloc/free y otras funciones en la biblioteca libc, detectando así si hay es una pérdida de memoria Condición. mtrace es una función C, declarada y definida en <mcheck.h>. El prototipo de la función es:

void mtrace(void);

principio de seguimiento

mtrace() La función instalará funciones de "enganche" para aquellas funciones relacionadas con la asignación de memoria dinámica (como malloc(), realloc(), memalign() y free()). Estas funciones de enlace registrarán toda la asignación de memoria relevante y la información de seguimiento publicada. y muntrace() descargará la función de enlace correspondiente.

Con base en la información de seguimiento de depuración generada por estas funciones de enlace, podemos analizar si existen problemas como "pérdidas de memoria".

Establecer ruta de generación de registros

El mecanismo mtrace requiere que ejecutemos el programa antes de que pueda generar registros de seguimiento, pero una cosa más que debemos hacer antes de ejecutar el programa es indicarle a mtrace (la función de enlace mencionada anteriormente) la ruta para generar el archivo de registro.

Hay dos formas de configurar la ruta de generación de registros, una es configurar la variable de entorno: export MALLOC_TRACE=./test.log // 当前目录下 la otra es configurarla a nivel de código: setenv("MALLOC_TRACE", "output_file_name", 1);``output_file_namees el nombre del archivo que almacena los resultados de la detección.

Ejemplo de prueba

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

int main(int argc, char **argv)
{
    mtrace();  // 开始跟踪

    char *p = (char *)malloc(100);
    free(p);
    p = NULL;
    p = (char *)malloc(100);

    muntrace();   // 结束跟踪,并生成日志信息
    return 0;
}

A partir del código anterior, esperamos poder verificar si hay una pérdida de memoria desde el principio hasta el final del programa. El ejemplo es simple. Puede ver de un vistazo que hay una pérdida de memoria, por lo que debemos verifique si mtrace puede verificar si hay pérdidas de memoria y verifique Cómo analizar y posicionar los resultados. gcc -g test.c -o testGenerar archivo ejecutable.

registro

Una vez que el programa termine de ejecutarse, el archivo test.log se generará en el directorio actual. Cuando lo abra, podrá ver el siguiente contenido:

= Start
@ ./test:[0x400624] + 0x21ed450 0x64
@ ./test:[0x400634] - 0x21ed450
@ ./test:[0x400646] + 0x21ed450 0x64
= End

En este archivo, podemos ver que las tres líneas en el medio corresponden a las operaciones malloc -> free -> malloc en el código fuente. Interpretación : ./test se refiere al nombre del programa que ejecutamos, [0x400624] es la información de dirección en el código de máquina de la primera llamada a la función malloc, + significa solicitar memoria (- significa liberar), 0x21ed450 es la dirección información solicitada por la función malloc, 0x64 representa el tamaño de memoria solicitado. Según este análisis, se lanzó la primera aplicación, pero la segunda aplicación no se lanzó y hay un problema de pérdida de memoria.

Análisis de fugas

Utilice la herramienta addr2line para localizar la ubicación del código fuente

Al utilizar la herramienta de comando "addr2line", puede obtener el número de línea del archivo fuente (puede usarlo para ubicar la ubicación del código fuente específico según la dirección del código de la máquina)

# addr2line -e test 0x400624
/home/test.c:9

Utilice la herramienta mtrace para analizar la información de registro

mtrace test ./test.logSe ejecutan mtrace + ruta del archivo ejecutable + ruta del archivo de registro  y se genera la siguiente información:

Memory not freed:
-----------------
           Address     Size     Caller
0x00000000021ed450     0x64  at /home/test.c:14

2. Valgrind analiza las pérdidas de memoria

Introducción a la herramienta Valgrind

Valgrind es una colección de herramientas de simulación y depuración de código abierto (GPL V2) en Linux. Valgrind consta de un núcleo y otras herramientas de depuración basadas en el núcleo. El kernel es similar a un marco, que simula un entorno de CPU y proporciona servicios a otras herramientas; otras herramientas son similares a los complementos y utilizan los servicios proporcionados por el kernel para completar varias tareas específicas de depuración de memoria. La arquitectura de Valgrind se muestra en la siguiente figura.

1、Memcheck

La herramienta más utilizada se utiliza para detectar problemas de memoria en los programas. Se detectarán todas las lecturas y escrituras en la memoria y se capturarán todas las llamadas a malloc() / free() / new / delete.

Por lo tanto, puede detectar los siguientes problemas: uso de memoria no inicializada; lectura/escritura de bloques de memoria liberados; lectura/escritura de bloques de memoria más allá de la asignación de malloc; lectura/escritura de bloques de memoria inapropiados en la pila; pérdidas de memoria, apuntando a un bloque Los punteros de memoria son perdido para siempre; coincidencia incorrecta de malloc/free o new/delete; los punteros dst y src en las funciones relacionadas con memcpy() se superponen.

2、Callgrind

Una herramienta de análisis similar a gprof, pero más detallada en observar el funcionamiento del programa y puede aportarnos más información. A diferencia de gprof, no requiere opciones especiales al compilar el código fuente, pero se recomienda agregar opciones de depuración.

Callgrind recopila algunos datos cuando el programa se está ejecutando, crea un gráfico de llamadas a funciones y, opcionalmente, puede realizar una simulación de caché. Al final de la ejecución, escribe los datos del análisis en un archivo. callgrind_annotate puede convertir el contenido de este archivo a un formato legible.

3、Cachégrind

El analizador de caché, que simula el caché de primer nivel I1, Dl y el caché de segundo nivel en la CPU, puede señalar con precisión errores y aciertos de caché en el programa. Si es necesario, también puede proporcionarnos la cantidad de errores de caché, la cantidad de referencias de memoria y la cantidad de instrucciones generadas por cada línea de código, cada función, cada módulo y el programa completo. Esta es una gran ayuda para optimizar programas.

4. Helgrind

Se utiliza principalmente para comprobar problemas de competencia que ocurren en programas multiproceso. Helgrind busca áreas de memoria a las que acceden varios subprocesos y que no están bloqueadas de manera consistente. Estas áreas suelen ser donde se pierde la sincronización entre subprocesos y pueden provocar errores difíciles de encontrar.

Helgrind implementó un algoritmo de detección de carreras llamado "Eraser" e realizó más mejoras para reducir la cantidad de errores reportados. Sin embargo, Helgrind todavía se encuentra en la etapa experimental.

5 、 Macizo

El analizador de pila, que mide cuánta memoria usa un programa en la pila, nos dice el tamaño de los bloques del montón, los bloques de administración del montón y la pila.

Massif puede ayudarnos a reducir el uso de memoria, en sistemas modernos con memoria virtual también puede acelerar la ejecución de nuestros programas y reducir la posibilidad de que el programa permanezca en el área de intercambio.

Además, se proporcionará lacayo y nulgrind. Lackey es una pequeña herramienta que rara vez se utiliza; Nulgrind simplemente muestra a los desarrolladores cómo crear una herramienta.

 Information Direct: ruta de aprendizaje de tecnología del código fuente del kernel de Linux + video tutorial sobre el código fuente del kernel

Learning Express: Código fuente del kernel de Linux Ajuste de memoria Sistema de archivos Gestión de procesos Controlador de dispositivo/Pila de protocolo de red

Principio de verificación de memoria

El objetivo de este artículo es detectar pérdidas de memoria, por lo que no explicaré demasiado sobre otras herramientas de valgrind, principalmente explico el trabajo de Memcheck. El principio de Memcheck para detectar problemas de memoria se muestra en la siguiente figura:

La clave de la capacidad de Memcheck para detectar problemas de memoria es que crea dos tablas globales.

  • La tabla Valor-Válido tiene 8 bits correspondientes a cada byte en todo el espacio de direcciones del proceso; también hay un vector de bits correspondiente para cada registro de la CPU. Estos bits son responsables de registrar si el valor del byte o del registro tiene un valor inicializado válido.
  • La tabla Dirección válida tiene un bit correspondiente para cada byte en todo el espacio de direcciones del proceso, que es responsable de registrar si la dirección se puede leer o escribir.
  • Principio de detección: cuando desee leer o escribir un byte en la memoria, primero verifique el bit A en la tabla de dirección válida correspondiente a este byte. Si el bit A muestra que la ubicación no es válida, memcheck informa un error de lectura y escritura. El núcleo es similar a un entorno de CPU virtual, por lo que cuando un determinado byte de la memoria se carga en la CPU real, el bit V en la tabla de valores válidos correspondiente al byte también se carga en el entorno de CPU virtual. Una vez que el valor en el registro se usa para generar una dirección de memoria, o el valor puede afectar la salida del programa, memcheck verificará los bits V correspondientes. Si el valor no se ha inicializado, se informará un error de memoria no inicializada.

Tipo de pérdida de memoria

Valgrind divide las pérdidas de memoria en 4 categorías:

  • Definitivamente perdida: la memoria no se ha liberado, pero no hay ningún puntero que apunte a la memoria y no se puede acceder a ella. Está establecido que es muy necesario parchear la memoria de ejecución perdida.
  • Filtrada indirectamente (perdida indirectamente): el puntero de memoria filtrada se almacena en la memoria que se ha filtrado. Como no se puede acceder a la memoria que se ha filtrado, no se puede acceder a la memoria que provocó la fuga indirecta. Por ejemplo:
struct list {
 struct list *next;
};

int main(int argc, char **argv)
{
 struct list *root;
 root = (struct list *)malloc(sizeof(struct list));
 root->next = (struct list *)malloc(sizeof(struct list));
 printf("root %p roop->next %p\n", root, root->next);
 root = NULL;
 return 0;
}

Lo que falta aquí es el puntero raíz (que es el tipo de fuga establecido), lo que hace que el siguiente puntero almacenado en la raíz se convierta en una fuga indirecta. Definitivamente será necesario parchear la memoria filtrada indirectamente, pero generalmente se parcheará junto con el parche de la fuga establecida.

  • Posiblemente perdido: la aguja no apunta a la dirección del encabezado de la memoria, sino a la ubicación dentro de la memoria. Valgrind a menudo sospecha que puede haber una fuga porque las manos ya están sesgadas y no hacia la cabeza de la memoria, sino hacia las partes internas de la memoria. En algunos casos, esto no es una fuga, porque este programa está diseñado de esa manera, por ejemplo, para lograr la alineación de la memoria, se devuelve memoria de procesamiento de aplicaciones adicional a la dirección de memoria alineada.
  • Aún accesible: el puntero siempre está presente e inclinado hacia la parte superior de la memoria, y la memoria no se libera hasta que se cierra el programa.

Configuración de parámetros de Valgrind

  • --leak-check=<no|summary|yes|full> Si se establece en sí o completo, después de que finalice el programa llamado, valgrind describirá cada pérdida de memoria en detalle. El valor predeterminado es resumen, que solo informa varias pérdidas de memoria.
  • --log-fd= [predeterminado: 2, stderr] valgrind imprime registros y los vuelca en el archivo o descriptor de archivo especificado. Sin este parámetro, los registros de valgrind se generarán junto con los registros del programa de usuario, lo que aparecerá muy desordenado.
  • --trace-children=<yes | no> [predeterminado: no] Ya sea para rastrear procesos secundarios. Si es un programa multiproceso, se recomienda utilizar esta función. Sin embargo, no tendrá mucho impacto si se habilita un solo proceso.
  • --keep-debuginfo=<yes | no> [predeterminado: no] Si el programa utiliza una biblioteca cargada dinámicamente (dlopen), la información de depuración se borrará cuando se descargue la biblioteca dinámica (dlclose). Después de habilitar esta opción, la información de la pila de llamadas se conservará incluso si se descarga la biblioteca dinámica.
  • --keep-stacktraces=<alloc | free | alloc-and-free | alloc-then-free | none> [predeterminado: alloc-and-free] Las pérdidas de memoria no son más que una falta de coincidencia entre la aplicación y la versión, y la llamada a la función La pila es solo registrar cuando se aplica o registrar cuando se solicita el lanzamiento. Si solo nos centramos en las pérdidas de memoria, en realidad no hay necesidad de registrar ambos cuando se solicita el lanzamiento, porque esto ocupará mucha memoria adicional y más consumo de CPU, lo que hará que la ya lenta ejecución del programa añade sal a la herida.
  • --freelist-vol= Cuando un programa cliente usa free o delete para liberar un bloque de memoria, el bloque de memoria no estará disponible inmediatamente para su reasignación. Solo se colocará en una cola de bloques libres (lista libre) y se marcará como no disponible. Acceso , lo cual es útil para detectar errores cuando el programa cliente accede al bloque liberado después de un período de tiempo muy importante. Esta opción especifica el tamaño del bloque de bytes ocupado por la cola. El valor predeterminado es 20 MB. Aumentar esta opción aumentará la sobrecarga de memoria de memcheck, pero también se mejorará la capacidad de detectar dichos errores.
  • --freelist-big-blocks= Al tomar bloques de memoria disponibles de la cola de la lista libre para reasignarlos, memcheck extraerá un bloque según la prioridad de aquellos bloques de memoria mayores que el número. Esta opción evita llamadas frecuentes a pequeños bloques de memoria en la lista libre y aumenta la probabilidad de detectar errores de puntero salvajes para bloques de memoria pequeños. Si esta opción se establece en 0, todos los bloques se reasignarán según el principio de primero en entrar, primero en salir. El valor predeterminado es 1M. Referencia: Introducción a valgrind (herramienta de verificación de memoria)

Parámetros de compilación recomendados

Para imprimir la información detallada de desapilado cuando ocurre un problema, es mejor agregar la opción -g al compilar el programa. Si hay una biblioteca cargada dinámicamente, debe agregarla.De  --keep-debuginfo=yes lo contrario, si se descubre que la biblioteca cargada dinámicamente se ha filtrado, no se puede encontrar la tabla de símbolos porque la biblioteca dinámica se ha desinstalado. Optimización del compilador de código, no se recomienda utilizar -O2 y superiores. Es probable que -O0 ralentice la operación, por lo que se recomienda utilizar -O1.

Descripción del ejemplo de detección

Solicitar memoria sin liberarla

#include <stdlib.h>
#include <stdio.h>
void func()
{
  //只申请内存而不释放
    void *p=malloc(sizeof(int));
}
int main()
{
    func();
    return 0;
}

Utilice el comando valgrind para ejecutar el programa y enviar el registro a un archivo

valgrind --log-file=valReport --leak-check=full --show-reachable=yes --leak-resolution=low ./a.out

Descripción de parámetros:

  • –log-file=valReport especifica generar un archivo de registro de análisis en el directorio de ejecución actual y el nombre del archivo es valReport
  • –leak-check=full muestra detalles de cada fuga
  • –show-reachable=yes Si se deben detectar fugas fuera del rango de control, como punteros globales, punteros estáticos, etc., y mostrar todos los tipos de pérdidas de memoria
  • –leak-solution=nivel de fusión del informe de pérdida de memoria bajo
  • –track-origins=yes significa activar la función de detección de “uso de memoria no inicializada” y abrir resultados detallados. Si no existe tal frase, la detección en esta área se realizará de forma predeterminada, pero no se imprimirán los resultados detallados. Después de ejecutar la salida, se interpreta el informe. 54017 se refiere al número de proceso. Si el programa se ejecuta utilizando múltiples procesos, se mostrará el contenido de múltiples procesos.
==54017== Memcheck, a memory error detector
==54017== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==54017== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==54017== Command: ./a.out
==54017== Parent PID: 52130

El segundo párrafo es un resumen de la asignación de memoria del montón y menciona que el programa solicitó memoria una vez, de los cuales se liberaron 0 y se asignaron 4 bytes ( 1 allocs, 0 frees, 4 bytes allocated).

En el resumen principal, está la cantidad total de memoria dinámica utilizada por el programa, la cantidad de tiempos de asignación de memoria y el número de tiempos de liberación de memoria. Si el número de tiempos de asignación de memoria y los tiempos de liberación de memoria son inconsistentes, significa que hay una pérdida de memoria.

==54017== HEAP SUMMARY:
==54017==   in use at exit: 4 bytes in 1 blocks
==54017==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated

El tercer párrafo describe la información específica de la pérdida de memoria. Hay una pieza de memoria que ocupa 4 bytes ( 4 bytes in 1 blocks). Se asigna llamando a malloc. Puede ver en la pila de llamadas que la función func finalmente llamó a malloc, por lo que esta información es relativamente preciso Esto localiza dónde se solicita nuestra memoria filtrada.

==54017== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==54017==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==54017==    by 0x40057E: func() (in /home/oceanstar/CLionProjects/Share/src/a.out)
==54017==    by 0x40058D: main (in /home/oceanstar/CLionProjects/Share/src/a.out)

El último párrafo es un resumen, una pérdida de memoria de 4 bytes.

==54017== LEAK SUMMARY:
==54017==    definitely lost: 4 bytes in 1 blocks  // 确立泄露
==54017==    indirectly lost: 0 bytes in 0 blocks  // 间接性泄露
==54017==    possibly lost: 0 bytes in 0 blocks   // 很有可能泄露
==54017==    still reachable: 0 bytes in 0 blocks // 仍可访达
==54017==    suppressed: 0 bytes in 0 blocks

Leer y escribir más allá de los límites

#include <stdio.h>
#include <iostream>
int main()
{
    int len = 5;
    int *pt = (int*)malloc(len*sizeof(int)); //problem1: not freed
    int *p = pt;
    for (int i = 0; i < len; i++){
        p++;
    }
    *p = 5; //problem2: heap block overrun
    printf("%d\n", *p); //problem3: heap block overrun
    // free(pt);
    return 0;
}

problema1: El puntero pt solicitó espacio, pero no fue liberado; problema2: pt solicitó el espacio de 5 entradas, y cuando p alcanzó la posición de p[5] después de 5 ciclos, el acceso estaba fuera de los límites (la escritura fue fuera de límites)  *p = 5. (Escritura no válida de tamaño 4 en el informe valgrind a continuación)

==58261== Invalid write of size 4
==58261==    at 0x400707: main (main.cpp:12)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

Problema 1: lectura fuera de límites (lectura no válida de tamaño 4 en el informe valgrind a continuación)

==58261== Invalid read of size 4
==58261==    at 0x400711: main (main.cpp:13)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

Liberación repetida

#include <stdio.h>
#include <iostream>
int main()
{
    int *x;
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    free(x);
    free(x);
    return 0;
}

El informe es el siguiente,Invalid free() / delete / delete[] / realloc()

==59602== Invalid free() / delete / delete[] / realloc()
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006FE: main (main.cpp:10)
==59602==  Address 0x5a230a0 is 0 bytes inside a block of size 32 free'd
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006F2: main (main.cpp:9)
==59602==  Block was alloc'd at
==59602==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==59602==    by 0x4006E2: main (main.cpp:8)

La interfaz de lanzamiento de la aplicación no coincide

El informe de discrepancia entre la interfaz de la aplicación y la versión es el siguiente: el puntero para solicitar espacio usando malloc se libera usando free; el espacio solicitado usando new se libera usando eliminar() Mismatched free() / delete / delete []:

==61950== Mismatched free() / delete / delete []
==61950==    at 0x4C2BB8F: operator delete[](void*) (vg_replace_malloc.c:651)
==61950==    by 0x4006E8: main (main.cpp:8)
==61950==  Address 0x5a23040 is 0 bytes inside a block of size 5 alloc'd
==61950==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==61950==    by 0x4006D1: main (main.cpp:7)

sobrescritura de memoria

int main()
{
    char str[11];
    for (int i = 0; i < 11; i++){
        str[i] = i;
    }
    memcpy(str + 1, str, 5);
    char x[5] = "abcd";
    strncpy(x + 2, x, 3);
}

El problema radica en memcpy. Copiar 5 caracteres comenzando desde la posición del puntero str al espacio señalado por str+1 provocará la sobrescritura de la memoria. Lo mismo ocurre con strncpy. El informe es el siguiente Source and destination overlap:

==61609== Source and destination overlap in memcpy(0x1ffefffe31, 0x1ffefffe30, 5)
==61609==    at 0x4C2E81D: memcpy@@GLIBC_2.14 (vg_replace_strmem.c:1035)
==61609==    by 0x400721: main (main.cpp:11)
==61609== 
==61609== Source and destination overlap in strncpy(0x1ffefffe25, 0x1ffefffe23, 3)
==61609==    at 0x4C2D453: strncpy (vg_replace_strmem.c:552)
==61609==    by 0x400748: main (main.cpp:14)

3. Resumen

Hay dos métodos de detección de memoria:

1. Mantener una lista vinculada de operaciones de memoria. Cuando hay una operación de aplicación de memoria, se agrega a esta lista vinculada. Cuando hay una operación de liberación, se elimina de la lista vinculada de la operación de aplicación. Si todavía hay contenido en la lista vinculada después de que finaliza el programa, significa que hay una pérdida de memoria; si la operación de memoria que se va a liberar no encuentra la operación correspondiente en la lista vinculada, significa que se ha liberado varias veces. . Utilice este método con herramientas de depuración integradas, Visual Leak Detecter, mtrace, memwatch, debug_new. 2. Simule el espacio de direcciones del proceso. Después del manejo de las operaciones de memoria de proceso por parte del sistema operativo, se mantiene una asignación de espacio de direcciones en modo de usuario. Este método requiere una comprensión profunda del procesamiento de espacios de direcciones de proceso. Debido a que la distribución del espacio de direcciones del proceso de Windows no es de código abierto, es difícil de simular, por lo que solo es compatible con Linux. El que adopta este enfoque es valgrind.

Autor original: Aprenda integrado juntos

Supongo que te gusta

Origin blog.csdn.net/youzhangjing_/article/details/132817245
Recomendado
Clasificación