Los lenguajes como c y c++ en los que la memoria es manejada por el desarrollador, los errores en la gestión de memoria son frecuentes.

Aunque c++ ha dado un paso hacia delante en la gestión de los recursos con el concepto de RAII, con sus constructores y destructores, incluso con conceptos más modernos como los smart pointers (hay una entrada en este blog, un poquito de proselitismo).

Aun así la gestión de la memoria sigue siendo tarea humana y potencialmente puede haber errores. Los casos comunes de errores en memoria pueden ser:

  • Fugas de memoria
  • Buffer overflow
  • Uso de variables después de liberarla
  • Uso de variables después de un return
  • Doble free

Y un largo etc.

Existen herramientas que permiten detectar este tipo de errores y son bastante interesante conocerlas para mejorar la calidad de nuestro código.

Valgrind

valgrind es una herramienta de análisis dinámico para detectar errores memoria y problemas de rendimiento.

La herramienta más usada es memcheck que tiene código de instrumentación para depurar el programa y detectar errores en la memoria. Es una herramienta bastante útil y simple de usar aunque el precio a pagar es un tiempo de ejecución notablemente alto.

Para proyectos pequeños y medios apenas se nota, pero para proyectos grandes con uso intensivo de malloc y free es notable.

Para usar valgrind tan solo hay que compilar el programa y lanzar el ejecutable con valgrind

valgrind --leak-check=full <programa>

Al finalizar la ejecución valgrind mostrará la salida con errores si los hubiese.

Detección fugas de memoria con valgrind

Creamos una simple fuga de memoria: realizamos un new sin su correspondiente delete

#include <iostream>
using namespace std;

int main(void)
{
    int* n = new int;
    *n = 10;
    printf("n=%i\n", *n);
    return 0;
}
g++ -g -std=c++20 main.cc -o main.out
valgrind --leak-check=full ./main.out

Al lanzar valgrind nos devuelve el reporte indicando la línea donde se reservó memoria y con la cantidad de bytes perdidos. Bastante útil para depurar.

==39838== Memcheck, a memory error detector
==39838== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==39838== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==39838== Command: ./main.out
==39838==
n=10
==39838==
==39838== HEAP SUMMARY:
==39838==     in use at exit: 4 bytes in 1 blocks
==39838==   total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated
==39838==
==39838== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==39838==    at 0x4846013: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==39838==    by 0x10917A: main (main.cc:17)
==39838==
==39838== LEAK SUMMARY:
==39838==    definitely lost: 4 bytes in 1 blocks
==39838==    indirectly lost: 0 bytes in 0 blocks
==39838==      possibly lost: 0 bytes in 0 blocks
==39838==    still reachable: 0 bytes in 0 blocks
==39838==         suppressed: 0 bytes in 0 blocks
==39838==
==39838== For lists of detected and suppressed errors, rerun with: -s
==39838== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Sanitizers

La familia de sanitizers es un proyecto de google que contiene diferentes sanitizer que detectan diferentes errores en el software.

Estas herramientas están incluidas en el proyecto LLVM y de su compilador clang tanto como en gcc

La familia de sanitizers incluyen:

  • Address Sanitizer
  • Leak sanitizer
  • Undefined behaviour
  • Memory Sanitizer

Incluso versiones para el kernel como KASAN

Aunque estas herramientas tienen un alcance diferente y más amplio que valgrind, se solapan en algunas cuestiones. El tiempo de ejecución de estas herramientas es notablemente menor.

Hay una entrada en el blog de redhat en donde hay una comparación de tiempos entre los sanitizers y valgrind y la diferencia es bastante grande.

Para usar los sanitizer basta con activar el correspondiente flag del compilador que usemos, compilar todo el código fuente y ejecutar. Al final de la ejecución tendremos un reporte detallado con los posibles errores detectados. Un detalle importante es que en el momento de escribir esta entrada, los flags que hay que habilitar en el compilador son los mismos tanto para gcc como clang.

Recomiendo buscar más información de todos los errores que cubre los sanitizers para estudiar que parámetro queremos perfilar en nuestro software. En la documentacion de clang se puede encontrar más detalles sobre los sanitizers

En esta entrada voy a señalar algunos casos simples pero que son igualmente ilustrativos de como usar estas herramientas.

Address Sanitizer (ASan)

