Pular para o conteúdo

A System V ABI e Convenção de Chamada

A System V AMD64 ABI é a convenção de chamada contra a qual quase todo sistema Unix-like x86-64, Linux incluído, compila código nativo, e vários artigos desta wiki já se apoiam em partes dela sem soletrar a convenção inteira: System Calls reaproveita suas atribuições de registrador para argumentos de syscall, e Build Systems explica por que -mno-red-zone importa em código de kernel sem definir o que a red zone de fato é. Uma convenção de chamada é simplesmente um acordo, de forma alguma imposto pela CPU, sobre onde argumentos vão, onde o valor de retorno volta, e quais registradores uma função chamada precisa deixar inalterados; nada impede código que a ignora por completo, exceto que ele então não consegue chamar corretamente, nem ser chamado corretamente por, qualquer código compilado esperando-a.

Os primeiros seis argumentos inteiros ou de ponteiro para uma função são passados em registradores fixos, em ordem: RDI, RSI, RDX, RCX, R8, R9. Um sétimo argumento inteiro, e qualquer argumento além do que cabe em registradores, é passado na pilha em vez disso, empilhado em ordem reversa de forma que o primeiro argumento de pilha acabe mais próximo do endereço de retorno. Argumentos de ponto flutuante seguem uma sequência inteiramente separada de XMM0 a XMM7, independente dos registradores inteiros, então uma função recebendo um argumento inteiro e um de ponto flutuante passa o primeiro em RDI e o segundo em XMM0, não RDI e RSI.

long example(long a, long b, double c);
// a -> RDI, b -> RSI, c -> XMM0

O valor de retorno de uma função volta em RAX, ou, para um valor grande demais para um único registrador (um inteiro de 128 bits, por exemplo), dividido entre RDX:RAX da mesma forma que um valor de MSR de 64 bits se divide entre EDX:EAX. Os próprios registradores são divididos em duas categorias que a ABI fixa por convenção: registradores caller-saved (RAX, RCX, RDX, RSI, RDI, R8R11) podem ser livremente sobrescritos por uma função chamada, então qualquer coisa que o chamador ainda precise deles tem que ser salva antes da chamada; registradores callee-saved (RBX, RBP, R12R15) precisam voltar contendo o que quer que contivessem na entrada, então uma função que os usa para seu próprio trabalho de rascunho precisa salvá-los e restaurá-los ela mesma. Essa é uma segunda razão, no nível da ABI (além daquela já coberta quando o próprio CPUID o sobrescreve), pela qual RBX especificamente precisa de salvamento explícito ao redor de assembly inline que o toca: o compilador presume, corretamente para qualquer função escrita de forma apropriada, que RBX sobrevive a uma chamada a menos que informado o contrário.

RSP precisa estar alinhado a 16 bytes no ponto em que uma instrução call executa, o que significa que ela está alinhada a 16 bytes mais 8 imediatamente depois da chamada (o endereço de retorno que call empilha tem 8 bytes, quebrando o que era alinhamento de 16 bytes num offset consistente e definido pela ABI a partir dele) dentro do próprio prólogo da função chamada. Código que aloca espaço de pilha em unidades não alinhadas a esse limite, ou que pula restaurar o alinhamento depois de empilhar um número ímpar de valores de 8 bytes, produz uma pilha desalinhada que a maior parte do código inteiro comum tolera silenciosamente mas que falha imediatamente no momento em que uma instrução SSE alinhada (MOVAPS, entre outras) executa contra ela, já que essas instruções específicas exigem alinhamento de 16 bytes como pré-condição rígida em vez de apenas preferi-lo.

Uma função-folha, uma que não chama nada mais ela mesma, tem permissão de usar até 128 bytes abaixo de RSP como espaço de rascunho sem primeiro subtrair nada de RSP para reservá-lo formalmente, já que nada mais pode estar dependendo dessa memória se a função não chama nada que também poderia querê-la. Essa red zone é o que -mno-red-zone, já coberto do lado da flag de build, desabilita: a suposição quebra especificamente dentro de um tratador de interrupção ou exceção, que pode ser invocado assincronamente a qualquer momento sobre qualquer pilha que o código interrompido estivesse usando, incluindo a própria red zone de uma função-folha ainda contendo dados vivos que o tratador sobrescreveria silenciosamente se usasse os mesmos 128 bytes para seu próprio espaço de rascunho, corrompendo o estado da função interrompida de uma forma que só se manifesta quando a execução volta para ela.

; função-folha usando a red zone, nenhum ajuste de pilha necessário
compute:
mov [rsp - 8], rax ; escrita de rascunho na red zone
mov [rsp - 16], rbx
; ... usa os valores de rascunho ...
ret

As regras da ABI se aplicam uniformemente tanto a código de user space quanto de kernel (a CPU não impõe distinção alguma), mas o build de um kernel precisa deliberadamente sair de duas suposições padrão que o compilador de outra forma faz em nome da ABI: a red zone, insegura no momento em que interrupções podem cair sobre código arbitrário, e geração de código SSE/ponto flutuante irrestrita, insegura até o kernel ter se comprometido a salvar e restaurar esse estado de registrador através de trocas de contexto, motivo pelo qual flags de build de kernel rotineiramente incluem tanto -mno-red-zone quanto -mno-sse/-mno-mmx juntas em vez de qualquer uma isoladamente.

  1. ^ M. Matz, J. Hubička, A. Jaeger, M. Mitchell, System V Application Binary Interface: AMD64 Architecture Processor Supplement: a especificação completa, incluindo atribuição de registrador, alinhamento de pilha, e a red zone.
  • System Calls: reaproveita os registradores de argumento inteiro deste artigo, com uma convenção definida pelo kernel em vez da do compilador.
  • Build Systems: as flags -mno-red-zone e -mno-sse que tiram um build de kernel de duas suposições da ABI inseguras em código de kernel.
  • CPUID: um exemplo concreto de instrução que sobrescreve o registrador callee-saved RBX e precisa dele salvo explicitamente.