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?
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?
Respuestas:
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:
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 Rn
instrucció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 R4
establecerí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 R3
y se ejecuta en la nueva dirección.
Para regresar, sería SEP R5
lo 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 R3
comenzar 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.
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);
}
}
static
para esto. En su lugar, podría pasar la dirección como argumento a una llamada recursiva.
static
, si llama a esto más de una vez, las llamadas posteriores pueden fallar ...
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.
En muchos MIPS y modernas arquitecturas RISC (como PowerPC, RISC-V, SPARC ...) no existen push
y pop
las 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
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.
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.
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.
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;
}