¿Cuál es la dirección del crecimiento de la pila en la mayoría de los sistemas modernos?

102

Estoy preparando algunos materiales de capacitación en C y quiero que mis ejemplos se ajusten al modelo de pila típico.

¿En qué dirección crece una pila C en Linux, Windows, Mac OSX (PPC y x86), Solaris y los Unixes más recientes?

Uri
fuente
3
Una versión descendente de por qué: stackoverflow.com/questions/2035568/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Respuestas:

147

El crecimiento de la pila no suele depender del sistema operativo en sí, sino del procesador en el que se ejecuta. Solaris, por ejemplo, se ejecuta en x86 y SPARC. Mac OSX (como mencionaste) se ejecuta en PPC y x86. Linux funciona con todo, desde mi gran System z en el trabajo hasta un diminuto reloj de pulsera .

Si la CPU ofrece algún tipo de elección, la convención ABI / llamada utilizada por el sistema operativo especifica qué elección debe hacer si desea que su código llame al código de todos los demás.

Los procesadores y su dirección son:

  • x86: abajo.
  • SPARC: seleccionable. El ABI estándar usa plumón.
  • PPC: abajo, creo.
  • System z: en una lista vinculada, no bromeo (pero aún así, al menos para zLinux).
  • ARM: seleccionable, pero Thumb2 tiene codificaciones compactas solo para abajo (LDMIA = incremento después, STMDB = decremento antes).
  • 6502: abajo (pero solo 256 bytes).
  • RCA 1802A: de la forma que desee, sujeto a la implementación de SCRT.
  • PDP11: abajo.
  • 8051: arriba.

Mostrando mi edad en esos últimos, el 1802 fue el chip utilizado para controlar los primeros transbordadores (sospecho que detectaba si las puertas estaban abiertas, según la potencia de procesamiento que tenía :-) y mi segunda computadora, la COMX-35 ( siguiendo mi ZX80 ).

Detalles de PDP11 obtenidos de aquí , 8051 detalles de aquí .

La arquitectura SPARC utiliza un modelo de registro de ventana deslizante. Los detalles arquitectónicamente visibles también incluyen un búfer circular de ventanas de registro que son válidas y se almacenan en caché internamente, con trampas cuando se desborda / desborda. Consulte aquí para obtener más detalles. Como explica el manual de SPARCv8, las instrucciones GUARDAR y RESTAURAR son como las instrucciones AGREGAR más la rotación de la ventana de registro. El uso de una constante positiva en lugar de la negativa habitual daría una pila de crecimiento ascendente.

La técnica SCRT antes mencionada es otra: el 1802 usó algunos o sus dieciséis registros de 16 bits para SCRT (técnica estándar de llamada y retorno). Uno era el contador del programa, se podía utilizar cualquier registro como PC con la SEP Rninstrucción. Uno era el puntero de la pila y dos estaban configurados siempre para apuntar a la dirección del código SCRT, uno para llamar y otro para devolver. Ningún registro fue tratado de manera especial. Tenga en cuenta que estos detalles son de memoria, es posible que no sean totalmente correctos.

Por ejemplo, si R3 era la PC, R4 era la dirección de llamada SCRT, R5 era la dirección de retorno SCRT y R2 era la "pila" (comillas, ya que está implementado en el software), SEP R4establecería R4 como la PC y comenzaría a ejecutar SCRT código de llamada.

Luego almacenaría R3 en la "pila" de R2 (creo que R6 se usó para almacenamiento temporal), ajustándolo hacia arriba o hacia abajo, tomando los dos bytes que siguen a R3, los carga en R3, luego lo hace SEP R3y se ejecuta en la nueva dirección.

Para regresar, sería SEP R5lo que sacaría la dirección anterior de la pila R2, agregaría dos (para omitir los bytes de dirección de la llamada), cargarla en R3 y SEP R3comenzar a ejecutar el código anterior.

Muy difícil de entender al principio después de todo el código basado en pila 6502/6809 / z80, pero aún así elegante en una especie de golpe de cabeza contra la pared. También una de las características más vendidas del chip fue un conjunto completo de 16 registros de 16 bits, a pesar de que de inmediato perdió 7 de ellos (5 para SCRT, dos para DMA e interrupciones de la memoria). Ahh, el triunfo del marketing sobre la realidad :-)

System z es bastante similar, utilizando sus registros R14 y R15 para llamada / retorno.

paxdiablo
fuente
3
Para agregar a la lista, ARM puede crecer en cualquier dirección, pero puede establecerse en una u otra mediante una implementación de silicio particular (o puede dejarse seleccionable por software). Los pocos con los que he tratado siempre han estado en modo de crecimiento.
Michael Burr
1
En la pequeña parte del mundo ARM que he visto hasta ahora (ARM7TDMI), la pila se maneja completamente en software. Las direcciones de retorno se almacenan en un registro que es guardado por software si es necesario, y las instrucciones de pre / post-incremento / decremento permiten ponerlo y otras cosas en la pila en cualquier dirección.
starblue
1
Una de las HPPA, ¡la pila creció! Bastante raro entre arquitecturas razonablemente modernas.
tml
2
Para los curiosos, aquí hay un buen recurso sobre cómo funciona la pila en z / OS: www-03.ibm.com/systems/resources/Stack+and+Heap.pdf
Dillon Cower
1
Gracias @paxdiablo por tu comprensión. A veces, las personas lo toman como una afrenta personal cuando haces un comentario de este tipo, especialmente cuando es más antiguo. Solo sé que hay una diferencia porque yo mismo cometí el mismo error en el pasado. Cuídate.
CasaDeRobison
23

En C ++ (adaptable a C) stack.cc :

