A pilha de chamadas também pode ser chamada de pilha de quadros.
As coisas que são empilhadas após o princípio LIFO não são as variáveis locais, mas todos os quadros de pilha ("chamadas") das funções que estão sendo chamadas . As variáveis locais são enviadas e colocadas juntas com esses quadros no chamado prólogo e epílogo da função , respectivamente.
Dentro do quadro, a ordem das variáveis é completamente não especificada; Os compiladores "reordenam" as posições das variáveis locais dentro de um quadro de maneira apropriada para otimizar seu alinhamento, de forma que o processador possa buscá-las o mais rápido possível. O fato crucial é que o deslocamento das variáveis em relação a algum endereço fixo é constante ao longo da vida útil do quadro - portanto, é suficiente pegar um endereço de âncora, digamos, o endereço do próprio quadro e trabalhar com deslocamentos desse endereço para as variáveis. Tal endereço de âncora está realmente contido na chamada base ou ponteiro de quadroarmazenado no registro EBP. Os offsets, por outro lado, são claramente conhecidos em tempo de compilação e, portanto, são codificados no código de máquina.
Este gráfico da Wikipedia mostra como a pilha de chamadas típica é estruturada como 1 :
Adicione o deslocamento de uma variável que queremos acessar ao endereço contido no ponteiro do quadro e obteremos o endereço de nossa variável. Resumindo, o código apenas os acessa diretamente por meio de deslocamentos de tempo de compilação constantes do ponteiro base; É simples aritmética de ponteiro.
Exemplo
#include <iostream>
int main()
{
char c = std::cin.get();
std::cout << c;
}
gcc.godbolt.org nos dá
main:
pushq %rbp
movq %rsp, %rbp
subq $16, %rsp
movl std::cin, %edi
call std::basic_istream<char, std::char_traits<char> >::get()
movb %al, -1(%rbp)
movsbl -1(%rbp), %eax
movl %eax, %esi
movl std::cout, %edi
call [... the insertion operator for char, long thing... ]
movl $0, %eax
leave
ret
.. para main
. Eu dividi o código em três subseções. O prólogo da função consiste nas três primeiras operações:
- O ponteiro da base é colocado na pilha.
- O ponteiro da pilha é salvo no ponteiro base
- O ponteiro da pilha é subtraído para abrir espaço para variáveis locais.
Em seguida, cin
é movido para o registrador EDI 2 e get
é chamado; O valor de retorno está em EAX.
Por enquanto, tudo bem. Agora o interessante acontece:
O byte de ordem inferior de EAX, designado pelo registrador de 8 bits AL, é obtido e armazenado no byte logo após o ponteiro de base : Ou seja -1(%rbp)
, o deslocamento do ponteiro de base é -1
. Este byte é nossa variávelc
. O deslocamento é negativo porque a pilha cresce para baixo em x86. A próxima operação armazena c
em EAX: EAX é movido para ESI, cout
é movido para EDI e então o operador de inserção é chamado com cout
e c
sendo os argumentos.
Finalmente,
- O valor de retorno de
main
é armazenado em EAX: 0. Isso se deve à return
declaração implícita . Você também pode ver em xorl rax rax
vez de movl
.
- sair e voltar ao site da chamada.
leave
está abreviando este epílogo e implicitamente
- Substitui o ponteiro da pilha pelo ponteiro da base e
- Abre o ponteiro da base.
Após esta operação e ret
ter sido executada, o quadro foi efetivamente exibido, embora o chamador ainda tenha que limpar os argumentos, pois estamos usando a convenção de chamada cdecl. Outras convenções, por exemplo, stdcall, exigem que o receptor faça a limpeza, por exemplo, passando a quantidade de bytes para ret
.
Omissão do Frame Pointer
Também é possível não usar deslocamentos do ponteiro de base / quadro, mas sim do ponteiro de pilha (ESB). Isso torna o registrador EBP que, de outra forma, conteria o valor do ponteiro do quadro disponível para uso arbitrário - mas pode tornar a depuração impossível em algumas máquinas e será implicitamente desativado para algumas funções . É particularmente útil ao compilar para processadores com apenas alguns registros, incluindo x86.
Essa otimização é conhecida como FPO (omissão de ponteiro de quadro) e definida por -fomit-frame-pointer
no GCC e -Oy
no Clang; observe que ele é acionado implicitamente por cada nível de otimização> 0 se e somente se a depuração ainda for possível, uma vez que não tem nenhum custo além disso. Para mais informações, clique aqui e aqui .
1 Conforme apontado nos comentários, o ponteiro do frame deve apontar para o endereço após o endereço do remetente.
2 Observe que os registros que começam com R são as contrapartes de 64 bits daqueles que começam com E. EAX designa os quatro bytes de ordem inferior de RAX. Usei os nomes dos registradores de 32 bits para maior clareza.