Frame de Pilha e Variáveis Locais em Assembly NASM
Quando uma função é chamada, ela pode precisar armazenar dados temporários — como variáveis locais, parâmetros e endereços de retorno. Essas informações são organizadas em uma estrutura chamada frame de pilha (stack frame), que representa a área da pilha reservada exclusivamente para a função em execução.
Na arquitetura x86-64, o registrador RBP (base pointer) é usado como referência para acessar o frame da função atual. Esse registrador marca o ponto de base da pilha, a partir do qual os parâmetros, variáveis locais e endereços podem ser acessados por deslocamento.
Estrutura do Frame de Pilha
A criação do frame começa no código que realiza a chamada da função:
- A instrução
callempilha o endereço de retorno. - Dentro da função chamada, o restante do frame é configurado, incluindo a área para variáveis locais.
Embora o Assembly não tenha o conceito de variáveis locais como nas linguagens de alto nível, é possível reservar espaço na pilha para dados que existirão apenas durante a execução da função. Quando a função termina, essa área é liberada automaticamente.
Exemplo Linux:
global _start
section .text
_start:
mov rdi, 11
call sum ; após a chamada, RAX contém o resultado
mov rdi, rax
mov rax, 60
syscall
sum:
push 5 ; cria uma variável local (valor 5)
mov rax, rdi
add rax, [rsp] ; soma RDI + 5
add rsp, 8 ; libera o espaço da pilha
retAqui, o valor 5 é tratado como uma variável local criada dentro da função sum.
A instrução push empilha o valor, ocupando 8 bytes, e o registrador RSP passa a apontar para esse endereço.
O endereço de retorno fica em [rsp+8].
Essa variável local também pode ser alterada dentro da função:
sum:
push 5
add byte [rsp], 2 ; aumenta a variável local de 5 para 7
mov rax, rdi
add rax, [rsp]
add rsp, 8
retAqui usamos add byte [rsp], 2, pois o valor cabe em um único byte.
Armazenando Parâmetros como Variáveis Locais
É comum salvar parâmetros recebidos em registradores dentro da pilha, liberando os registradores para outros cálculos.
Exemplo em Linux:
global _start
section .text
_start:
mov rdi, 11
call sum
mov rdi, rax
mov rax, 60
syscall
sum:
sub rsp, 8 ; reserva 8 bytes (duas variáveis locais)
mov dword [rsp+4], 5 ; primeira variável = 5
mov dword [rsp], edi ; segunda variável = valor de EDI (parâmetro)
mov eax, [rsp+4]
add eax, [rsp]
add rsp, 8
retAs duas variáveis ocupam 8 bytes no total (4 bytes cada).
Elas são acessadas por meio dos deslocamentos [rsp] e [rsp+4].
Versão equivalente em Windows:
global _start
section .text
_start:
mov rcx, 11
call sum
ret
sum:
sub rsp, 8
mov dword [rsp+4], 5
mov dword [rsp], ecx
mov eax, [rsp+4]
add eax, [rsp]
add rsp, 8
retNomeando Variáveis Locais
Para tornar o código mais legível e evitar confusão com deslocamentos numéricos, é possível usar constantes simbólicas:
global _start
_a equ 4
_b equ 0
section .text
_start:
mov rdi, 12
call sum
mov rdi, rax
mov rax, 60
syscall
sum:
sub rsp, 8
mov dword [rsp+_a], 5
mov dword [rsp+_b], edi
mov eax, [rsp+_a]
add eax, [rsp+_b]
add rsp, 8
retAs constantes _a e _b representam os deslocamentos dentro da pilha, substituindo diretamente os valores 4 e 0.
Essa abordagem facilita a manutenção e reduz erros.
Uso do Registrador RBP
O registrador RBP atua como uma âncora para o frame da função. Ele mantém o endereço base da pilha, permitindo acessar facilmente parâmetros e variáveis locais por deslocamento.
A sequência típica para criar e destruir um frame é:
push rbp ; salva o RBP anterior
mov rbp, rsp ; define o novo frame base
sub rsp, N ; reserva N bytes para variáveis locaisE antes de retornar:
mov rsp, rbp
pop rbp
ret- Parâmetros enviados pela pilha são acessados com deslocamentos positivos (
[rbp+16],[rbp+24], etc.). - Variáveis locais usam deslocamentos negativos (
[rbp-8],[rbp-16], etc.).
Exemplo Linux:
global _start
section .text
_start:
mov rdi, 11
call sum
mov rdi, rax
mov rax, 60
syscall
sum:
push rbp
mov rbp, rsp
sub rsp, 16 ; duas variáveis locais (8 bytes cada)
mov qword [rbp-8], 7
mov qword [rbp-16], rdi
mov rax, [rbp-8]
add rax, [rbp-16]
mov rsp, rbp
pop rbp
retVisualização do frame:
[rsp] - segunda variável local [rbp-16] [rsp+8] - primeira variável local [rbp-8] [rbp] - valor anterior de RBP [rbp+8] - endereço de retorno
Exemplo com Constantes Nomeadas
global _start
_a equ -8
_b equ -16
section .text
_start:
mov rdi, 11
call sum
mov rdi, rax
mov rax, 60
syscall
sum:
push rbp
mov rbp, rsp
sub rsp, 16
mov qword [rbp+_a], 7
mov qword [rbp+_b], rdi
mov rax, [rbp+_a]
add rax, [rbp+_b]
mov rsp, rbp
pop rbp
retVersão Windows:
global _start
_a equ -8
_b equ -16
section .text
_start:
mov rcx, 11
call sum
ret
sum:
push rbp
mov rbp, rsp
sub rsp, 16
mov qword [rbp+_a], 7
mov qword [rbp+_b], rcx
mov rax, [rbp+_a]
add rax, [rbp+_b]
mov rsp, rbp
pop rbp
retInstruções enter e leave
Como a configuração do frame com rbp é muito comum, o NASM fornece as instruções enter e leave para simplificar o processo.
Substitui-se:
push rbp
mov rbp, rsp
sub rsp, Npor:
enter N, 0E, no final da função:
mov rsp, rbp
pop rbppor:
leaveExemplo com enter e leave:
global _start
_a equ -8
_b equ -16
section .text
_start:
mov rdi, 12
call sum
mov rdi, rax
mov rax, 60
syscall
sum:
enter 16, 0 ; cria o frame e reserva 16 bytes
mov qword [rbp+_a], 7
mov qword [rbp+_b], rdi
mov rax, [rbp+_a]
add rax, [rbp+_b]
leave ; restaura RSP e RBP
retA instrução enter é funcionalmente prática, mas costuma ser mais lenta que sua sequência equivalente.
Por isso, é raramente usada em código otimizado.
Já leave é mais eficiente e amplamente utilizada.
Resumo
- Cada função possui seu próprio frame de pilha, com variáveis locais, parâmetros e endereço de retorno.
- O registrador RBP atua como referência base do frame.
- Variáveis locais usam deslocamentos negativos, enquanto parâmetros usam positivos.
pushesub rspreservam espaço na pilha;add rspouleaveliberam-no.- As instruções
entereleavesimplificam o gerenciamento do frame, sendoleavea mais comum na prática.