USANDO STACK PARA ENVIAR PARAMETROS A FUNCIONES
Para usar la Stack y enviar argumentos a una función, es necesario conocer el funcionamiento de los registers ESP y EBP que se encargan de apuntar a la posición actual de la Stack y apuntar a los datos almacenados respectivamente.
Como se ha indicado anteriormente, la Stack en un segmento de ejecución que contiene valores de memoria Alta y Baja que se van apilando en base a una pila FIFO, cuando se almacenan valores en dicha pila, necesariamente el valor de memoria del register ESP se actualiza para apuntar a la nueva posición del ultimo elemento almacenado en la pila, de esta forma se mantiene la consistencia de la misma y se puede invocar a operaciones PUSH y POP sobre la pila de forma segura, es decir, sin tener la preocupación de que se vayan a recuperar o almacenar valores en posiciones de memoria inconsistentes.
Partiendo de un ejemplo sencillo, se podría asumir que se cuenta con el flujo normal de un programa, donde este a su vez, invoca a una función que realiza una serie de operaciones para retornar algún tipo de resultado. Esta función que recibe parámetros por medio de la Stack (como se verá más adelante utilizando el register EBP) almacena dichos parámetros directamente en la Stack, lo que obliga al register ESP moverse a la nueva posición de memoria (el ultimo elemento almacenado en la Stack), posteriormente cuando la función ha finalizado, el siguiente elemento que se almacena en la Stack es el retorno que se indica con la instrucción RET, posteriormente el EBP almacena su valor antiguo en la pila también, actualizando su posición de memoria para apuntar a dicha posición, hasta este punto tanto EBP como ESP apuntan a la misma dirección de memoria, sin embargo, después de almacenar el valor antiguo del EBP, se almacenan las variables locales de la función, de este modo, tenemos hasta este punto un orden consistente de almacenamiento en la Stack, en primer lugar, se almacenan los parámetros en su orden de aparición, en segundo lugar se almacena el resultado de la función, en tercer lugar se almacena el valor del EBP y la posición de este register apuntará a esta posición de memoria y finalmente, en la cima de la pila, se almacena como ultimo valor, las variables locales de la función. Todas estas posiciones de memoria, hacen que el ESP se actualice en cada operación, es decir, Parámetros, Valores de retorno, EBP y finalmente variables locales a la función.
La siguiente Tabla indica como se mapea en la pila la explicación anterior
Pila Plataforma 32 bits
High Memory
-
Parametro 4 (valor=20) 20(%EBP) Parametro 3 (valor=20) 16(%EBP) Parametro 2 (valor=10) 12(%EBP) Parametro 1 (valor=2) 8(%EBP) RET 4(%EBP) EBP-old %EBP (%EBP) Variable Local 1 (valor= 52) -4(%EBP) Variable Local 2 (valor=0) %ESP -8(%EBP)
Low Memory
Pila Plataforma 64 bits
High Memory
-
Parametro 4 (valor=20) 40(%EBP) Parametro 3 (valor=20) 32(%EBP) Parametro 2 (valor=10) 24(%EBP) Parametro 1 (valor=2) 16(%EBP) RET 8(%EBP) EBP-old %EBP (%EBP) Variable Local 1 (valor= 52) -8(%EBP) Variable Local 2 (valor=0) %ESP -16(%EBP)
Low Memory
En la tabla anterior se aprecia la Stack con 4 parámetros almacenados por la ejecución de una función, ademas de el valor de retorno, el valor antiguo del EBP y dos variables locales a la función, en esta tabla se aprecia, como el rgister ESP apunta a la ultima variable local de la función, que corresponde a la ultima referencia y el EBP apunta a la posición en la Stack donde se ha almacenado el valor antiguo del EBP.
En Assembly podemos acceder al valor de un parámetro almacenado en la pila por medio de la posición de memoria del register EBP siguiendo la nomenclatura en tópicos anteriores, es decir, acceder directamente a la posición de memoria por medio del uso de paréntesis en un register dado, en este caso si se desea acceder a uno de los parámetros o al valor de retorno es necesario ir sumando posiciones de 4 bytes (que es el tamaño de cada elemento en la Stack) de esta forma se puede acceder a la dirección de memoria de cualquier valor almacenado en la Stack en el caso de que los valores se encuentren por encima de la posición actual del register EBP se debe restar en segmentos de 4 bytes (arquitectura de 32 bits), acercándose de esta forma al limite inferior de la pila (Low Memory), esto para acceder por ejemplo a las variables localmente definidas.
Ejemplo de Funciones con paso de valores por medio de Stack
#Ejemplo del uso de Stack en funciones.dataHelloWorld:
.asciz «Hello» .bss .lcomm Longitud, 100 .lcomm Valor, 100 .text .globl _start .type PrintFunction, @function PrintFunction: pushq %rbp #Almacenar el valor actual del EBP en la pila movq %rsp, %rbp #El RBP debe apuntar al ultimo elemento de la pila para acceder a otros elementos en la misma, por esta razon se establece la localizacion de memoria del RSP al RBP, de este modo, ambos registeres se encontraran apuntando al mismo elemento de la Stack #La funcion Write movq $4, %rax movq $1, %rbx movq 16(%rbp), %rcx #Accedemos al valor de la variable HelloWorld almacenado en la pila movq 24(%rbp), %rdx #Accedemos al valor del tamaño total del String HelloWorld (5) int $0x80 #Invocamos la llamada del sistema. movq %rbp, %rsp #Restablecer el valor antiguo del ESP, con esto las variables locales se van a derreferenciar, ya que el ESP no apunta mas a dicha localizacion de memoria. popq %rbp #Restablecer el valor antiguo del EBP retq # Cambiar el EIP al inicio de la proxima instruccion _start: nop pushq $5 pushq $HelloWorld callq PrintFunction #Ajustar el puntero de Stack, se suman 8 bytes dado que se ha invocado antes de ejecutar la funcion 2 veces la funcion pushq, lo que representa 4 bytes por invocacion addq $8, %rsp ExitCall: movq $1, %rax movq $0, %rbx int $0x80 |
A continuación se intenta indicar como cambia la memoria en el flujo de ejecución del programa partiendo de un punto de ruptura en pushq $5
En este punto las localizaciones de memoria de los registers son las siguientes:
(gdb) print /x $rsp
$1 = 0x7fffffffe250
(gdb) x/4xw $rsp
0x7fffffffe250: 0x00000001 0x00000000 0xffffe509 0x00007fff
Al ejecutar la instrucción pushq:
(gdb) print /x $rsp
$2 = 0x7fffffffe248
(gdb) x/4xw $rsp
0x7fffffffe248: 0x00000005 0x00000000 0x00000001 0x00000000
Como puede verse se ha metido en la pila el valor de 5.
En la siguiente instrucción, se invoca la instrucción pushq $HelloWorld
(gdb) print /x $rsp
$3 = 0x7fffffffe240
(gdb) x/4xw $rsp
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
(gdb) print /x &HelloWorld
$4 = 0x6000f4
En la instrucción anterior se ha metido la variable HelloWorld en la Stack, como puede apreciarse, las posiciones de memoria entre un elemento y otro no son contiguas, en una plataforma de 64 bits, se establece entre cada elemento de la pila un espacio reservado anotado con: 0x00000000
En la siguiente instrucción continua con la invocación de la función por medio de la llamada a callq, una nota importante sobre este punto, es que una vez se invoca la instrucción callq, la Stack almacena en la cima, la próxima dirección de memoria para almacenar el retorno de dicha función
(gdb) disassemble _start
Dump of assembler code for function _start:
0x00000000004000d1 <+0>: nop
0x00000000004000d2 <+1>: pushq $0x5
0x00000000004000d4 <+3>: pushq $0x6000f4
=> 0x00000000004000d9 <+8>: callq 0x4000b0 <PrintFunction>
0x00000000004000de <+13>: add $0x10,%rsp
End of assembler dump.
Como puede apreciarse, la próxima invocación sera la correspondiente a la función add, es donde se encuentra el register RSP apuntando.
Al ejecutar esta instrucción, se entra en la función PrintFuntion, en este punto estos son los valores que se encuentran almacenados en la Stack:
(gdb) x/10xw $rsp
0x7fffffffe238: 0x004000de 0x00000000 0x006000f4 0x00000000
0x7fffffffe248: 0x00000005 0x00000000 0x00000001 0x00000000
0x7fffffffe258: 0xffffe509 0x00007fff
Como puede verse, en la Stack, el ultimo elemento adicionado ha sido: 0x004000de que corresponde con la posición de memoria correspondiente a la siguiente instrucción de la invocación a callq, esta posición de memoria corresponde a la dirección de memoria de retorno (RET).
Después de ejecutar la siguiente instrucción correspondiente al establecimiento del register RBP en la Stack examinamos la memoria y encontramos:
(gdb) print /x $rbp
$6 = 0x0
Despues de pushq %rbp:
(gdb) x/10xw $rsp
0x7fffffffe230: 0x00000000 0x00000000 0x004000de 0x00000000
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000
Se ha almacenado la posición de memoria del register RBP correspondiente a: 0x00000000
Después de ejecutar la instrucción movq %rsp, %rbp
(gdb) print /x $rbp
$8 = 0x7fffffffe230
(gdb) print /x $rsp
$9 = 0x7fffffffe230
Las siguientes instrucciones corresponden a la invocación de la llamada a sistema write.
Es de especial interés la instrucción movq 16(%rbp), %rcx, en esta instrucción, se carga la posición de memoria almacenada en la Stack correspondiente a la variable HelloWorld en el register RCX
Antes de ejecutar dicha instrucción, se examinan los valores del register RBP, que en realidad corresponden a los valores del RSP dado que anteriormente se ha invocado la instrucción movl con estos dos registers.
(gdb) x /10xw $rbp
0x7fffffffe230: 0x00000000 0x00000000 0x004000de 0x00000000
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000
(gdb) x /10xw $rsp
0x7fffffffe230: 0x00000000 0x00000000 0x004000de 0x00000000
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000
(gdb) print /x &HelloWorld
$11 = 0x6000f4
Como puede verse, la posición de memoria a la variable HelloWorld se encuentra almacenada en el RBP (equivalente a EBP) y evidentemente en la Stack también, por lo tanto es necesario acceder a dicha variable almacenada en la Stack y posteriormente establecerla al register RCX, y esto es justo lo que realiza la instrucción a punto de ejecutarse.
Después de ejecutar dicha instrucción, se examina el valor de memoria al que apunta el register RCX
(gdb) print /x $rcx
$12 = 0x6000f4
La siguiente instrucción, realiza la misma operación que la anterior, esta vez, se accede al valor almacenado correspondiente a la longitud de la cadena
(gdb) print /x $rdx
$14 = 0x5
El valor es 5, justo el que se almaceno al inicio de la ejecución del programa.
Posteriormente en la instrucción movq %rbp, %rsp se restablece el valor del RSP para que apunte al elemento de la pila correspondiente al RBP, de este modo, el valor almacenado (RET) va a ser des-asignado de la Stack, Finalmente se ejecuta la instrucción popq %rbp que elimina el valor del register RBP de la Stack
Si en este momento se examina la cima de la Stack los valores corresponde a:
Antes de la instrucción popq %rbp
(gdb) x /10xw $rsp
0x7fffffffe230: 0x00000000 0x00000000 0x004000de 0x00000000
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000
Después de la instrucción popq %rbp
(gdb) x /10xw $rsp
0x7fffffffe238: 0x004000de 0x00000000 0x006000f4 0x00000000
0x7fffffffe248: 0x00000005 0x00000000 0x00000001 0x00000000
0x7fffffffe258: 0xffffe509 0x00007fff
Después de que la instrucción ret es ejecutada y se finaliza el contexto de la función, la posición de memoria almacenada anteriormente correspondiente al RET, se elimina automáticamente, dado que el register RSP ya no apunta a dicha posición:
Antes del retorno de la función:
(gdb) x /10xw $rsp
0x7fffffffe238: 0x004000de 0x00000000 0x006000f4 0x00000000
0x7fffffffe248: 0x00000005 0x00000000 0x00000001 0x00000000
0x7fffffffe258: 0xffffe509 0x00007fff
Después del retorno de la función:
(gdb) x /10xw $rsp
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000 0xffffe509 0x00007fff
0x7fffffffe260: 0x00000000 0x00000000
En la instrucción addq $16, %rsp se actualiza el valor del RSP
Antes de la instrucción addq $16, %rsp
(gdb) x /10xw $rsp
0x7fffffffe240: 0x006000f4 0x00000000 0x00000005 0x00000000
0x7fffffffe250: 0x00000001 0x00000000 0xffffe509 0x00007fff
0x7fffffffe260: 0x00000000 0x00000000
Después de la instrucción addq $16, %rsp
(gdb) x /10xw $rsp
0x7fffffffe250: 0x00000001 0x00000000 0xffffe509 0x00007fff
0x7fffffffe260: 0x00000000 0x00000000 0xffffe52e 0x00007fff
0x7fffffffe270: 0xffffe551 0x00007fff
Como puede apreciarse, se han removido de la pila los valores correspondientes a los parámetros HelloWorld y el tamaño del String.
Con esto el programa finaliza correctamente y se podrá ver por pantalla el mensaje correspondiente a la invocación de la función write de GNU/Linux