description |
---|
Aprendendo sobre a convenção de chamada do C usada no Linux. |
Sistemas UNIX-Like, incluindo o Linux, seguem a padronização da System V ABI (ou SysV ABI). Onde ABI é sigla para Application Binary Interface (Interface binária de aplicação) que é basicamente uma padronização que dita como código binário deve ser escrito e executado no sistema operacional. Uma das coisas que a SysV ABI padroniza é a convenção de chamada utilizada em cada arquitetura de processador.
Neste tópico vamos aprender sobre a convenção de chamada da SysV ABI e o tamanho dos tipos de dados usados na linguagem C.
- Os registradores RBP, RBX, RSP e R12 até R15 são considerados como pertencentes a função chamadora. Isto é, se a função que foi chamada precisar modificar esses registradores ela obrigatoriamente precisa preservar seus valores e antes de retornar restaurá-los para o valor anterior. Todos os outros registradores podem ser modificados livremente pela função chamada. Portanto não espere que esses outros registradores tenham seu valor preservado ao chamar uma função.
- A Direction Flag (DF) no RFLAGS precisa obrigatoriamente estar zerada ao chamar ou retornar de uma função.
Cada função chamada pode (se precisar) reservar um pedaço da pilha para ser usada como memória local da função e pode, por exemplo, ser usada para alocar variáveis locais. Esse espaço é chamado de stack frame e o código que aloca e desaloca o stack frame é chamado de prólogo e epílogo respectivamente. Exemplo:
{% code title="assembly.s" %}
.text
.globl assembly
assembly:
sub $8, %rsp
movl $12344, (%rsp) # var_0
movl $1, 4(%rsp) # var_4
mov (%rsp), %eax
add 4(%rsp), %eax
add $8, %rsp
ret
{% endcode %}
O espaço de 128 bytes antes do endereço apontado por RSP é uma região chamada de redzone que por convenção pode ser usada por funções folha (leaf), que são funções que não chamam outras funções. Ou então pode ser usada em qualquer função onde o valor não precise ser preservado após chamar outra função.
O endereço entre -128(%rsp)
e -1(%rsp)
pode ser usado livremente sem a necessidade de alocar um stack frame.
{% hint style="info" %}
Vale lembrar que CALL empilha o endereço de retorno, portanto ao chamar uma função 0(%rsp)
aponta para o endereço de retorno da mesma.
{% endhint %}
Os parâmetros inteiros (e ponteiros) são passados em registradores de propósito geral na seguinte ordem: RDI, RSI, RDX, RCX, R8 e R9. Parâmetros float ou double são passados nos registradores XMM0 até XMM7 como valores escalares (na parte menos significativa do registrador).
Caso a função precise de mais argumentos e os registradores acabem, os demais argumentos serão empilhados na ordem inversa. Por exemplo caso uma função precise de 9 argumentos inteiros eles seriam definidos na seguinte ordem pela função chamadora:
mov $1, %rdi
mov $2, %rsi
mov $3, %rdx
mov $4, %rcx
mov $5, %r8
mov $6, %r9
push $9
push $8
push $7
call my_function
add $24, %esp
Assim que a função fosse chamada 8(%rsp)
, 16(%rsp)
e 24(%rsp)
apontariam para os argumentos 7, 8 e 9 respectivamente.
{% hint style="warning" %} A função chamadora (caller) precisa garantir que o último valor empilhado esteja em um endereço alinhado por 16 bytes.
A função chamadora é a responsável por remover os argumentos empilhados da pilha. {% endhint %}
- No caso do retorno de estruturas (structs) a função chamadora precisa alocar o espaço necessário para a struct e passar o endereço do espaço no registrador RDI como se fosse o primeiro argumento para a função (os outros argumentos usam RSI em diante). A função então precisa retornar o mesmo endereço passado por RDI em RAX.
- O retorno de valores inteiros e ponteiros é feito no registrador RAX.
- Valores float ou double são retornados no registrador XMM0 na parte menos significativa.
- Os registradores EBX, EBP, ESI, EDI e ESP precisam ter seus valores preservados pela função chamada. Os demais registradores de propósito geral podem ser usados livremente.
- A Direction Flag (DF) no EFLAGS precisa obrigatoriamente estar zerada ao chamar ou retornar de uma função.
O stack frame em IA-32 funciona da mesma maneira que o stack frame em x86-64, com a diferença de que não existe redzone em IA-32 e toda função que precisar de memória local precisa obrigatoriamente construir um stack frame.
Vale lembrar que cada valor inserido na stack em IA-32 tem 4 bytes de tamanho, enquanto em x86-64 cada valor tem 8 bytes de tamanho.
Os argumentos da função são empilhados na ordem inversa, assim como ocorre em x86-64 quando os registradores acabam. Conforme exemplo:
push $4
push $3
push $2
push $1
call my_function
add $16, %esp
Assim que a função é chamada 4(%esp)
, 8(%esp)
, 12(%esp)
e 16(%esp)
apontam para os argumentos 1, 2, 3 e 4 respectivamente.
{% hint style="warning" %} A função chamadora precisa garantir que o último valor empilhado esteja em um endereço alinhado por 16 bytes.
A função chamadora é a responsável por remover os argumentos empilhados da pilha. {% endhint %}
- Retorno de struct é feito de maneira semelhante do x86-64. Um ponteiro para a região de memória para gravar os dados da struct é passado como primeiro argumento para a função (o último valor a ser empilhado). É obrigação da função chamada fazer o pop desse ponteiro e retorná-lo em EAX.
- Valores inteiros e ponteiros são retornados em EAX.
- Valores float ou double são retornados em ST0 (ver Usando instruções da FPU).
Existe uma convenção de escrita do prólogo e do epílogo da função que se trata de preservar o antigo valor de ESP/RSP no registrador EBP/RBP, e depois subtrair ESP/RSP para alocar o stack frame. Conforme exemplo:
example:
push %rbp
mov %rsp, %rbp
sub $16, %rsp
# etc...
mov %rbp, %rsp
pop %rbp
ret
Também existe a instrução leave
que pode ser usada no epílogo. Ela basicamente faz a operação de mov %rbp, %rsp
e pop %rbp
em uma única instrução (também pode ser usada em 32 e 16 bits atuando com EBP/ESP e BP/SP respectivamente).
Mas como já foi demonstrado em um exemplo mais acima isso não é obrigatório e podemos apenas incrementar e subtrair ESP/RSP no prólogo e no epílogo. Código otimizado gerado pelo GCC costuma apenas fazer isso, já código com a otimização desligada costuma gerar o prólogo e epílogo "clássico".
A tabela abaixo lista os principais tipos da linguagem C e seu tamanho em bytes no IA-32 e x86-64. Como também exibe em qual registrador o tipo deve ser retornado.
Tipo | Tamanho |
Tamanho |
Registrador de retorno |
Registrador de retorno |
---|---|---|---|---|
_Bool char signed char unsigned char |
1 | 1 | AL | AL |
short signed short unsigned short |
2 | 2 | AX | AX |
int signed int unsigned int long signed long unsigned long enum |
4 | 4 | EAX | EAX |
long long signed long long unsigned long long |
8 | 8 | *EDX:EAX | RAX |
Ponteiros | 4 | 8 | EAX | RAX |
float | 4 | 4 | ST0 | XMM0 |
double | 8 | 8 | ST0 | XMM0 |
**long double | 12 | 16 | ST0 | ST0 |
*No registrador EDX é armazenado os 32 bits mais significativos e em EAX os 32 bits menos significativos.
**O tipo long double
ocupa na memória o espaço de 12 e 16 bytes por motivos de alinhamento, mas na verdade se trata de um float de 80 bits (10 bytes).