Cómo entender la memoria de su programa

Al codificar en un lenguaje como C o C ++, puede interactuar con su memoria de una manera más baja. A veces, esto crea muchos problemas que no tenía antes: segfaults . Estos errores son bastante molestos y pueden causarle muchos problemas. A menudo son indicadores de que estás usando memoria que no debes usar.

Uno de los problemas más comunes es acceder a la memoria que ya se ha liberado. Esta es la memoria con la que ha liberado freeo la memoria que su programa ha liberado automáticamente, por ejemplo, de la pila.

Comprender todo esto es realmente simple y definitivamente hará que programes mejor y de una manera más inteligente.

¿Cómo se divide la memoria?

La memoria se divide en varios segmentos. Dos de los más importantes, para esta publicación, son la pila y el montón . La pila es un lugar de inserción ordenado, mientras que el montón es todo aleatorio: asigna memoria siempre que pueda.

La memoria de pila tiene un conjunto de formas y operaciones para su trabajo. Es donde se guarda parte de la información de los registros de su procesador. Y es donde va la información relevante sobre su programa: qué funciones se llaman, qué variables creó y algo más de información. Esta memoria también es administrada por el programa y no por el desarrollador.

El montón se usa a menudo para asignar grandes cantidades de memoria que se supone que existen mientras el desarrollador lo desee. Dicho esto, es el trabajo del desarrollador controlar el uso de la memoria en el montón . Al crear programas complejos, a menudo es necesario asignar grandes cantidades de memoria, y ahí es donde usa el montón. A esto lo llamamos memoria dinámica .

Estás colocando cosas en el montón cada vez que usas mallocpara asignar memoria para algo. Cualquier otra llamada que se parezca a int i;es memoria de pila. Saber esto es realmente importante para que pueda encontrar fácilmente errores en su programa y mejorar aún más su búsqueda de errores de Segfault.

Entendiendo la pila

Aunque es posible que no lo sepa, su programa asigna constantemente memoria de pila para que funcione. Todas las variables locales y todas las funciones que llames van allí. Con esto, puede hacer muchas cosas, la mayoría de ellas son cosas que no deseaba que sucedieran, como desbordamientos de búfer y acceso a memoria incorrecta.

Entonces, ¿cómo funciona realmente?

La pila es una estructura de datos LIFO (Last-In-First-Out). Puede verlo como una caja de libros perfectamente ajustados: el último libro que coloca es el primero que saca. Al usar esta estructura, el programa puede administrar fácilmente todas sus operaciones y alcances usando dos operaciones simples: empujar y hacer estallar .

Estos dos hacen exactamente lo contrario entre sí. Empujar inserta el valor en la parte superior de la pila. Pop toma el valor máximo de él.

Para realizar un seguimiento del lugar de la memoria actual, hay un registro de procesador especial llamado Stack Pointer . Cada vez que necesita guardar algo, como una variable o la dirección de retorno de una función, empuja y mueve el puntero de la pila hacia arriba. Cada vez que sale de una función, aparece todo, desde el puntero de la pila hasta la dirección de retorno guardada de la función. ¡Es simple!

Para probar si entendiste, usemos el siguiente ejemplo (intenta encontrar el error solo ☺️):

Si lo ejecuta, el programa simplemente segregará. ¿Por qué pasó esto? ¡Todo parece estar en su lugar! Excepto por ... la pila.

Cuando llamamos a la función createArray, la pila:

  • guarda la dirección de devolución,
  • crea arren la memoria de la pila y la devuelve (una matriz es simplemente un puntero a una ubicación de memoria con su información)
  • pero como no lo usamos mallocse guarda en la memoria de pila.

Después de devolver el puntero, dado que no tenemos ningún control sobre las operaciones de la pila, el programa extrae la información de la pila y la usa como necesita. Cuando intentamos completar la matriz después de regresar de la función, dañamos la memoria, lo que hace que el programa sea un error de segmentación.

Entendiendo el montón

A diferencia de la pila, el montón es lo que usa cuando desea que algo exista durante algún tiempo independientemente de las funciones y los ámbitos. Para utilizar esta memoria, el lenguaje C stdlib es muy bueno ya que aporta dos funciones impresionantes: mallocy free.

Malloc (asignación de memoria) solicita al sistema la cantidad de memoria solicitada y devuelve un puntero a la dirección inicial. Free le dice al sistema que la memoria que solicitamos ya no es necesaria y puede usarse para otras tareas. Parece realmente simple, siempre que evite errores.

El sistema no puede anular lo que pidieron los desarrolladores. Así que depende de nosotros, los humanos, gestionarlo con las dos funciones anteriores. Esto abre la puerta a un error humano: fugas de memoria.

Pérdida de memoria es una memoria solicitada por el usuario que nunca se liberó, cuando el programa finalizó o se perdieron los punteros a sus ubicaciones. Esto hace que el programa use mucha más memoria de la que se suponía. Para evitar esto, cada vez que ya no necesitamos un elemento asignado al montón, lo liberamos.

En la imagen de arriba, el mal camino nunca libera la memoria que usamos. Esto termina desperdiciando 20 * 4 bytes (tamaño int en 64 bits) = 80 bytes. Puede que esto no parezca mucho, pero imagina no hacerlo en un programa gigante. ¡Podemos acabar desperdiciando gigabytes!

Administrar la memoria del montón es esencial para que la memoria de sus programas sea eficiente. Pero también debe tener cuidado con cómo lo usa. Al igual que en la memoria de pila, después de que se libera la memoria, acceder a ella o usarla puede causarle una falla de segmento.

Bono: estructuras y el montón

Uno de los errores comunes al usar estructuras es simplemente liberar la estructura. Esto está bien, siempre que no asignemos memoria a los punteros dentro de la estructura. Si la memoria se asigna a punteros dentro de la estructura, primero debemos liberarlos. Entonces podemos liberar toda la estructura.

Cómo resuelvo mis problemas de pérdida de memoria

La mayoría de las veces, cuando programo en C, estoy usando estructuras. Por lo tanto, siempre tengo dos funciones obligatorias para usar con mis estructuras: el constructor y el destructor .

Estas dos funciones son las únicas en las que uso mallocs y libera en la estructura. Esto hace que sea realmente simple y fácil resolver mis pérdidas de memoria.

(Si desea saber más sobre cómo hacer que el código sea más fácil de leer, consulte mi publicación sobre abstracción).

Una gran herramienta de gestión de memoria: Valgrind

Es difícil administrar su memoria y asegurarse de que manejó todo correctamente. Una gran herramienta para validar si su programa se está comportando correctamente es Valgrind. Esta herramienta valida su programa, le dice cuánta memoria asignó, cuánto se liberó, si intentó escribir en un área de memoria incorrecta ... Usarla es una excelente manera de validar si todo está bien, y se debe usar para evitar compromisos de seguridad.

¡No olvides seguirme!

Además de publicar aquí en Medium, también estoy en Twitter.

Si tiene alguna pregunta o sugerencia, no dude en ponerse en contacto conmigo.