Using the Stack
Hemos aprendido hasta ahora dos tipos de Instrucciones de Control: Loops
y Branching
. Antes de discutir sobre Functions
, necesitamos entender cómo usar la memoria Stack
. En la sección de Computer Architecture
, discutimos cómo la RAM está segmentada en cuatro segmentos diferentes, y cómo a cada aplicación se le asigna su propia Memoria Virtual y sus segmentos. También discutimos el segmento text
, donde las instrucciones en ensamblador de la aplicación se cargan para que la CPU pueda acceder a ellas, y el segmento data
que contiene las variables de la aplicación. Ahora comencemos a discutir el Stack
.
The Stack
El stack es un segmento de memoria asignado para que el programa almacene datos y generalmente se utiliza para almacenar y recuperar datos temporalmente. La parte superior del stack es referida por el Top Stack Pointer rsp
, mientras que la parte inferior es referida por el Base Stack Pointer rbp
.
Podemos push
datos en el stack, colocándolos en la parte superior del stack (es decir, rsp
), y luego podemos pop
datos fuera del stack hacia un registro o una dirección de memoria, eliminándolos de la parte superior del stack.
Instruction | Description | Example |
---|---|---|
push |
Copia el registro/dirección especificada en la parte superior del stack | push rax |
pop |
Mueve el elemento en la parte superior del stack al registro/dirección especificada | pop rax |
El stack tiene un diseño de Last-in First-out
(LIFO
), lo que significa que solo podemos pop
el último elemento que fue push
ed en el stack. Por ejemplo, si hacemos push rax
en el stack, la parte superior del stack contendrá el valor de rax
que acabamos de empujar. Si hacemos push
de algo más encima, tendremos que hacer pop
de esos elementos fuera del stack hasta que el valor de rax
llegue a la parte superior del stack, y entonces podremos hacer pop
de ese valor de vuelta a rax
.
Usage With Functions/Syscalls
Principalmente estaremos empujando datos desde registros hacia el stack antes de llamar a una function
o hacer una syscall
, y luego restaurarlos después de la función o syscall. Esto se debe a que las functions
y syscalls
generalmente utilizan los registros para su procesamiento, por lo que si los valores almacenados en los registros cambian después de una llamada a función o syscall, los perderemos.
Por ejemplo, si quisiéramos llamar a una syscall para imprimir Hello World
en la pantalla y retener el valor actual almacenado en rax
, haríamos push rax
en el stack. Luego podríamos ejecutar la syscall y después hacer pop
del valor de vuelta a rax
. De esta manera, seríamos capaces de ejecutar la syscall y conservar el valor de rax
.
PUSH/POP
Nuestro código actualmente se ve de la siguiente manera:
global _start
section .text
_start:
xor rax, rax ; initialize rax to 0
xor rbx, rbx ; initialize rbx to 0
inc rbx ; increment rbx to 1
loopFib:
add rax, rbx ; get the next number
xchg rax, rbx ; swap values
cmp rbx, 10 ; do rbx - 10
js loopFib ; jump if result is <0
Supongamos que queremos llamar a una function
o una syscall
antes de entrar en el loop. Para preservar nuestros registros, necesitaremos hacer push
en el stack de todos los registros que estamos usando y luego hacer pop
de ellos después de la syscall
.
Para hacer push
de un valor en el stack, podemos usar su nombre como operando, como en push rax
, y el valor será copied
a la parte superior del stack. Cuando queramos recuperar ese valor, primero necesitamos asegurarnos de que esté en la parte superior del stack, y luego podemos especificar la ubicación de almacenamiento como el operando, como en pop rax
, después de lo cual el valor será moved
a rax
y será removed
de la parte superior del stack. El valor debajo de él ahora estará en la parte superior del stack (como se muestra en el ejercicio anterior).
Dado que el stack tiene un diseño LIFO, cuando restauramos nuestros registros, tenemos que hacerlo en orden inverso. Por ejemplo, si hacemos push rax y luego push rbx, al restaurar, tenemos que hacer pop rbx y luego pop rax.
Así que, para guardar nuestros registros antes de entrar en el loop, hagamos push
de ellos al stack. Afortunadamente, solo estamos usando rax
y rbx
, por lo que solo necesitaremos hacer push
de estos dos registros al stack y luego hacer pop
de ellos después de la syscall, como sigue:
global _start
section .text
_start:
xor rax, rax ; initialize rax to 0
xor rbx, rbx ; initialize rbx to 0
inc rbx ; increment rbx to 1
push rax ; push registers to stack
push rbx
; call function
pop rbx ; restore registers from stack
pop rax
...SNIP...
Nota cómo restaurar los registros con pop
fue en orden inverso.
Ahora, ensamblamos nuestro código y lo probamos con gdb
:
$ ./assembler.sh fib.s -g
gef➤ b _start
gef➤ r
...SNIP...
gef➤ si
gef➤ si
gef➤ si
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x1
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe410│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffe418│+0x0008: 0x0000000000000000
0x00007fffffffe420│+0x0010: 0x0000000000000000
0x00007fffffffe428│+0x0018: 0x0000000000000000
0x00007fffffffe430│+0x0020: 0x0000000000000000
0x00007fffffffe438│+0x0028: 0x0000000000000000
0x00007fffffffe440│+0x0030: 0x0000000000000000
0x00007fffffffe448│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
→ 0x40100e <_start+9> push rax
0x40100f <_start+10> push rbx
0x401010 <_start+11> pop rbx
0x401011 <_start+12> pop rax
────────────────────────────────────────────────────────────────────────────────────────────────────
Vemos que antes de ejecutar push rax
, tenemos rax = 0x0
y rbx = 0x1
. Ahora hagamos push
de ambos rax
y rbx
y veamos cómo cambian el stack y los registros:
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x1
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe408│+0x0000: 0x0000000000000000 ← $rsp
0x00007fffffffe410│+0x0008: 0x0000000000000001
0x00007fffffffe418│+0x0010: 0x0000000000000000
0x00007fffffffe420│+0x0018: 0x0000000000000000
0x00007fffffffe428│+0x0020: 0x0000000000000000
0x00007fffffffe430│+0x0028: 0x0000000000000000
0x00007fffffffe438│+0x0030: 0x0000000000000000
0x00007fffffffe440│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x40100e <loopFib+9> push rax
→ 0x40100f <_start+10> push rbx
0x401010 <_start+11> pop rbx
0x401011 <_start+12> pop rax
────────────────────────────────────────────────────────────────────────────────────────────────────
...SNIP...
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x1
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe400│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffe408│+0x0008: 0x0000000000000000
0x00007fffffffe410│+0x0010: 0x0000000000000001
0x00007fffffffe418│+0x0018: 0x0000000000000000
0x00007fffffffe420│+0x0020: 0x0000000000000000
0x00007fffffffe428│+0x0028: 0x0000000000000000
0x00007fffffffe430│+0x0030: 0x0000000000000000
0x00007fffffffe438│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x40100e <_start+9> push rax
0x40100f <_start+10> push rbx
→ 0x401010 <_start+11> pop rbx
0x401011 <_start+12> pop rax
────────────────────────────────────────────────────────────────────────────────────────────────────
Vemos que después de hacer push
de ambos rax
y rbx
, tenemos los siguientes valores en la parte superior de nuestro stack:
0x00007fffffffe408│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffe410│+0x0008: 0x0000000000000000
Observamos que en la parte superior del stack, tenemos el último valor que empujamos, que es rbx = 0x1
, y justo debajo de él, tenemos el valor que empujamos antes, rax = 0x0
. Esto es como esperábamos y similar al ejercicio de stack anterior. También notamos que después de empujar nuestros valores, permanecieron en los registros, lo que significa que un push
es, de hecho, una copia al stack.
Ahora supongamos que terminamos de ejecutar una print
function y queremos recuperar nuestros valores, así que continuamos con las instrucciones pop
:
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x1
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe408│+0x0000: 0x0000000000000000 ← $rsp
0x00007fffffffe410│+0x0008: 0x0000000000000001
0x00007fffffffe418│+0x0010: 0x0000000000000000
0x00007fffffffe420│+0x0018: 0x0000000000000000
0x00007fffffffe428│+0x0020: 0x0000000000000000
0x00007fffffffe430│+0x0028: 0x0000000000000000
0x00007fffffffe438│+0x0030: 0x0000000000000000
0x00007fffffffe440│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x40100e <_start+9> push rax
0x40100f <_start+10> push rbx
0x401010 <_start+11> pop rbx
→ 0x401011 <_start+12> pop rax
────────────────────────────────────────────────────────────────────────────────────────────────────
...SNIP...
───────────────────────────────────────────────────────────────────────────────────── registers ────
$rax : 0x0
$rbx : 0x1
───────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffe410│+0x0000: 0x0000000000000001 ← $rsp
0x00007fffffffe418│+0x0008: 0x0000000000000000
0x00007fffffffe420│+0x0010: 0x0000000000000000
0x00007fffffffe428│+0x0018: 0x0000000000000000
0x00007fffffffe430│+0x0020: 0x0000000000000000
0x00007fffffffe438│+0x0028: 0x0000000000000000
0x00007fffffffe440│+0x0030: 0x0000000000000000
0x00007fffffffe448│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x40100f <_start+9> push rax
0x40100f <_start+10> push rbx
0x401010 <_start+11> pop rbx
0x401011 <_start+12> pop rax
→ 0x401011 <loopFib+0> add rax, rbx
────────────────────────────────────────────────────────────────────────────────────────────────────
Vemos que después de hacer pop
de dos valores desde la parte superior del stack, fueron eliminados del stack y el stack ahora luce exactamente como cuando comenzamos. Ambos valores fueron colocados de nuevo en rbx
y rax
. Es posible que no hayamos notado ninguna diferencia, ya que no fueron cambiados en los registros en este caso.
Usar el stack es muy sencillo. Lo único que debemos tener en cuenta es el orden en que hacemos push
de nuestros registros y el estado del stack para restaurar nuestros datos de manera segura y no restaurar un valor diferente al hacer pop
cuando un valor diferente está en la parte superior del stack.
Podemos eliminar las instrucciones push
y pop
de nuestro código por ahora, y las usaremos cuando entremos en llamadas a funciones. Con eso, deberíamos estar listos para usar llamadas syscall
y function
. Hablemos de syscalls
a continuación.