Para fornecer um exemplo concreto de como um compilador gerencia a pilha e como os valores na pilha são acessados, podemos ver representações visuais, além do código gerado GCC
em um ambiente Linux com o i386 como arquitetura de destino.
1. Quadros de pilha
Como você sabe, a pilha é um local no espaço de endereço de um processo em execução usado por funções ou procedimentos , no sentido de que o espaço é alocado na pilha para variáveis declaradas localmente, bem como argumentos passados para a função ( espaço para variáveis declaradas fora de qualquer função (ou seja, variáveis globais) é alocado em uma região diferente na memória virtual). O espaço alocado para todos os dados de uma função é referido a um quadro de pilha . Aqui está uma representação visual de vários quadros de pilha (de Computer Systems: A Programmer's Perspective ):
2. Gerenciamento de quadros de pilha e localização variável
Para que os valores gravados na pilha em um quadro de pilha específico sejam gerenciados pelo compilador e lidos pelo programa, deve haver algum método para calcular as posições desses valores e recuperar seu endereço de memória. Os registradores na CPU referidos como ponteiro de pilha e o ponteiro base ajudam nisso.
O ponteiro de base, ebp
por convenção, contém o endereço de memória da parte inferior ou base da pilha. As posições de todos os valores dentro do quadro da pilha podem ser calculadas usando o endereço no ponteiro base como referência. Isso é mostrado na figura acima: %ebp + 4
é o endereço de memória armazenado no ponteiro base mais 4, por exemplo.
3. Código gerado pelo compilador
Mas o que eu não entendo é como as variáveis na pilha são lidas por um aplicativo - se eu declarar e atribuir x como um número inteiro, digamos x = 3, e o armazenamento for reservado na pilha e seu valor 3 for armazenado lá e, em seguida, na mesma função, declaro e atribuo y como, digamos 4, e depois utilizo x em outra expressão (digamos z = 5 + x) como o programa pode ler x para avaliar z quando está abaixo de y na pilha?
Vamos usar um programa de exemplo simples escrito em C para ver como isso funciona:
int main(void)
{
int x = 3;
int y = 4;
int z = 5 + x;
return 0;
}
Vamos examinar o texto de montagem produzido pelo GCC para este texto de origem C (limpei-o um pouco por uma questão de clareza):
main:
pushl %ebp # save previous frame's base address on stack
movl %esp, %ebp # use current address of stack pointer as new frame base address
subl $16, %esp # allocate 16 bytes of space on stack for function data
movl $3, -12(%ebp) # variable x at address %ebp - 12
movl $4, -8(%ebp) # variable y at address %ebp - 8
movl -12(%ebp), %eax # write x to register %eax
addl $5, %eax # x + 5 = 9
movl %eax, -4(%ebp) # write 9 to address %ebp - 4 - this is z
movl $0, %eax
leave
O que observamos é que as variáveis X, Y e Z estão localizados em endereços %ebp - 12
, %ebp -8
e %ebp - 4
, respectivamente. Em outras palavras, os locais das variáveis no quadro da pilha main()
são calculados usando o endereço de memória salvo no registro da CPU %ebp
.
4. Os dados na memória além do ponteiro da pilha estão fora do escopo
Estou claramente perdendo alguma coisa. Será que o local na pilha é apenas sobre o tempo de vida / escopo da variável e que toda a pilha está realmente acessível ao programa o tempo todo? Em caso afirmativo, isso implica que existe algum outro índice que mantém os endereços apenas das variáveis na pilha para permitir que os valores sejam recuperados? Mas então eu pensei que o ponto principal da pilha era que os valores eram armazenados no mesmo local que o endereço da variável?
A pilha é uma região na memória virtual, cujo uso é gerenciado pelo compilador. O compilador gera código de maneira que valores além do ponteiro da pilha (valores além do topo da pilha) nunca sejam referenciados. Quando uma função é chamada, a posição do ponteiro da pilha muda para criar espaço na pilha considerado não "fora dos limites", por assim dizer.
À medida que as funções são chamadas e retornam, o ponteiro da pilha é decrementado e incrementado. Os dados gravados na pilha não desaparecem depois que estão fora do escopo, mas o compilador não gera instruções referenciando esses dados porque não há como o compilador calcular os endereços desses dados usando %ebp
ou %esp
.
5. Resumo
O código que pode ser executado diretamente pela CPU é gerado pelo compilador. O compilador gerencia a pilha, quadros de pilha para funções e registros da CPU. Uma estratégia usada pelo GCC para rastrear os locais das variáveis nos quadros de pilha no código destinado à execução na arquitetura i386 é usar o endereço de memória no ponteiro base do quadro de pilha %ebp
, como referência e escrever valores de variáveis nos locais nos quadros de pilha em deslocamentos para o endereço em %ebp
.