Address Sanitizer es un detector de errores de memoria, tales como acceso a zonas de memoria fuera de su rango, uso de variables después de un free, doble free, etc.

buffer overflow con variables en el stack

Un buffer overflow es un accesso a una zona de memoria que está fuera del límite del buffer reservado.

Lo interesante de ASan es que es capaz de detectar un buffer overflow para variable declaradas en el stack. Esto es una mejora importante con respecto a valgrind porque valgrind no detecta este tipo de errores para variables que son reservadas de forma estáticas on el stack. Ver el siguiente enlace en la sección Caveats:

Memcheck cannot detect every memory error your program has. For example, it can’t detect out-of-range reads or writes to arrays that are allocated statically or on the stack. But it should detect many errors that could crash your program (eg. cause a segmentation fault).

Veamos un caso práctico en el que se accede a una zona de memoria fuera de los límites de su tamaño.

#include <iostream>
#include <cstring>
using namespace std;

int main(void)
{
    char message[] = {"Hello from hardfloat.com"};
    message[sizeof(message) + 20] = '$';
    return 0;
}
g++ -g -Wall -std=c++20 -fsanitize=address main.cc -o main-sanitizer.o

Que da como resultado:

==3766==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffe6de04fcd at pc 0x55672a95c39d bp 0x7ffe6de04f70 sp 0x7ffe6de04f60
WRITE of size 1 at 0x7ffe6de04fcd thread T0
    #0 0x55672a95c39c in main /home/andres/Documents/projects/cpp/address-sanitizer/main.cc:29
    #1 0x7fdf2294d30f in __libc_start_call_main (/usr/lib/libc.so.6+0x2d30f)
    #2 0x7fdf2294d3c0 in __libc_start_main@GLIBC_2.2.5 (/usr/lib/libc.so.6+0x2d3c0)
    #3 0x55672a95c124 in _start (/home/andres/Documents/projects/cpp/address-sanitizer/main-sanitizer.out+0x1124)

Address 0x7ffe6de04fcd is located in stack of thread T0 at offset 77 in frame
    #0 0x55672a95c208 in main /home/andres/Documents/projects/cpp/address-sanitizer/main.cc:6

  This frame has 2 object(s):
    [32, 57) 'message' (line 24)
    [96, 1120) 'copy' (line 26) <== Memory access at offset 77 underflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-overflow /home/andres/Documents/projects/cpp/address-sanitizer/main.cc:29 in main
Shadow bytes around the buggy address:
  0x10004dbb89a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb89b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb89c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb89d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb89e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x10004dbb89f0: f1 f1 f1 f1 00 00 00 01 f2[f2]f2 f2 00 00 00 00
  0x10004dbb8a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb8a10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb8a20: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb8a30: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x10004dbb8a40: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
  Shadow gap:              cc
==3766==ABORTING

Si lanzamos este mismo caso (compilando de nuevo sin la opción fsanitize) en valgrind veremos como no detecta ningún tipo de error.

valgrind --leak-check=full ./main.out
==5276== Memcheck, a memory error detector
==5276== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5276== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==5276== Command: ./main.out
==5276==
==5276==
==5276== HEAP SUMMARY:
==5276==     in use at exit: 0 bytes in 0 blocks
==5276==   total heap usage: 1 allocs, 1 frees, 72,704 bytes allocated
==5276==
==5276== All heap blocks were freed -- no leaks are possible
==5276==
==5276== For lists of detected and suppressed errors, rerun with: -s
==5276== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

buffer overflow con variables en el heap

Este es un claro caso de uso para valgrind, puesto que la variable se ha declarado con new/malloc etc y valgrind si puede detectarlo.

#include <iostream>
#include <cstring>
using namespace std;

int main(void)
{
    char message[] = {"Hello from hardfloat.com"};
    char *message_heap = new char[sizeof(message)];
    message_heap[sizeof(message) + 20] = '$';
    delete[] message_heap;
    return 0;
}

Ahora si vemos como valgrind lo detecta.

