Parâmetros de Funções em Assembly NASM
Parâmetros permitem que uma função receba valores externos para processar. Em Assembly, esses valores geralmente são enviados por registradores, mas também podem ser passados por pilha ou por variáveis globais. É possível combinar esses métodos conforme a necessidade.
Quando o número de parâmetros é pequeno, o modo mais comum é enviá-los por registradores.
Passagem de Parâmetros por Registradores
A seguir, um exemplo simples em Linux:
global _start
section .text
_start:
mov rcx, 3 ; primeiro parâmetro para a função sum
mov rdx, 4 ; segundo parâmetro para a função sum
call sum
mov rax, 60
syscall
; definição da função sum
sum:
mov rdi, rcx ; copia o valor de RCX para RDI
add rdi, rdx ; soma com o valor de RDX
retOs valores 3 e 4 são enviados para a função sum pelos registradores rcx e rdx. Dentro da função, rdi recebe o valor de rcx e o soma com rdx.
Ao final, rdi conterá o resultado 7.
Exemplo equivalente em Windows:
global _start
section .text
_start:
mov rcx, 3 ; primeiro parâmetro para a função sum
mov rdx, 4 ; segundo parâmetro para a função sum
call sum
ret
; definição da função sum
sum:
mov rax, rcx ; copia o valor de RCX para RAX
add rax, rdx ; soma com o valor de RDX
retQuando todas as funções do programa são escritas em Assembly, é possível definir livremente quais registradores serão usados para enviar parâmetros. Entretanto, ao interagir com funções externas, como as de bibliotecas C/C++ ou APIs do sistema operacional, é necessário seguir as convenções de chamada de cada plataforma.
- Linux (System V AMD64): os seis primeiros parâmetros são enviados nos registradores
rdi,rsi,rdx,rcx,r8er9. - Windows (Win64): os quatro primeiros parâmetros são enviados nos registradores
rcx,rdx,r8er9. - Parâmetros adicionais são enviados pela pilha.
Essas convenções também se aplicam ao Assembly quando há integração com outras linguagens.
Passagem de Parâmetros pela Pilha
Outra forma de enviar parâmetros é por meio da pilha (stack).
Exemplo em Linux:
global _start
section .text
_start:
push 1 ; [rsp + 24] = 1
push 2 ; [rsp + 16] = 2
push 3 ; [rsp + 8] = 3
call sum ; [rsp] contém o endereço de retorno
add rsp, 24 ; restaura o ponteiro da pilha
mov rax, 60
syscall
; definição da função sum
sum:
mov rdi, [rsp + 24] ; RDI = 1
add rdi, [rsp + 16] ; RDI = 3
add rdi, [rsp + 8] ; RDI = 6
retCada instrução push insere um valor de 8 bytes na pilha.
No exemplo, três valores são empilhados (24 bytes no total). Após a chamada, add rsp, 24 limpa esse espaço.
Durante a execução da função sum, a pilha estará organizada da seguinte forma:
RSP → endereço de retorno [RSP + 8] = 3 [RSP + 16] = 2 [RSP + 24] = 1
Assim, para acessar cada parâmetro, são usados deslocamentos de 8, 16 e 24.
Exemplo equivalente em Windows:
global _start
section .text
_start:
push 1
push 2
push 3
call sum
add rsp, 24
ret
; definição da função sum
sum:
mov rax, [rsp + 24]
add rax, [rsp + 16]
add rax, [rsp + 8]
retInserindo Parâmetros Manualmente na Pilha
Usar várias instruções push pode não ser a forma mais eficiente, especialmente quando os valores são pequenos.
É possível escrever diretamente na pilha, reservando espaço manualmente.
Exemplo em Linux:
global _start
section .text
_start:
sub rsp, 3 ; reserva 3 bytes para parâmetros
mov byte [rsp + 2], 3
mov byte [rsp + 1], 4
mov byte [rsp], 5
call sum
add rsp, 3 ; restaura a pilha
mov rdi, rax ; copia o resultado para RDI
mov rax, 60
syscall
; definição da função sum
sum:
xor rax, rax
mov al, [rsp + 10]
add al, [rsp + 9]
add al, [rsp + 8]
retNesse caso, apenas 3 bytes são reservados, pois os parâmetros são de 1 byte cada.
O endereço de retorno ainda ocupa 8 bytes, portanto o primeiro parâmetro fica em [rsp + 8].
Exemplo em Windows:
global _start
section .text
_start:
sub rsp, 3
mov byte [rsp + 2], 3
mov byte [rsp + 1], 4
mov byte [rsp], 5
call sum
add rsp, 3
ret
; definição da função sum
sum:
xor rax, rax
mov al, [rsp + 10]
add al, [rsp + 9]
add al, [rsp + 8]
retConsiderações sobre Alinhamento da Pilha
Em alguns contextos — especialmente ao interagir com APIs externas ou funções C/C++ — há restrições quanto ao alinhamento da pilha. Certas instruções exigem que os dados estejam alinhados em endereços pares ou múltiplos de 16 bytes.
Isso significa que empilhar valores de 1 byte pode causar desalinhamento e comportamento incorreto. Quando necessário, deve-se ajustar a pilha para garantir o alinhamento adequado, mesmo que isso gere algum espaço não utilizado.
Resumo
- Parâmetros podem ser enviados por registradores, pilha ou variáveis globais.
- O método mais simples é o envio por registradores, quando há poucos parâmetros.
- Em chamadas que seguem convenções de linguagens como C/C++, a ordem e os registradores usados dependem do sistema operacional.
- O envio por pilha é flexível, mas requer cuidado com o uso de espaço e o alinhamento da memória.
- É possível escrever diretamente na pilha, sem usar
push, para otimizar o uso de memória.