static int
find_stack_direction ()
{
    static char *addr = 0;
    auto char dummy;
    if (addr == 0)
    {
        addr = &dummy;
        return find_stack_direction ();
    }
    else
    {
        return ((&dummy > addr) ? 1 : -1);
    }
}
jfs
fuente
14
Vaya, ha pasado mucho tiempo desde que vi la palabra clave "auto".
paxdiablo
9
(& dummy> addr) no está definido. El resultado de alimentar dos punteros a un operador relacional se define solo si los dos punteros apuntan dentro de la misma matriz o estructura.
sigjuice
2
Tratar de investigar el diseño de su propia pila, algo que C / C ++ no especifica en absoluto, es "intransitable" para empezar, por lo que realmente no me importaría. Sin embargo, parece que esta función solo funcionará correctamente una vez.
ephemient
9
No es necesario utilizar un staticpara esto. En su lugar, podría pasar la dirección como argumento a una llamada recursiva.
R .. GitHub DEJA DE AYUDAR A ICE
5
Además, al usar a static, si llama a esto más de una vez, las llamadas posteriores pueden fallar ...
Chris Dodd
7

La ventaja de crecer hacia abajo es que en los sistemas más antiguos, la pila estaba típicamente en la parte superior de la memoria. Los programas generalmente llenaban la memoria comenzando desde la parte inferior, por lo que este tipo de administración de memoria minimizó la necesidad de medir y colocar la parte inferior de la pila en algún lugar sensible.

mP.
fuente
3
No es una "ventaja", en realidad una tautología.
Marqués de Lorne
1
No es una tautología. El punto es tener dos regiones de memoria en crecimiento que no interfieran (a menos que la memoria esté llena de todos modos), como señaló @valenok.
YvesgereY
6

La pila crece hacia abajo en x86 (definido por la arquitectura, pop incrementa el puntero de pila, empuja decrementa).

Miguel
fuente
5

En muchos MIPS y modernas arquitecturas RISC (como PowerPC, RISC-V, SPARC ...) no existen pushy poplas instrucciones. Esas operaciones se realizan explícitamente ajustando manualmente el puntero de la pila y luego cargando / almacenando el valor en relación con el puntero ajustado. Todos los registros (excepto el registro cero) son de propósito general, por lo que, en teoría, cualquier registro puede ser un puntero de pila, y la pila puede crecer en cualquier dirección que desee el programador.

Dicho esto, la pila generalmente crece hacia abajo en la mayoría de las arquitecturas, probablemente para evitar el caso en que la pila y los datos del programa o los datos de la pila crecen y chocan entre sí. También están las excelentes razones de direccionamiento mencionadas en la respuesta de sh- . Algunos ejemplos: MIPS ABI crece hacia abajo y usa $29(AKA $sp) como puntero de pila, RISC-V ABI también crece hacia abajo y usa x2 como puntero de pila

En Intel 8051, la pila crece, probablemente porque el espacio de memoria es tan pequeño (128 bytes en la versión original) que no hay pila y no es necesario colocar la pila en la parte superior para que se separe de la pila que crece desde la parte inferior

Puede encontrar más información sobre el uso de la pila en varias arquitecturas en https://en.wikipedia.org/wiki/Calling_convention

Ver también

phuclv
fuente
2

Solo una pequeña adición a las otras respuestas, que por lo que puedo ver no han tocado este punto:

Hacer que la pila crezca hacia abajo hace que todas las direcciones dentro de la pila tengan un desplazamiento positivo en relación con el puntero de la pila. No hay necesidad de compensaciones negativas, ya que solo apuntarían al espacio de pila no utilizado. Esto simplifica el acceso a las ubicaciones de la pila cuando el procesador admite el direccionamiento relativo al puntero de la pila.

Muchos procesadores tienen instrucciones que permiten accesos con un desplazamiento solo positivo en relación con algún registro. Entre ellos se incluyen muchas arquitecturas modernas, así como algunas antiguas. Por ejemplo, ARM Thumb ABI proporciona accesos relativos al puntero de pila con un desplazamiento positivo codificado dentro de una sola palabra de instrucción de 16 bits.

Si la pila creciera hacia arriba, todas las compensaciones útiles relativas al puntero de pila serían negativas, lo que es menos intuitivo y menos conveniente. También está en desacuerdo con otras aplicaciones de direccionamiento relativo al registro, por ejemplo, para acceder a campos de una estructura.

sh-
fuente
2

En la mayoría de los sistemas, la pila disminuye y mi artículo en https://gist.github.com/cpq/8598782 explica POR QUÉ disminuye. Es simple: ¿cómo diseñar dos bloques de memoria en crecimiento (montón y pila) en una porción fija de memoria? La mejor solución es ponerlos en los extremos opuestos y dejarlos crecer uno hacia el otro.

Valenok
fuente
esa esencia parece estar muerta ahora :(
Ven
@Ven - Puedo hacerlo
Brett Holman
1

Crece porque la memoria asignada al programa tiene los "datos permanentes", es decir, el código del programa en sí en la parte inferior, luego el montón en el medio. Necesita otro punto fijo desde el que hacer referencia a la pila, de modo que eso le deja en la parte superior. Esto significa que la pila crece hacia abajo, hasta que es potencialmente adyacente a los objetos en el montón.

Kai
fuente
0

Esta macro debería detectarlo en tiempo de ejecución sin UB:

#define stk_grows_up_eh() stk_grows_up__(&(char){0})
_Bool stk_grows_up__(char *ParentsLocal);

__attribute((__noinline__))
_Bool stk_grows_up__(char *ParentsLocal) { 
    return (uintptr_t)ParentsLocal < (uintptr_t)&ParentsLocal;
}
PSkocik
fuente