Tras varios meses , tengo algo de estudio…
Pues, hoy veremos un tema fundamental en el ensamblador de 64 bits bajo el estándar de intel (x86_64).
La pila / The Stack
Cómo el famoso foro, esta esta siendo relegado por la I.A., StackOverflow. Que se llama así por el desbordamiento de pila.| 0x1000 | Comienzo del programa
| 0x1008 | Sigue tu programa
| 0x1010 |
| 0x1018 |
| 0x1020 |
|
^
|
| 0x2000 | La pila
| 0x2008 |
Pues, el esquema de arriba, de forma sintetizada, marca que tu programa comienza en determinada dirección de memoria, y crece (cuando programas más para él) hacia abajo.
En cambio, la pila está “más abajo” en la memoria, y crece en sentido opuesto, cuando la pila crece demasiado, puede chocar con el programa, ocasionando un crasheo o la famosa “Violación del Segmento”.
Funcionamiento
Tras ver, en el tutorial anterior, un ejemplo coloreando pixeles, pues, ahora toca un poquito de teoría.
En 64 bits hay los siguientes registros:
rax rcx rdx rbx rsp rbp rsi rdi
En 32 bits, o su parte baja de 4 bytes:
eax ecx edx ebx esp ebp esi edi
En 16 bits, o su parte baja de 2 bytes:
ax cx dx bx sp bp si di
En 8 bits, o su parte baja de 1 bytes:
al cl dl bl spl bpl sil dil
Todos accesibles desde 64 bits.
Pues, en 64 bits, la pila se maneja con los registros rsp y rbp:
; STACK
;
;0000 | | <- RBP <- RSP PUSH RBP MOV RBP, RSP
;0001 | | PUSH 0X00 RSP + 8 BYTES
;0002 | | |
;0003 | | |
;0004 | | |
;0005 | | |
;0006 | | |
;0007 | | |
;0008 | | <- RSP <---------
;
; RSP == RBP - 8
Cuando entras en una función haces:
push rbp
mov rbp, rsp
Para dar inició al frame, cuando sales:
leave
ret
Para finalizar el frame de la función/rutina, y volver al frame anterior (anterior función) de la pila.
En 64, la pila siempre tiene que estar alineada a 16 bits antes de hacer un call
, o saltará a cualquier parte.
Cuando empieza con la _start:
, ya esta alineada, pero tras push rbp
y mov rbp, rsp
. Hay que alinear la pila, con un push
o un sub rsp, 8
.
Así cada vez que se carguen variables locales en la pila.
Cuando empieza en main:
, empieza desalineada, y con el push rbp
, y mov rbp, rsp
se alinea la pila. Con la _start:
, al hacerlo, hay que agregarle un sub rsp, 8…
Si reservas con push, se cargan 8 bytes, con lo que serían dos push seguidos para mantener alineada la pila, o reservar con sub, y reservar con múltiplos de 16.
global _start
extern strlen
extern printf
; STACK
;
;0000 | | <- RBP <- RSP PUSH RBP MOV RBP, RSP
;0001 | | PUSH 0X00 RSP + 8 BYTES
;0002 | | |
;0003 | | |
;0004 | | |
;0005 | | |
;0006 | | |
;0007 | | |
;0008 | | <- RSP <---------
;
; RSP == RBP - 8
section .text
_start:
push rbp
mov rbp, rsp
push 0x0A
push "!"
push "o"
push "Mund"
push 0x20
push "Hola"
mov rax, 1
mov rdi, 1
lea rsi, -48[rbp]
mov rdx, 48
syscall
add rsp, 48
push 0x0A
push "Hola"
mov rax, 1
mov rdi, 1
lea rsi, -16[rbp]
mov rdx, 16
syscall
add rsp, 16
push msg
call print
add rsp, 8
sub rsp, 8
push 2
call print_num
add rsp, 16
leave
mov rax, 60
mov rdi, 7
syscall
print:
push rbp
mov rbp, rsp
lea rdi, [rbp + 16]
call strlen
mov rax, 1
mov rdi, 1
mov rsi, [rbp + 16]
mov rdx, len
syscall
leave
ret
print_num:
push rbp
mov rbp, rsp
sub rsp, 24
push "n:%d"
push 0x00
lea rdi, [rsp+16]
mov rsi, 16
call printf
add rsp, 40
leave
ret
section .data
msg db "Hola Mundo!",0x0A,0x00
len equ $-msg
Para linux x86_64, compila con:
yasm -f elf64 -g DWARF2 main.asm -o obj.o
ld obj.o -lX11 -lc -lm --dynamic-linker /usr/lib64/ld-linux-x86-64.so.2 -o a
Dónde poder ver y analizar lo aquí expuesto en el ejemplo.
Material de consulta y bibliografía:
https://cs.lmu.edu/~ray/notes/nasmtutorial/
Tras hacer esas dos instrucciones al comienzo de una función, preparas el frame de la pila, de forma que está lista para:
[rbp] → respaldo del rbp original (que se restaurará con leave)
[rbp + 8] → dirección de retorno
[rbp + 16] → Primer parámetro
[rbp + 26] → Segundo Parámetro
[rbp + 32] → Tercer Parámetro
Tras un push
[rsp] → Marca el último push hecho
[rsp+8] → Siguiente push
[rsp + 16] → Siguiente…
Ante cualquier duda, pregunten, intentaré responder.