Cuando llegas y entras en tu habitación asignas tres cajas. Las apilas en sus lados para que puedas ver el extremo abierto de cada caja. Pones el sombrero, el reloj y la taza en las cajas, un objeto en cada una. Esa es la clave para que la pila sea útil para la computación de propósito general. Puedes tener acceso aleatorio a tu pila asignada durante el tiempo que estés en tu habitación. Cuando te mudas, liberas esas cajas y las devuelves al montón de cajas de la pila vacías.
Consigues tu habitación, vas a asignar una caja, la colocas de tal manera que el lado abierto esté de cara a ti (no está arriba o abajo sino a un lado). Pon tu taza en ella. Vas a asignar otra caja, la colocas encima de la primera caja apilada, también con el extremo abierto hacia ti, y pones tu sombrero en ella. Repite la operación hasta que tengas los artículos deseados almacenados en cajas de apilamiento. Mientras dure tu estancia puedes acceder al azar (cualquier artículo en cualquier orden) a cualquiera de las veces de tu pila. Cuando te vayas, vacías y devuelves una caja a la vez del propietario de las cajas de la pila.
Es cierto que apilarlas de forma que el extremo abierto quede cubierto por la siguiente caja de la pila y sólo se pueda acceder al elemento de la parte superior de la pila no funciona ni tiene sentido para la informática de propósito general (con lenguajes de programación compilados de alto nivel). Hay un caso de uso que se encuentra en algunos procesadores. Las direcciones de retorno de las llamadas a funciones. Se anidan las llamadas a funciones y no se puede hacer trampa, hay que regresar de una antes de poder regresar de la siguiente superior. Así que puedes usar una pila de tal manera que sólo puedes ver en cualquier momento el contenido del elemento en la parte superior de la pila y todos los demás están bloqueados de la vista.
Así que, ¿por qué no probarlo?
unsigned int fun ( unsigned int a, unsigned int b )
{
unsigned int x;
x=b;
return a+x;
}
Sin optimizar:
Disassembly of section .text:
00000000 <fun>:
0: e24dd010 sub sp, sp, #16
4: e58d0004 str r0, [sp, #4]
8: e58d1000 str r1, [sp]
c: e59d3000 ldr r3, [sp]
10: e58d300c str r3, [sp, #12]
14: e59d2004 ldr r2, [sp, #4]
18: e59d300c ldr r3, [sp, #12]
1c: e0823003 add r3, r2, r3
20: e1a00003 mov r0, r3
24: e28dd010 add sp, sp, #16
28: e12fff1e bx lr
Tenemos idealmente tres cosas en la pila, copia de a, copia de b y x. Son 12 bytes. La convención de llamada para esta herramienta y objetivo dice que la pila debe estar alineada en un límite de 64 bits, por lo que el compilador en su lugar asigna 16 bytes.
0: e24dd010 sub sp, sp, #16
Ajustar el puntero de la pila, similar a hacer cuatro push en la pila. La pila crece hacia abajo (bastante común) en el espacio de direcciones. La pila crece hacia arriba desde las direcciones más bajas hacia las más altas.
En este momento no sabemos ni nos importa lo que hay en esas ubicaciones de la pila. Suponemos que es basura.
4: e58d0004 str r0, [sp, #4]
Esta es la clave de su comprensión. El puntero de la pila se puede utilizar con un desplazamiento (no era y no es siempre el caso). Así que usted 1) no tiene que empujar para asignar y 2) no tiene que pop para descubrir y descubrir los elementos que fueron empujados.
r0 es donde se pasa la variable a así:
[sp+12] [sp+8] [sp+4] escriba aquí la variable a (r0) [sp+0]
8: e58d1000 str r1, [sp]
r1 contiene la variable b según la convención
[sp+12] [sp+8] [sp+4] a [sp+0] escriba b aquí
c: e59d3000 ldr r3, [sp]
[sp+12] [sp+8] [sp+4] a [sp+0] b
leer el valor b de la pila en r3
10: e58d300c str r3, [sp, #12]
[sp+12] esto es x, escribe r3 (el valor b) aquí [sp+8] [sp+4] a [sp+0] b
14: e59d2004 ldr r2, [sp, #4]
leer a en r2
[sp+12] x [sp+8] [sp+4] a [sp+0] b
18: e59d300c ldr r3, [sp, #12]
leer x en r3
[sp+12] x [sp+8] [sp+4] a [sp+0] b
1c: e0823003 add r3, r2, r3
sumar r2 (a) y r3 (x) y guardar en r3
20: e1a00003 mov r0, r3
hacer de r3 (a+x)(que es a+b) el valor de retorno (r0)
sip, absolutamente podrían haber hecho una suma r0,r3,r2 pero esto no está optimizado y por el momento r3 mantiene x
24: e28dd010 add sp, sp, #16
siempre devuelve la pila (puntero) tal y como la encontraste
28: e12fff1e bx lr
Para ser justos, esto es lo que hace la optimización:
00000000 <fun>:
0: e0810000 add r0, r1, r0
4: e12fff1e bx lr
Aquí hay otro y este está técnicamente conectado con los primeros días del concepto de pila.
unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
return(a+more_fun(b));
}
Optimizado:
00000000 <fun>:
0: e92d4010 push {r4, lr}
4: e1a04000 mov r4, r0
8: e1a00001 mov r0, r1
c: ebfffffe bl 0 <more_fun>
10: e0800004 add r0, r0, r4
14: e8bd4010 pop {r4, lr}
18: e12fff1e bx lr
Si ayuda a entenderlo, esto es el equivalente funcional de lo anterior:
push {lr}
push {r4}
mov r4, r0
mov r0, r1
bl <more_fun>
add r0, r0, r4
pop {r4}
pop {lr}
bx lr
Así que hay varias cosas que suceden aquí. Estamos empujando dos cosas en la pila. r4 y lr. Por la convención de llamada no podemos destruir r4 (r0-r3 sí, no r4). Así que si queremos usar r4 tenemos que guardarlo y restaurarlo (uso perfecto de la pila). lr contiene la dirección de retorno de la función. bl modifica lr, así que tenemos que guardar lr para esta función para que la llamada a more_fun no pierda nuestro lugar en el mundo. r0 es el primer parámetro, a, para la llamada a fun, también es el primer parámetro de la llamada a more_fun así como el valor de retorno de more_fun, así que tenemos que guardar a en algún sitio, y el compilador eligió usar r4, que veremos por qué más adelante. En lugar de guardar r0 en la pila, guarda algún otro registro en la pila (que more_fun y todas las llamadas a funciones anidadas devolverán/dejarán sin tocar) y guarda r0 (a) en ese registro (r4).
He creado esta función para que la variable a se utilice después de la llamada a more_fun, lo que significa que tenemos que recordar la variable a a través de una llamada. Y el compilador eligió empujar r4 y guardar a en r4.
0: e92d4010 push {r4, lr}
guardar la dirección de retorno y conservar el contenido de r4 para poder utilizarlo en la función
4: e1a04000 mov r4, r0
guardar el valor a en r4
8: e1a00001 mov r0, r1
el valor b (r1) es el primer parámetro (r0) de la llamada a more_fun, prepara la llamada a more_fun poniendo b en r0
c: ebfffffe bl 0 <more_fun>
llamar a more_fun
10: e0800004 add r0, r0, r4
sumar el valor de retorno de more_fun y la variable en r4 (a) y guardar en r0 (el valor de retorno de fun())
14: e8bd4010 pop {r4, lr}
restaurar el contenido de r4 al valor encontrado cuando la función comenzó. restaurar la dirección de retorno de fun()
18: e12fff1e bx lr
retorno de la llamada a la función.
Quizá se pregunte por qué no hacerlo así:
push {r0}
mov r0, r1
push {lr}
bl 0 <more_fun>
pop {lr}
pop {r1}
add r0, r0, r1
bx lr
push {r0}
guardar la variable a en la pila la necesitamos más tarde
mov r0, r1
preparar la llamada a more_fun poniendo b como primer parámetro
push {lr}
bl modifica lr, lr es la dirección que necesitamos devolver de fun() por lo que necesitamos guardarla temporalmente en la pila.
bl 0 <more_fun>
llame a más diversión (tenga en cuenta que esto está desvinculado el enlazador rellenaría la dirección relativa más tarde)
pop {lr}
restaurar la dirección de retorno a lr
pop {r1}
recuperar el valor de la variable a en r1
add r0, r0, r1
añade el valor de retorno de more_fun más la variable a. r0 es el valor de retorno de la función fun()
bx lr
retorno de fun();
Si la convención de llamada no tenía una regla de alineación de la pila no hay nada de malo en que el compilador genere código así. Utiliza mucho menos espacio en la pila cuando empiezas a anidar funciones (lo que hacemos normalmente). Solemos llegar a quemar un segundo registro como puntero de marco, añadir más instrucciones por función para preparar el marco y restaurarlo al final. ¿Por qué? para poder desenrollar pilas con herramientas de depuración, no me sirven los depuradores y menos desenrollar pilas de alguna manera automática. Personalmente no tengo necesidad de quemar permanentemente código y espacio de memoria para algo que nunca usaré.
Pre-asignar todo lo que necesitamos para la duración de la función hace que sea más fácil de leer para los humanos (una variable específica en la pila estará o puede estar siempre en un desplazamiento fijo de la pila o del puntero del marco (ambos)). Esto facilita la generación de código desde el compilador y facilita la depuración del código generado, lo que hace que el compilador sea más fiable y tenga menos errores. A costa de la memoria para el usuario. Los propios autores del compilador son más que capaces de escribir código para llevar la cuenta de los elementos de la pila durante la función
[sp+0] variable a
empuje b
[sp+4] variable a [sp+0] variable b
Se puede generar fácilmente código para saber que en este punto de la función a está en [sp+4]
pop b
[sp+0] variable a
y en este punto de la función a está en [sp+0].
Buena suerte para conseguir que los compiladores lo hagan. Es lo que es, si se utiliza un lenguaje de alto nivel se acepta automáticamente un mayor consumo de código y datos y un golpe de rendimiento. Es cierto que se obtiene más código de calidad, depurado y más fácil de mantener y reutilizar.
Pero a partir de un concepto tradicional, elemental, de escribir cosas en una tarjeta de notas y apilar las tarjetas de notas para guardar cosas temporalmente . Esto demuestra que
push {r0}
mov r0, r1
push {lr}
bl 0 <more_fun>
pop {lr}
pop {r1}
add r0, r0, r1
bx lr
Ahora, ¿por qué empujar r4 y no r0?
push {r4, lr} push {r0,lr}
mov r4, r0
mov r0, r1 mov r0,r1
bl 0 <more_fun> bl more_fun
ldr r1,[sp]
add r0, r0, r4 add r0,r0,r1
pop {r4, lr} pop {r1,lr}
bx lr bx lr
mismo número de instrucciones. Pero cambias una operación de registro por una operación de memoria que es más lenta/peor. Así que hacer lo de r4 ya es mejor aquí. Añade más variables (no demasiadas) y hay un punto dulce en el que usar más registros en la función (r4,r5,r6...) empujándolos al principio y sacándolos al final, es mucho más eficiente que hacer accesos a la pila durante la función, no sólo una instrucción más lenta cambiada por otra.
Se puede prescindir de las pilas si se puede prescindir de los lenguajes de programación (relativamente modernos) (si se escribe en lenguaje ensamblador, por ejemplo). Los lenguajes compilados dan lugar a convenciones de llamada y a variables locales y globales. Las variables locales, las direcciones de retorno, etc. conducen a una pila y a un puntero de pila y a un direccionamiento relativo del puntero de pila. Sin las instrucciones de direccionamiento relativo del puntero de la pila (o del marco de la pila), los lenguajes compilados se vuelven mucho menos atractivos para ese procesador.