¿Por qué seguimos haciendo crecer la pila al revés?

46

Al compilar código C y mirar el ensamblaje, todo hace que la pila crezca hacia atrás de la siguiente manera:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- ¿Significa esto que el puntero base o el puntero de la pila en realidad están bajando las direcciones de memoria en lugar de subir? ¿Porqué es eso?

Cambié $5, -4(%rbp)a $5, +4(%rbp), compilé y ejecuté el código y no hubo errores. Entonces, ¿por qué todavía tenemos que retroceder en la pila de memoria?

alex
fuente
2
Tenga en cuenta que -4(%rbp)no mueve el puntero base en absoluto y que +4(%rbp)no podría haber funcionado.
Margaret Bloom
14
" ¿Por qué tenemos que ir todavía hacia atrás ? ¿Cuál crees que sería la ventaja de avanzar? En definitiva, no importa, solo tienes que elegir uno.
Bergi
31
"¿Por qué hacemos crecer la pila al revés?" - porque si no lo hacemos, alguien más preguntaría por qué malloccrece el montón hacia atrás
slebetman
2
@MargaretBloom: Aparentemente en la plataforma de OP, al código de inicio de CRT no le importa si maincambia su RBP. Eso es ciertamente posible. (Y sí, escribir 4(%rbp)pisaría el valor RBP guardado). Err en realidad, este principal nunca lo hace mov %rsp, %rbp, por lo que el acceso a la memoria es relativo al RBP de la persona que llama , si eso es lo que el OP realmente probó. Si esto se copió de la salida del compilador, ¡se omitieron algunas instrucciones!
Peter Cordes
1
Me parece que "hacia atrás" o "hacia adelante" (o "abajo" y "arriba") depende de su punto de vista. Si ha diagramado la memoria como una columna con direcciones bajas en la parte superior, entonces el crecimiento de la pila disminuyendo un puntero de pila sería análogo a una pila física.
jamesdlin

Respuestas:

86

¿Significa esto que el puntero base o el puntero de pila se mueven hacia abajo en las direcciones de memoria en lugar de subir? ¿Porqué es eso?

Sí, las pushinstrucciones disminuyen el puntero de la pila y escriben en la pila, mientras que pophacen lo contrario, leen la pila e incrementan el puntero de la pila.

Esto es algo histórico en el sentido de que para máquinas con memoria limitada, la pila se colocó alta y se hizo crecer hacia abajo, mientras que el montón se colocó bajo y se hizo crecer hacia arriba. Solo hay una brecha de "memoria libre": entre el montón y la pila, y esta brecha se comparte, cualquiera puede crecer en la brecha según sea necesario individualmente. Por lo tanto, el programa solo se queda sin memoria cuando la pila y el montón chocan sin dejar memoria libre. 

Si la pila y el montón crecen en la misma dirección, entonces hay dos huecos, y la pila realmente no puede crecer en la brecha del montón (lo contrario también es problemático).

Originalmente, los procesadores no tenían instrucciones de manejo de pila dedicadas. Sin embargo, a medida que se agregó soporte de pila al hardware, adoptó este patrón de crecimiento descendente, y los procesadores aún siguen este patrón hoy.

Se podría argumentar que en una máquina de 64 bits hay suficiente espacio de direcciones para permitir múltiples espacios, y como evidencia, múltiples espacios son necesariamente el caso cuando un proceso tiene múltiples hilos. Aunque esto no es suficiente motivación para cambiar las cosas, ya que con los sistemas de brechas múltiples, la dirección del crecimiento es posiblemente arbitraria, por lo que la tradición / compatibilidad inclina la balanza.


Habría que cambiar las instrucciones de manejo de la CPU pila con el fin de cambiar la dirección de la pila, o bien renunciar a la utilización de las empujando y haciendo estallar las instrucciones (por ejemplo, dedicados push, pop, call, ret, otros).

Tenga en cuenta que la arquitectura del conjunto de instrucciones MIPS no tiene push& dedicado pop, por lo que es práctico hacer crecer la pila en cualquier dirección: aún puede desear un diseño de memoria de un espacio para un proceso de subproceso único, pero podría aumentar la pila hacia arriba y el montón hacia abajo. Sin embargo, si hiciera eso, algunos códigos de C varargs podrían requerir un ajuste en la fuente o en el paso de parámetros bajo el capó.

