En mi sistema Debian GNU / Linux 9, cuando se ejecuta un binario,
- la pila no está inicializada pero
- el montón está inicializado en cero.
¿Por qué?
Supongo que la inicialización cero promueve la seguridad, pero, si es para el montón, ¿por qué no también para la pila? ¿La pila tampoco necesita seguridad?
Mi pregunta no es específica de Debian hasta donde yo sé.
Código C de muestra:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 8;
// --------------------------------------------------------------------
// UNINTERESTING CODE
// --------------------------------------------------------------------
static void print_array(
const int *const p, const size_t size, const char *const name
)
{
printf("%s at %p: ", name, p);
for (size_t i = 0; i < size; ++i) printf("%d ", p[i]);
printf("\n");
}
// --------------------------------------------------------------------
// INTERESTING CODE
// --------------------------------------------------------------------
int main()
{
int a[n];
int *const b = malloc(n*sizeof(int));
print_array(a, n, "a");
print_array(b, n, "b");
free(b);
return 0;
}
Salida:
a at 0x7ffe118997e0: 194 0 294230047 32766 294230046 32766 -550453275 32713
b at 0x561d4bbfe010: 0 0 0 0 0 0 0 0
El estándar C no pide malloc()
borrar memoria antes de asignarlo, por supuesto, pero mi programa C es meramente ilustrativo. La pregunta no es una pregunta sobre C o sobre la biblioteca estándar de C. Más bien, la pregunta es por qué el kernel y / o el cargador de tiempo de ejecución están poniendo a cero el montón pero no la pila.
OTRO EXPERIMENTO
Mi pregunta se refiere al comportamiento observable de GNU / Linux en lugar de los requisitos de los documentos estándar. Si no está seguro de lo que quiero decir, intente este código, que invoca un comportamiento indefinido adicional ( indefinido, es decir, en lo que respecta al estándar C) para ilustrar el punto:
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(sizeof(int));
printf("%p %d ", p, *p);
++*p;
printf("%d\n", *p);
free(p);
}
return 0;
}
Salida de mi máquina:
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
0x555e86696010 0 1
En lo que respecta al estándar C, el comportamiento no está definido, por lo que mi pregunta no tiene en cuenta el estándar C. No es necesario que una llamada malloc()
devuelva la misma dirección cada vez, pero dado que esta llamada malloc()
sí devuelve la misma dirección cada vez, es interesante notar que la memoria, que está en el montón, se pone a cero cada vez.
La pila, por el contrario, no parecía estar puesta a cero.
No sé qué hará este último código en su máquina, ya que no sé qué capa del sistema GNU / Linux está causando el comportamiento observado. Solo puedes intentarlo.
ACTUALIZAR
@Kusalananda ha observado en comentarios:
Por lo que vale, su código más reciente devuelve diferentes direcciones y datos (no ocasionales) no inicializados (no cero) cuando se ejecuta en OpenBSD. Obviamente, esto no dice nada sobre el comportamiento que está presenciando en Linux.
Que mi resultado difiere del resultado en OpenBSD es realmente interesante. Aparentemente, mis experimentos descubrieron no un protocolo de seguridad del núcleo (o enlazador), como había pensado, sino un mero artefacto de implementación.
En este sentido, creo que, juntas, las respuestas a continuación de @mosvy, @StephenKitt y @AndreasGrapentin resuelven mi pregunta.
Ver también en Desbordamiento de pila: ¿Por qué malloc inicializa los valores a 0 en gcc? (crédito: @bta).
new
operador en C ++ (también "heap") está en Linux solo como un contenedor para malloc (); el núcleo no sabe ni le importa cuál es el "montón".Respuestas:
El almacenamiento devuelto por malloc () no tiene inicialización cero. Nunca asumas que es así.
En su programa de prueba, es solo una casualidad: supongo que
malloc()
acaba de obtener un nuevo bloqueommap()
, pero tampoco confíe en eso.Por ejemplo, si ejecuto su programa en mi máquina de esta manera:
Su segundo ejemplo es simplemente exponer un artefacto de la
malloc
implementación en glibc; si hace eso repetidomalloc
/free
con un búfer de más de 8 bytes, verá claramente que solo los primeros 8 bytes están a cero, como en el siguiente código de muestra.Salida:
fuente
Independientemente de cómo se inicializa la pila, no está viendo una pila prístina, porque la biblioteca C hace varias cosas antes de llamar
main
, y tocan la pila.Con la biblioteca GNU C, en x86-64, la ejecución comienza en el punto de entrada _start , que llama
__libc_start_main
para configurar las cosas, y este último termina llamandomain
. Pero antes de llamarmain
, llama a una serie de otras funciones, lo que hace que se escriban varios datos en la pila. El contenido de la pila no se borra entre las llamadas a funciones, por lo que cuando ingresamain
, su pila contiene los restos de las llamadas a funciones anteriores.Esto solo explica los resultados que obtiene de la pila, vea las otras respuestas con respecto a su enfoque general y suposiciones.
fuente
main()
se llama, las rutinas de inicialización pueden muy bien haber devuelto la memoria modificadamalloc()
, especialmente si las bibliotecas de C ++ están vinculadas. Asumir que el "montón" se inicializa a cualquier cosa es una suposición muy, muy mala.En ambos casos, obtienes memoria no inicializada y no puedes hacer suposiciones sobre su contenido.
Cuando el sistema operativo tiene que distribuir una nueva página a su proceso (ya sea para su pila o para la arena utilizada por
malloc()
), garantiza que no expondrá los datos de otros procesos; la forma habitual de asegurarse es llenarlo con ceros (pero es igualmente válido sobrescribir con cualquier otra cosa, incluso una página que valga la pena/dev/urandom
; de hecho, algunasmalloc()
implementaciones de depuración escriben patrones distintos de cero, para detectar suposiciones erróneas como la suya).Si
malloc()
puede satisfacer la solicitud de la memoria ya utilizada y liberada por este proceso, su contenido no se borrará (de hecho, la limpieza no tiene nada que vermalloc()
y no puede serlo; tiene que suceder antes de que la memoria se asigne a su espacio de direcciones) Puede obtener memoria que ha sido escrita previamente por su proceso / programa (por ejemplo, antesmain()
).En su programa de ejemplo, está viendo una
malloc()
región que aún no ha sido escrita por este proceso (es decir, es directa desde una página nueva) y una pila en la que se ha escrito (pormain()
código previo en su programa). Si examina más de la pila, encontrará que está llena de cero más abajo (en su dirección de crecimiento).Si realmente quiere comprender lo que está sucediendo a nivel del sistema operativo, le recomiendo que omita la capa de la Biblioteca C e interactúe utilizando llamadas del sistema como
brk()
y en summap()
lugar.fuente
malloc()
yfree()
repetidamente. Aunque nada requieremalloc()
reutilizar el mismo almacenamiento recientemente liberado, en el experimento,malloc()
sucedió eso. Sucedió que devolvía la misma dirección cada vez, pero también anuló la memoria cada vez, lo que no había esperado. Esto fue interesante para mi. Otros experimentos han llevado a la pregunta de hoy.malloc()
no hacen absolutamente nada con la memoria que le entregan, ya sea que se haya utilizado previamente o esté recién asignado (y, por lo tanto, el sistema operativo lo haya puesto a cero). En su prueba, evidentemente obtuvo el último. Del mismo modo, la memoria de la pila se entrega a su proceso en el estado borrado, pero no la examina lo suficiente como para ver partes que su proceso aún no ha tocado. Su memoria de pila se borra antes de que se le dé a su proceso.calloc
podría ser una opción (en lugar dememset
)mmap(MAP_ANONYMOUS)
menos que lo useMAP_POPULATE
también. Con suerte, las nuevas páginas de pila están respaldadas por páginas físicas nuevas y conectadas (mapeadas en las tablas de páginas de hardware, así como la lista de punteros / longitud del mapeo del núcleo) cuando crecen, porque normalmente se escribe nueva memoria de pila cuando se toca por primera vez . Pero sí, el núcleo debe evitar la pérdida de datos de alguna manera, y la reducción a cero es la más económica y útil.Tu premisa está mal.
Lo que usted describe como 'seguridad' es realmente confidencialidad , lo que significa que ningún proceso puede leer la memoria de otros procesos, a menos que esta memoria se comparta explícitamente entre estos procesos. En un sistema operativo, este es un aspecto del aislamiento de actividades o procesos concurrentes.
Lo que está haciendo el sistema operativo para garantizar este aislamiento es cuando el proceso solicita memoria para asignaciones de montón o pila, esta memoria proviene de una región en memoria física que se llena con ceros, o que se llena con basura que es viniendo del mismo proceso .
Esto garantiza que solo verá ceros, o su propia basura, por lo que se garantiza la confidencialidad, y tanto el montón como la pila son 'seguros', aunque no necesariamente inicializados (cero).
Estás leyendo demasiado en tus medidas.
fuente