Convenção de chamada de funções
A convenção abordada aqui é a convenção de chamada padrão do System V AMD64 ABI, usado pelo Linux, macOS, BSD, etc. para código em modo usuário.
Imagine que temos uma função funcao(arg1, arg2, arg3,...). Como a função obtém seus parâmetros?
Troca de stack frame
Lmbre-se que no início de toda função temos a criação de um novo stack frame para a função com push rbp (salva rbp/stack frame antigo) e mov RBP, RSP (novo stack frame). Antes do push rbp, a instrução call faz o push rip (salva return address) e jmp.
Assim, antes de começar a puxar os parâmetros na função, temos a troca de stack frame.
Convenção de chamada para 32-bit e 64-bit
x86 (32-bit)
- Argumentos são todos passados na stack (
push arg1; push arg2; ...) - Chama-se função (
call funcao=push rip; jmp funcao) - Função cria novo stack frame (
push rbp; mov rbp, rsp) funcaovai buscar o valor para o primeiro parâmetro após oreturn address(depois de criar novo frame). Isso seria aproximadamenteesp+4(para o stack frame antigo, considerando o endereço de retorno na stack)
Antes entrar em funcao, mas após call:
ESP → endereço de retorno
ESP+4 → primeiro argumento
ESP+8 → segundo argumento, etc.
Quando entramos em funcao:
- Novo ESP e EBP (novo stack frame) (
push ebp; mov ebp, esp) funcaoespera encontrar o valor paraarg1emebp+8, e os outros em seguida
EBP+0 → RBP antigo
EBP+4 → endereço de retorno
ESP+8 → primeiro argumento
ESP+12 → segundo argumento, etc.
x86-64 (64-bit)
- Os primeiros argumentos são passados em registradores
- Argumentos excedentes são passados na stack (
push arg7; push arg8; ...) - Chama-se função (
call funcao=push rip; jmp funcao) - Função cria novo stack frame (
push rbp; mov rbp, rsp) funcaovai primeiro olhar nos registradores que armazenam parâmetros (RDI,RSI,RDX,RCX,R8,R9)- Só depois busca na stack
Lembrando a ordem de passagem de argumentos para registradores:
RDI(Endereço da Format String)RSI(Parâmetro 1)RDX(Parâmetro 2)RCX(...)R8R9- Stack (RSP+8, RSP+16, ...)
Alinhamento de stack
A stack deve estar alinhada em 16 bytes no momento da chamada call (rsp % 16 == 0). Para funções nativas do código isso não é problema, mas para ataques precisamos ter isso em mente.