[ASM] Curso de Ensamblador / Assembly (con juegos) #nº1 La Pila / The Stack

Tras varios meses :sweat_smile:, 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 rbpy 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, rspse 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.

2 Me gusta