Chamada de Funções C/C++ no Windows em Assembly NASM
Ao chamar funções escritas em C ou C++ a partir de código Assembly no Windows, é necessário seguir as convenções definidas pelo Microsoft Windows ABI (Application Binary Interface). Esse conjunto de regras estabelece como os parâmetros são enviados para as funções e como os valores de retorno são tratados. Essas convenções diferem significativamente do padrão System V ABI utilizado no Linux.
Convenções do Microsoft Windows ABI
A função que faz a chamada envia os quatro primeiros parâmetros por registradores. Os parâmetros inteiros são passados nos registradores RCX, RDX, R8 e R9, nessa ordem. Quando os parâmetros são valores de ponto flutuante, eles são enviados pelos registradores XMM0, XMM1, XMM2 e XMM3, sendo também espelhados nos registradores correspondentes RCX, RDX, R8 e R9. Por exemplo, se o segundo parâmetro for um número de ponto flutuante, ele é colocado tanto em XMM1 quanto em RDX.
Os parâmetros a partir do quinto são passados pela pilha. Assim, o quinto parâmetro ocupa o endereço [RSP + 32], o sexto [RSP + 40] e assim por diante.
Quando a função possui parâmetros mistos — inteiros e de ponto flutuante —, cada um é colocado no registrador correspondente à sua posição. Por exemplo, considere a função em C/C++:
void someFunc(int a, double b, char *c, double d)A disposição dos parâmetros será:
| Parâmetro | Registrador(es) utilizados |
|---|---|
| a (int) | RCX |
| b (double) | XMM1 e RDX |
| c (char *) | R8 |
| d (double) | XMM3 e R9 |
Todos os parâmetros são tratados como valores de 8 bytes.
Shadow Storage e Alinhamento da Pilha
A função que faz a chamada deve reservar 32 bytes na pilha para os parâmetros — mesmo que a função receba menos de quatro argumentos. Esse espaço é chamado de shadow storage (ou shadow space). Ele serve como área reservada que a função chamada pode usar para armazenar temporariamente os parâmetros, ou mesmo para variáveis locais.
De forma simplificada, pode-se imaginar que:
- o primeiro parâmetro ocupa
[RSP], - o segundo
[RSP + 8], - o terceiro
[RSP + 16], - o quarto
[RSP + 24].
Antes de qualquer chamada, o registrador RSP (ponteiro de pilha) deve estar alinhado a 16 bytes — o que garante a conformidade com o ABI e o correto funcionamento de funções que dependem desse alinhamento.
Registros Voláteis e Não Voláteis
O Microsoft Windows ABI divide os registradores em dois grupos principais:
- Voláteis (volatile): podem ser modificados livremente pelas funções chamadas. A função que faz a chamada não pode assumir que seus valores serão preservados. Incluem: RAX, RCX, RDX, R8, R9, R10, R11, e XMM0–XMM5 / YMM0–YMM5.
- Não voláteis (nonvolatile): devem ser preservados entre chamadas. A função chamada deve salvar e restaurar esses registradores caso precise modificá-los. Incluem: RBX, RBP, RDI, RSI, RSP, R12–R15, e XMM6–XMM15 / YMM6–YMM16 (apenas a metade superior dos YMM6–YMM16 pode ser alterada).
O valor de retorno é colocado em RAX para números inteiros e em XMM0 para valores de ponto flutuante.
Ponto de Entrada
Assim como no Linux, o ponto de entrada do programa Assembly que usa funções C/C++ é a função main, e não o rótulo _start:
global main ; função main - ponto de entrada
section .text
main:
; instruções da função main
retA execução e finalização do programa ocorrem por meio da instrução ret.
O código associado a _start é fornecido por bibliotecas do sistema, responsáveis por inicializar o ambiente de execução da linguagem C e chamar a função main.
Após a conclusão, main retorna o controle ao código da biblioteca.
Exemplo: Chamada da Função printf no Windows
O exemplo a seguir mostra como chamar printf em um programa NASM no Windows:
global main ; função main - ponto de entrada
extern printf ; referência externa para printf
section .data
message db "Hello www.programicio.com", 10 ; string a ser exibida
section .text
main:
sub rsp, 40 ; alinhamento de 16 bytes + 32 bytes de shadow space
lea rcx, [rel message] ; primeiro parâmetro de printf - endereço da string
call printf
add rsp, 40 ; restaura o valor original da pilha
retDiferentemente do código no Linux, aqui o primeiro parâmetro é enviado em RCX, e não em RDI.
A instrução lea com endereçamento relativo (rel) é usada para carregar o endereço da variável no registrador.
Antes da chamada de função, o código ajusta a pilha em 40 bytes — 32 bytes para o shadow storage e 8 bytes adicionais para o alinhamento de 16 bytes.
Como o ponteiro da pilha (RSP) já está alinhado a 8 bytes no início da execução, o total de 40 bytes garante o alinhamento correto.
Compilação e Linkagem
Criação do arquivo objeto:
nasm -f win64 hello.asm -o hello.o
Se o GCC for utilizado como vinculador:
gcc hello.o -o hello.exe
Se for usado o link.exe do Microsoft Visual C++, é necessário incluir bibliotecas adicionais:
link hello.o legacy_stdio_definitions.lib msvcrt.lib /subsystem:console /out:hello2.exe
Essas bibliotecas fornecem as definições de entrada/saída e a implementação da printf.
Outros vinculadores podem exigir parâmetros diferentes conforme a ferramenta utilizada.
Envio de Parâmetros para Funções C/C++
Exemplo com múltiplos parâmetros sendo passados para printf:
global main
extern printf
section .data
message db "Name: %s Age: %u Company: %s Salary: %u", 10, 0
name db "Tom", 0
age dq 39
company db "www.programicio.com", 0
salary dq 1150
section .text
main:
sub rsp, 40
lea rcx, [rel message] ; string de formatação
lea rdx, [rel name] ; primeiro argumento
mov r8, [rel age] ; segundo argumento
lea r9, [rel company] ; terceiro argumento
mov r10, [rel salary]
mov qword [rsp+32], r10 ; quarto argumento (via pilha)
call printf
add rsp, 40
retNesse exemplo, printf recebe cinco parâmetros.
Os quatro primeiros são passados nos registradores RCX, RDX, R8 e R9, enquanto o quinto argumento é colocado na pilha, no endereço [RSP + 32].
Resumo
- O Microsoft Windows ABI define convenções próprias, diferentes do System V ABI do Linux.
- Os quatro primeiros parâmetros são enviados por registradores (RCX, RDX, R8, R9).
- O shadow space reserva 32 bytes na pilha, mesmo que a função receba menos parâmetros.
- O ponteiro de pilha (RSP) deve estar alinhado a 16 bytes antes da chamada.
- RAX é usado para retornos inteiros, XMM0 para valores de ponto flutuante.
- Registradores voláteis podem ser alterados livremente; não voláteis devem ser salvos e restaurados.
- Compilação pode ser feita com GCC ou link.exe, exigindo, neste último caso, as bibliotecas
legacy_stdio_definitions.libemsvcrt.lib.