==5803== Memcheck, a memory error detector
==5803== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5803== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==5803== Command: ./main.out
==5803==
==5803== Invalid write of size 1
==5803==    at 0x1091D4: main (main.cc:29)
==5803==  Address 0x4dcacad is 20 bytes after a block of size 25 alloc'd
==5803==    at 0x48472F3: operator new[](unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5803==    by 0x1091C7: main (main.cc:25)
==5803==
==5803==
==5803== HEAP SUMMARY:
==5803==     in use at exit: 0 bytes in 0 blocks
==5803==   total heap usage: 2 allocs, 2 frees, 72,729 bytes allocated
==5803==
==5803== All heap blocks were freed -- no leaks are possible
==5803==
==5803== For lists of detected and suppressed errors, rerun with: -s
==5803== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

Uso después de free

Imaginemos el caso en el que se se utilice una variable después de liberarla.

#include <iostream>
using namespace std;

int main(void)
{
    int* num = new int;
    *num = 1;
    delete num;
    return *num;
}

Compilando con los siguientes flags del compilador:

g++ -fsanitize=address -g -std=c++20 main.cc -o main.out
clang++ -fsanitize=address -g -std=c++20 main.cc -o main.out

Para obtener el resultado es necesario lanzar el ejecutable. En este caso resulta en:

==22678==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x56058484b23c bp 0x7fffdd826d00 sp 0x7fffdd826cf0
...
SUMMARY: AddressSanitizer: heap-use-after-free /home/andres/Documents/projects/cpp/address-sanitizer/main.cc:8 in main

Undefined behaviour (UBSan)

Es una herramienta para detectar comportamientos no definidos en el lenguaje.

Desbordamiento de enteros con signo (signed integer overflow)

Un desbordamiento de enteros ocurre cuando un entero pasa de su valor máximo a cero. Esto puede suponer un error lógico en ciertos escenarios y es conveniente tener herramientsas que lo detecten.

#include <iostream>
#include <limits.h>
using namespace std;

int main(void)
{
    int num = INT_MAX;
    num++;
    printf("num=%i\n", num);
    return 0;
}
g++ -g -Wall -std=c++20 -fsanitize=undefined main.cc -o main.out
clang++ -g -Wall -std=c++20 -fsanitize=undefined main.cc -o main.out
main.cc:8:8: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
num=-2147483648

Desbordamiento de bits (shift overflow)

Otro check sería el desbordamiendo a nivel de bits. Es decir cuando se realiza un movimiento de bits y el movimiento desborda el tamaño del bit, como en el ejemplo siguiente

#include <iostream>
#include <limits.h>
using namespace std;

uint8_t mask = 0;

int main(void)
{
    int mask = 0;
    for ( int i = 0; i <= 32; i++) {
        mask = (1 << i);
        printf("mask=0x%0X\n", mask);
    }
    return 0;
}
g++ -g -Wall -std=c++20 -fsanitize=undefined main.cc -o main.out
clang++ -g -Wall -std=c++20 -fsanitize=undefined main.cc -o main.out
...
main.cc:15:19: runtime error: shift exponent 32 is too large for 32-bit type 'int'
...

Leak sanitizer (LSan)

Es un detector de fugas de memoria en tiempo de ejecución. Este detector se solapa más con valgrind

En este caso se hace un new y nunca se devuelve la memoria con un delete. Por lo tanto se produce una fuga de memoria.

#include <iostream>
using namespace std;

int main(void)
{
    int* n = new int;
    *n = 10;
    printf("n=%i\n", *n);
    return 0;
}
g++ -g -Wall -std=c++20 -fsanitize=leak main.cc -o main.out
n=10

=================================================================
==40639==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 4 byte(s) in 1 object(s) allocated from:
    #0 0x7fe6e34b1811 in operator new(unsigned long) /usr/src/debug/gcc/libsanitizer/asan/asan_new_delete.cpp:99
    #1 0x55b53027c1ea in main /home/andres/Documents/projects/cpp/address-sanitizer/main.cc:17
    #2 0x7fe6e2ef230f in __libc_start_call_main (/usr/lib/libc.so.6+0x2d30f)

SUMMARY: AddressSanitizer: 4 byte(s) leaked in 1 allocation(s).

Como podemos ver por el resultado del análisis LSan se puede interpretar que la línea 17 de main.cc se ha reservado memoria que no ha sido liberada al finalizar el programa.

Esta información es bastante útil para depurar un leak de memoria. Normalmente los leaks son complejos de depurar sin este tipo de herramientas.

https://github.com/google/sanitizers/wiki/AddressSanitizer

https://developers.redhat.com/blog/2021/05/05/memory-error-checking-in-c-and-c-comparing-sanitizers-and-valgrind

https://github.com/google/sanitizers

Información de los distintos sanitizers