Aprendendo sobre as convenções de chamada usadas no Windows (x64, cdecl e stdcall).
O Windows tem suas próprias convenções de chamadas e o objetivo desse tópico é aprender sobre as três principais que dizem respeito à linguagem C.
Essa é a convenção de chamada padrão usada em x86-64 e portanto é essencial aprendê-la caso vá programar no Windows diretamente em Assembly.
Os registradores RBX, RBP, RDI, RSI, RSP, R12 até R15 e XMM6 até XMM15 devem ser preservados pela função chamada (callee). Caso a função chamada precise alterar o valor de algum desses registradores ela tem a obrigação de preservar o valor anterior e restaurá-lo antes de retornar.
Os demais registradores são considerados voláteis, isto é, podem ter seu valor alterado quando uma chamada de função é efetuada. A função chamada pode modificar o valor dos registradores voláteis livremente.
Os primeiros quatro argumentos inteiros ou ponteiros são passados nos seguintes registradores e na mesma ordem: RCX, RDX, R8 e R9. Os demais argumentos devem ser empilhados na ordem inversa.
Os primeiros quatro argumentos float ou double são passados nos registradores XMM0 até XMM3 como valores escalares. Os demais também são empilhados na ordem inversa.
Structs e unions de 8, 16, 32 ou 64 bits são passados como se fossem inteiros do respectivo tamanho. Se forem de outro tamanho a função chamadora deve então passar um ponteiro para a struct/union que será armazenada em uma memória alocada pela própria função chamadora. Essa memória deve estar em um endereço alinhado por 16 bytes.
A função chamadora (caller) é responsável por alocar um espaço de 32 bytes na pilha chamado de shadow space. Ele é alocado com o intuito de ser usado pela função chamada (callee) para armazenar os parâmetros passados em registradores caso seja necessário, por exemplo caso a função chamada precise usar esses registradores com outro intuito. Esse espaço vem antes mesmo do primeiro parâmetro empilhado.
Exemplo de protótipo de função:
Assim que a função fosse chamada ECX, EDX, R8D e R9D armazenariam os parâmetros a
, b
, c
e d
respectivamente. O parâmetro f
seria empilhado seguido do parâmetro e
.
O 0(%rsp)
seria o endereço de retorno. O espaço entre 8(%rsp)
e 40(%rsp)
é o shadow space. 40(%rsp)
apontaria para o parâmetro e
, enquanto 48(%rsp)
apontaria para o parâmetro f
. Como na demonstração abaixo:
Valores inteiros e ponteiros são retornados em RAX.
Valores float ou double são retornados no registrador XMM0.
O retorno de structs é feito com a função chamadora alocando o espaço de memória necessário para a struct, ela então passa o ponteiro para esse espaço como primeiro argumento para a função em RCX. A função chamada (callee) deve retornar o mesmo ponteiro em RAX.
A convenção de chamada __cdecl
é a convenção padrão usada em código escrito em C na arquitetura IA-32 (x86).
Apenas os registradores EAX, ECX e EDX são considerados voláteis, ou seja, registradores que podem ser modificados livremente pela função chamada. Todos os demais registradores precisam ser preservados e restaurados antes do retorno da função.
Todos os parâmetros são passados na pilha e devem ser empilhados na ordem inversa. A função chamadora (caller) é a responsável por remover os argumentos da pilha após a função retornar.
Exemplo:
Valores inteiros ou ponteiros são retornados em EAX.
Valores float ou double são retornados em ST0.
O retorno de structs ocorre da mesma maneira que na convenção de chamada x64. Com a diferença que o primeiro argumento é, obviamente, passado na pilha.
A convenção de chamada __stdcall
é a utilizada para chamar funções da WinAPI.
Assim como na __cdecl
os registradores EAX, ECX e EDX são voláteis e os demais devem ser preservados pela função chamada.
Todos os argumentos são passados na pilha na ordem inversa. A função chamada (callee) é a responsável por remover os argumentos da pilha. Exemplo:
O retorno de valores funciona da mesma maneira que o retorno de valores da __cdecl
.