Convenção de chamada da System V ABI

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.

Convenção de chamada em x86-64

Registradores

  • 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.

Stack frame

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:

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

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.

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.

Passagem de parâmetros

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.

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.

Retorno de valores

  • 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.

Convenção de chamada em IA-32

Registradores

  • 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.

Stack frame

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.

Passagem de parâmetros

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.

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.

Retorno de valores

  • 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".

Tamanho dos tipos da linguagem C

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 IA-32

Tamanho x86-64

Registrador de retorno IA-32

Registrador de retorno x86-64

_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).

Last updated