En las computadoras, para que un proceso sea ejecutable, debe colocarse en la memoria. Para ello se debe asignar un campo a un proceso en memoria. La asignación de memoria es un tema importante a tener en cuenta, especialmente en las arquitecturas del kernel y del sistema.
Echemos un vistazo a la asignación de memoria de Linux en detalle y comprendamos lo que sucede detrás de escena.
¿Cómo se realiza la asignación de memoria?
La mayoría de los ingenieros de software no conocen los detalles de este proceso. Pero si eres un candidato a programador de sistemas, deberías saber más al respecto. Al observar el proceso de asignación, es necesario entrar en un pequeño detalle sobre Linux y la biblioteca glibc .
Cuando las aplicaciones necesitan memoria, tienen que solicitarla al sistema operativo. Esta solicitud del kernel naturalmente requerirá una llamada al sistema. No puede asignar memoria usted mismo en el modo de usuario.
La familia de funciones malloc() es responsable de la asignación de memoria en el lenguaje C. La pregunta que hay que hacerse aquí es si malloc(), como función de glibc, realiza una llamada directa al sistema.
No hay una llamada al sistema llamada malloc en el kernel de Linux. Sin embargo, hay dos llamadas al sistema para las demandas de memoria de las aplicaciones, que son brk y mmap .
Dado que solicitará memoria en su aplicación a través de las funciones de glibc, es posible que se pregunte cuál de estas llamadas al sistema está utilizando glibc en este momento. La respuesta es ambos.
La primera llamada al sistema: brk
Cada proceso tiene un campo de datos contiguo. Con la llamada al sistema brk, se aumenta el valor de interrupción del programa, que determina el límite del campo de datos, y se realiza el proceso de asignación.
Aunque la asignación de memoria con este método es muy rápida, no siempre es posible devolver el espacio no utilizado al sistema.
Por ejemplo, considere que asigna cinco campos, cada uno de 16 KB de tamaño, con la llamada al sistema brk a través de la función malloc(). Cuando haya terminado con el número dos de estos campos, no es posible devolver el recurso relevante (desasignación) para que el sistema pueda usarlo. Porque si reduce el valor de la dirección para mostrar el lugar donde comienza su campo número dos, con una llamada a brk, habrá realizado la desasignación de los campos números tres, cuatro y cinco.
Para evitar la pérdida de memoria en este escenario, la implementación de malloc en glibc monitorea los lugares asignados en el campo de datos de proceso y luego especifica devolverlo al sistema con la función free(), para que el sistema pueda usar el espacio libre para más memoria. asignaciones
En otras palabras, después de asignar cinco áreas de 16 KB, si la segunda área se devuelve con la función free() y se vuelve a solicitar otra área de 16 KB después de un tiempo, en lugar de ampliar el área de datos a través de la llamada al sistema brk, la dirección anterior es devuelto
Sin embargo, si el área recién solicitada tiene más de 16 KB, el área de datos se ampliará mediante la asignación de una nueva área con la llamada al sistema brk, ya que el área dos no se puede utilizar. Aunque el área número dos no está en uso, la aplicación no puede usarla debido a la diferencia de tamaño. Debido a escenarios como este, existe una situación llamada fragmentación interna y, de hecho, rara vez puede usar todas las partes de la memoria al máximo.
Para una mejor comprensión, intente compilar y ejecutar la siguiente aplicación de ejemplo:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
char *ptr[7];
int n;
printf("Pid of %s: %d", argv[0], getpid());
printf("Initial program break : %p", sbrk(0));
for(n=0; n<5; n++) ptr[n] = malloc(16 * 1024);
printf("After 5 x 16kB malloc : %p", sbrk(0));
free(ptr[1]);
printf("After free of second 16kB : %p", sbrk(0));
ptr[5] = malloc(16 * 1024);
printf("After allocating 6th of 16kB : %p", sbrk(0));
free(ptr[5]);
printf("After freeing last block : %p", sbrk(0));
ptr[6] = malloc(18 * 1024);
printf("After allocating a new 18kB : %p", sbrk(0));
getchar();
return 0;
}
Cuando ejecute la aplicación, obtendrá un resultado similar al siguiente:
Pid of ./a.out: 31990
Initial program break : 0x55ebcadf4000
After 5 x 16kB malloc : 0x55ebcadf4000
After free of second 16kB : 0x55ebcadf4000
After allocating 6th of 16kB : 0x55ebcadf4000
After freeing last block : 0x55ebcadf4000
After allocating a new 18kB : 0x55ebcadf4000
La salida para brk con strace será la siguiente:
brk(NULL) = 0x5608595b6000
brk(0x5608595d7000) = 0x5608595d7000
Como puede ver, se agregó 0x21000 a la dirección final del campo de datos. Puede comprender esto a partir del valor 0x5608595d7000 . Entonces, se asignaron aproximadamente 0x21000 o 132 KB de memoria.
Hay dos puntos importantes a considerar aquí. El primero es la asignación de más de la cantidad especificada en el código de muestra. Otra es qué línea de código provocó la llamada brk que proporcionó la asignación.
Aleatorización del diseño del espacio de direcciones: ASLR
Cuando ejecuta la aplicación de ejemplo anterior una tras otra, verá diferentes valores de dirección cada vez. Hacer que el espacio de direcciones cambie aleatoriamente de esta manera complica significativamente el trabajo de los ataques de seguridad y aumenta la seguridad del software .
Sin embargo, en arquitecturas de 32 bits, generalmente se utilizan ocho bits para aleatorizar el espacio de direcciones. No será apropiado aumentar el número de bits ya que el área direccionable sobre los bits restantes será muy baja. Además, el uso de solo combinaciones de 8 bits no complica lo suficiente al atacante.
En las arquitecturas de 64 bits, por otro lado, dado que hay demasiados bits que se pueden asignar para la operación ASLR, se proporciona una aleatoriedad mucho mayor y aumenta el grado de seguridad.
El kernel de Linux también funciona con dispositivos basados en Android y la función ASLR está completamente activada en Android 4.0.3 y versiones posteriores. Incluso solo por esta razón, no estaría mal decir que un teléfono inteligente de 64 bits ofrece una ventaja de seguridad significativa sobre las versiones de 32 bits.
Al deshabilitar temporalmente la función ASLR con el siguiente comando, parecerá que la aplicación de prueba anterior devuelve los mismos valores de dirección cada vez que se ejecuta:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Para restaurarlo a su estado anterior, bastará con escribir 2 en lugar de 0 en el mismo archivo.
La segunda llamada al sistema: mmap
mmap es la segunda llamada al sistema utilizada para la asignación de memoria en Linux. Con la llamada mmap, el espacio libre en cualquier área de la memoria se asigna al espacio de direcciones del proceso de llamada.
En una asignación de memoria realizada de esta manera, cuando desee devolver la segunda partición de 16 KB con la función free() en el ejemplo anterior de brk, no existe ningún mecanismo para evitar esta operación. El segmento de memoria relevante se elimina del espacio de direcciones del proceso. Se marca como que ya no se usa y se devuelve al sistema.
Debido a que las asignaciones de memoria con mmap son muy lentas en comparación con aquellas con brk, se necesita una asignación de brk.
Con mmap, cualquier área libre de memoria se asigna al espacio de direcciones del proceso, por lo que el contenido del espacio asignado se restablece antes de que se complete este proceso. Si el restablecimiento no se hizo de esta manera, el siguiente proceso no relacionado también podría acceder a los datos pertenecientes al proceso que utilizó anteriormente el área de memoria relevante. Esto imposibilitaría hablar de seguridad en los sistemas.
Importancia de la asignación de memoria en Linux
La asignación de memoria es muy importante, especialmente en temas de optimización y seguridad. Como se ve en los ejemplos anteriores, no comprender completamente este problema puede significar destruir la seguridad de su sistema.
Incluso conceptos similares a push y pop que existen en muchos lenguajes de programación se basan en operaciones de asignación de memoria. Ser capaz de usar y dominar bien la memoria del sistema es vital tanto en la programación de sistemas integrados como en el desarrollo de una arquitectura de sistema segura y optimizada.