(De hecho, dado que no hay un manejo dedicado de la pila en MIPS, podríamos usar pre o post incremento o pre o post decremento para empujar a la pila siempre que usemos el reverso exacto para saltar de la pila, y también asumiendo que el sistema operativo respeta el modelo de uso de pila elegido. De hecho, en algunos sistemas embebidos y algunos sistemas educativos, la pila MIPS crece hacia arriba).

Erik Eidt
fuente
32
No es sólo pushy popen la mayoría de las arquitecturas, sino también la interrupción de manipulación mucho más importante, call, ret, y todo lo que se ha horneado en interacción con la pila.
Deduplicador
3
ARM puede tener los cuatro sabores de pila.
Margaret Bloom
14
Por lo que vale, no creo que "la dirección del crecimiento sea arbitraria" en el sentido de que cualquiera de las dos opciones es igualmente buena. Crecer tiene la propiedad de que el desbordamiento del final de un búfer registra cuadros de pila anteriores, incluidas las direcciones de retorno guardadas. Crecer tiene la propiedad de que el desbordamiento del extremo de un búfer registra solo el almacenamiento en el mismo o más tarde (si el búfer no está en el último, puede haber más tarde) marco de llamada, y posiblemente incluso solo espacio no utilizado (todos suponiendo un guardia página después de la pila). Entonces, desde una perspectiva de seguridad, crecer es muy preferible
R ..
66
@R ..: crecer no elimina las vulnerabilidades de desbordamiento del búfer, porque las funciones vulnerables generalmente no son funciones de hoja: llaman a otras funciones, colocando una dirección de retorno sobre el búfer. Las funciones de hoja que obtienen un puntero de su interlocutor pueden volverse susceptibles de sobrescribir su propia dirección de remitente. por ejemplo, si una función asigna un búfer en la pila y se lo pasa gets(), o hace algo strcpy()que no está en línea, entonces el retorno en esas funciones de la biblioteca utilizará la dirección de retorno sobrescrita. Actualmente con pilas de crecimiento descendente, es cuando regresa la persona que llama.
Peter Cordes
55
@PeterCordes: De hecho, mi comentario señaló que los marcos de pila del mismo nivel o más recientes que el búfer desbordado aún son potencialmente clobberables, pero eso es mucho menos. En el caso donde la función de clobbering es una función de hoja directamente llamada por la función cuyo buffer es (por ejemplo strcpy), en un arco donde la dirección de retorno se mantiene en un registro a menos que deba ser derramada, no hay acceso a clobber el retorno habla a.
R ..
8

En su sistema específico, la pila comienza desde una dirección de memoria alta y "crece" hacia abajo a direcciones de memoria baja. (el caso simétrico de menor a mayor también existe)

Y dado que cambiaste de -4 y +4 y se ejecutó, no significa que sea correcto. El diseño de memoria de un programa en ejecución es más complejo y depende de muchos otros factores que pueden contribuir al hecho de que no se bloqueó instantáneamente en este programa extremadamente simple.

nadir
fuente
1

El puntero de la pila apunta al límite entre la memoria de pila asignada y la no asignada. Crecerlo hacia abajo significa que apunta al comienzo de la primera estructura en el espacio de pila asignado, con otros elementos asignados que siguen en direcciones más grandes. Hacer que los punteros apunten al inicio de las estructuras asignadas es mucho más común que al revés.

Ahora en muchos sistemas en estos días, hay un registro separado para los marcos de la pila que se pueden desenrollar de manera confiable para descubrir la cadena de llamadas, con almacenamiento local variable intercalado. La forma en que este registro de marco de pila se configura en algunas arquitecturas significa que termina apuntando detrás del almacenamiento de variables locales en lugar del puntero de pila anterior . Entonces, el uso de este registro de marco de pila requiere una indexación negativa.

Tenga en cuenta que los marcos de pila y su indexación son un aspecto secundario de los lenguajes informáticos compilados, por lo que es el generador de código del compilador el que tiene que lidiar con la "antinaturalidad" en lugar de un mal programador de lenguaje ensamblador.

Entonces, si bien hubo buenas razones históricas para elegir que las pilas crezcan hacia abajo (y algunas de ellas se conservan si programa en lenguaje ensamblador y no se molesta en configurar un marco de pila adecuado), se han vuelto menos visibles.


fuente
2
"Ahora, en muchos sistemas en estos días, hay un registro separado para los marcos de la pila" está atrasado. Los formatos de información de depuración más ricos han eliminado en gran medida la necesidad de punteros de trama hoy en día.
Peter Green