Pilha (Stack) em Assembly NASM
A pilha é uma estrutura de dados dinâmica usada para armazenar informações temporárias do programa — como variáveis locais, endereços de retorno e dados intermediários. O processador x86-64 controla a pilha por meio do registrador RSP (Stack Pointer), que sempre aponta para o topo da pilha.
Quando o programa é iniciado, o sistema operacional inicializa o registrador RSP com o endereço da última célula de memória reservada para o segmento de pilha. No Linux x86-64, por exemplo, o tamanho padrão da pilha é de 2 MB.
A pilha cresce dos endereços mais altos para os mais baixos, ou seja, a cada novo dado adicionado, o valor de RSP é reduzido. Por padrão, o endereço da pilha é alinhado em 16 bytes no início da execução.
Instruções PUSH e POP
A instrução push insere um valor no topo da pilha.
Ela pode receber como operando um registrador de 16 ou 64 bits, um endereço de memória, ou uma constante de 16 a 32 bits (neste caso, a constante de 32 bits é expandida para 64 bits).
Ao executar push, o processador subtrai o tamanho do operando do registrador RSP e armazena o valor no novo endereço apontado:
rsp = rsp - tamanho_do_operando [rsp] = valor
A instrução pop faz o inverso: recupera o valor do topo da pilha e o armazena no operando informado (geralmente um registrador).
Em seguida, o valor de RSP é incrementado pelo tamanho do operando:
operando = [rsp] rsp = rsp + tamanho_do_operando
Exemplo (Linux)
global _start
section .text
_start:
mov rdx, 15
push rdx ; adiciona o valor de RDX à pilha
pop rdi ; recupera o valor no topo da pilha para RDI
mov rax, 60
syscallSe o registrador RSP inicialmente contém o endereço 0x00FFFFF0, após o comando push rdx:
RSPé decrementado em 8 bytes (0x00FFFFE8);- o valor
15é armazenado no endereço apontado porRSP.
O comando pop rdi então:
- lê o valor armazenado (
15); - move-o para
RDI; - e incrementa novamente
RSPpara0x00FFFFF0.
Exemplo (Windows)
global _start
section .text
_start:
mov rdx, 15
push rdx
pop rax
retPrincípio LIFO e Preservação de Registradores
A pilha segue o princípio LIFO (Last In, First Out — último a entrar, primeiro a sair). Isso significa que o último valor adicionado será o primeiro a ser removido.
Uma das utilizações mais comuns de push e pop é salvar o conteúdo dos registradores durante cálculos intermediários, permitindo reutilizá-los sem perder valores anteriores.
Exemplo (Linux)
global _start
section .text
_start:
mov rdi, 11
mov rdx, 33
push rdi
push rdx
pop rdi ; rdi = 33
pop rdx ; rdx = 11
mov rax, 60
syscallOs valores são recuperados em ordem inversa à que foram empilhados — o último push é o primeiro a ser pop.
Sempre que dados forem adicionados à pilha, deve haver a mesma quantidade de remoções posteriores para manter o equilíbrio do RSP.
Salvando e Restaurando os Flags
As instruções pushfq e popfq servem para armazenar e restaurar o registrador RFLAGS, que contém os flags de estado do processador.
Exemplo (Linux)
global _start
section .text
_start:
pushfq ; salva os flags atuais
mov al, 255
add al, 2 ; 255 + 2 = 257 → ativa o flag CF (Carry Flag)
popfq ; restaura o estado anterior dos flags
jc set ; o salto não ocorrerá, pois CF foi restaurado para 0
mov rdi, 11
jmp exit
set:
mov rdi, 22
exit:
mov rax, 60
syscallO uso de pushfq e popfq é essencial quando operações aritméticas alteram os flags e o programa precisa preservá-los para instruções subsequentes.
Restaurando o Ponteiro da Pilha sem POP
Nem sempre é necessário remover explicitamente os dados da pilha. Se for preciso apenas restaurar o valor original de RSP, pode-se somar o tamanho dos dados empilhados diretamente:
global _start
section .text
_start:
mov rdi, 11
mov rdx, 33
push rdi
push rdx
add rsp, 16 ; restaura RSP (2 × 8 bytes)
mov rax, 60
syscallDa mesma forma, pode-se reservar espaço na pilha subtraindo um valor de RSP:
global _start
section .text
_start:
sub rsp, 16 ; reserva 16 bytes
; uso temporário da pilha
add rsp, 16 ; restaura o ponteiro
mov rax, 60
syscallAcesso Direto à Pilha
O registrador RSP também pode ser usado para acessar diretamente os dados na pilha, sem usar push ou pop.
Isso é feito por meio de endereçamento indireto:
global _start
section .text
_start:
sub rsp, 16
mov rdx, 11
mov [rsp], rdx ; escreve 11 no endereço atual da pilha
mov rdi, [rsp] ; lê o mesmo valor (11)
add rsp, 16
mov rax, 60
syscallEsse método é útil quando não se deseja alterar o ponteiro da pilha.
Exemplo com Deslocamento
global _start
section .text
_start:
push 12
push 13
push 14
push 15
mov rdi, [rsp+16] ; acessa o valor 13
add rsp, 32
mov rax, 60
syscallComo cada push ocupa 8 bytes, rsp+16 aponta para o terceiro valor empilhado.
Exemplo com Soma de Valores (Linux)
global _start
section .text
_start:
sub rsp, 16
mov rcx, 12
mov rdx, 13
mov [rsp + 8], rcx ; [rsp+8] = 12
mov [rsp], rdx ; [rsp] = 13
mov rdi, [rsp]
add rdi, [rsp+8] ; 13 + 12 = 25
add rsp, 16
mov rax, 60
syscallExemplo equivalente (Windows)
global _start
section .text
_start:
sub rsp, 16
mov rcx, 12
mov rdx, 13
mov [rsp + 8], rcx
mov [rsp], rdx
mov rax, [rsp]
add rax, [rsp+8]
add rsp, 16
retResumo
- A pilha armazena dados temporários e é controlada pelo registrador RSP.
- A pilha cresce em direção a endereços menores.
pushinsere um valor;popo remove.- O acesso segue o princípio LIFO (Last In, First Out).
pushfqepopfqsalvam e restauram os flags de estado.- É possível ajustar diretamente o ponteiro da pilha com
add rsp, Nousub rsp, N. - O endereçamento indireto com [rsp + deslocamento] permite ler ou gravar valores específicos sem alterar RSP.