Por que ainda aumentamos a pilha para trás?

46

Ao compilar o código C e observar o assembly, tudo faz com que a pilha cresça para trás da seguinte maneira:

_main:
    pushq   %rbp
    movl    $5, -4(%rbp)
     popq    %rbp
    ret

-4(%rbp)- isso significa que o ponteiro base ou o ponteiro da pilha está realmente movendo-se pelos endereços de memória em vez de subir? Por que é que?

Mudei $5, -4(%rbp)para $5, +4(%rbp), compilei e executei o código e não houve erros. Então, por que ainda precisamos retroceder na pilha de memória?

alex
fonte
2
Observe que -4(%rbp)não move o ponteiro da base e +4(%rbp)não poderia ter funcionado.
Margaret Bloom
14
" por que ainda precisamos retroceder " - qual você acha que seria a vantagem de avançar? Em última análise, não importa, você só precisa escolher um.
Bergi 27/01
31
"por que aumentamos a pilha para trás?" - porque se não o fizermos, outra pessoa perguntaria por que mallocaumenta a pilha para trás
slebetman 28/01
2
@MargaretBloom: Aparentemente na plataforma do OP, código do CRT arranque não se importa se mainclobbers sua RBP. Isso é certamente possível. (E sim, a escrita 4(%rbp)pisaria no valor RBP salvo). Na verdade, esse principal nunca funciona mov %rsp, %rbp, portanto o acesso à memória é relativo ao RBP do chamador , se é isso que o OP realmente testou !!! Se isso foi realmente copiado da saída do compilador, algumas instruções foram deixadas de fora!
Peter Cordes
1
Parece-me que "para trás" ou "para frente" (ou "para baixo" e "para cima") depende do seu ponto de vista. Se você tiver diagramado a memória como uma coluna com endereços baixos na parte superior, o aumento da pilha decrementando um ponteiro da pilha seria análogo a uma pilha física.
jamesdlin 29/01

Respostas:

86

Isso significa que o ponteiro base ou o ponteiro da pilha estão realmente movendo os endereços de memória em vez de subir? Por que é que?

Sim, as pushinstruções diminuem o ponteiro da pilha e gravam na pilha, enquanto popfazem o inverso, leem da pilha e aumentam o ponteiro da pilha.

Isso é um pouco histórico, pois para máquinas com memória limitada, a pilha foi colocada alta e cresceu para baixo, enquanto a pilha foi colocada baixa e cresceu para cima. Há apenas uma lacuna de "memória livre" - entre a pilha e a pilha, e essa lacuna é compartilhada; qualquer um pode crescer na lacuna conforme necessário individualmente. Portanto, o programa fica sem memória quando a pilha e o heap colidem, deixando sem memória livre. 

Se a pilha e a pilha crescem na mesma direção, há duas lacunas, e a pilha não pode realmente crescer na lacuna da pilha (o vice-versa também é problemático).

Originalmente, os processadores não tinham instruções de manipulação de pilha dedicadas. No entanto, como o suporte à pilha foi adicionado ao hardware, ele assumiu esse padrão de crescimento descendente e os processadores ainda seguem esse padrão hoje.

Alguém poderia argumentar que em uma máquina de 64 bits há espaço de endereço suficiente para permitir várias lacunas - e, como evidência, várias lacunas são necessariamente o caso quando um processo possui vários encadeamentos. Embora isso não seja motivação suficiente para mudar tudo, uma vez que, com sistemas com múltiplos hiatos, a direção do crescimento é indiscutivelmente arbitrária, então a tradição / compatibilidade diminui a escala.


Você teria que mudar as instruções de manuseamento da pilha CPU, a fim de mudar a direção da pilha, ou então desistir de uso das empurrando & popping instruções (por exemplo, dedicados push, pop, call, ret, outros).

Observe que a arquitetura do conjunto de instruções MIPS não possui push& dedicado pop; portanto, é prático aumentar a pilha em qualquer direção - você ainda pode querer um layout de memória de uma lacuna para um processo de encadeamento único, mas pode aumentar a pilha para cima e para a pilha. para baixo. Se você fez isso, no entanto, algum código C varargs pode exigir ajuste na origem ou na passagem de parâmetro oculto.

(De fato, como não há manipulação de pilha dedicada no MIPS, poderíamos usar pré ou pós incremento ou pré ou pós decremento para empurrar a pilha, desde que usássemos o reverso exato para saltar da pilha e também assumindo que o O sistema operacional respeita o modelo de uso de pilha escolhido. De fato, em alguns sistemas embarcados e em alguns sistemas educacionais, a pilha MIPS é aumentada.)

Erik Eidt
fonte
32
Não é apenas pushe popna maioria das arquiteturas, mas também o mais importante de interrupção-assistência, call, ret, e tudo aquilo que tem cozido em interação com a pilha.
Deduplicator
3
O ARM pode ter todos os quatro tipos de pilha.
Margaret Bloom
14
Pelo que vale, não acho que "a direção do crescimento seja arbitrária", no sentido de que qualquer uma das opções é igualmente boa. Crescendo para baixo tem a propriedade que ultrapassa o final de um buffer clobbers quadros de pilha anteriores, incluindo endereços de retorno salvos. Crescer tem a propriedade de estourar o final de um buffer de armazenamento apenas no mesmo ou mais tarde (se o buffer não for o mais recente, pode haver mais tarde) quadro de chamada e, possivelmente, apenas o espaço não utilizado (todos assumindo uma proteção página após a pilha). Portanto, do ponto de vista da segurança, crescer parece altamente preferível
R ..
6
@R ..: crescer não elimina explorações de saturação de buffer, porque funções vulneráveis ​​geralmente não são funções folha: elas chamam outras funções, colocando um endereço de retorno acima do buffer. As funções de folha que recebem um ponteiro do chamador podem se tornar vulneráveis ​​à substituição de seu próprio endereço de retorno. Por exemplo, se uma função alocar um buffer na pilha e o passar para gets(), ou fizer um strcpy()que não seja incorporado, o retorno nessas funções da biblioteca usará o endereço de retorno substituído. Atualmente, com pilhas de crescimento descendente, é quando o chamador retorna.
Peter Cordes
5
@ PeterCordes: Na verdade, meu comentário observou que os quadros de pilha do mesmo nível ou mais recentes que o buffer estourado ainda são potencialmente imperceptíveis, mas isso é muito menos. No caso em que a função clobber é uma função folha chamada diretamente pela função cujo buffer é (por exemplo strcpy), em um arco em que o endereço de retorno é mantido em um registro, a menos que precise ser derramado, não há acesso para clobber o retorno endereço.
R ..
8

No seu sistema específico, a pilha começa do endereço de memória alta e "cresce" para baixo, para endereços de memória baixa. (o caso simétrico de baixo para alto também existe)

E como você mudou de -4 e +4 e foi executado, isso não significa que está correto. O layout da memória de um programa em execução é mais complexo e depende de muitos outros fatores que podem contribuir para o fato de você não ter travado instantaneamente neste programa extremamente simples.

nadir
fonte
1

O ponteiro da pilha aponta para o limite entre a memória da pilha alocada e a não alocada. Aumentá-lo para baixo significa que aponta para o início da primeira estrutura no espaço de pilha alocado, com outros itens alocados seguindo em endereços maiores. Ter ponteiros apontando para o início das estruturas alocadas é muito mais comum do que o contrário.

Atualmente, em muitos sistemas hoje em dia, existe um registro separado para os quadros de pilha que podem ser desenrolados de maneira confiável para descobrir a cadeia de chamadas, com o armazenamento variável local intercalado. A maneira como esse registro de quadro de pilha é configurado em algumas arquiteturas significa que ele acaba apontando para trás do armazenamento da variável local, em oposição ao ponteiro da pilha antes dele. Portanto, o uso desse registro de quadro de pilha requer indexação negativa.

Observe que os quadros de pilha e sua indexação são um aspecto lateral das linguagens de computador compiladas; portanto, é o gerador de código do compilador que precisa lidar com a "falta de naturalidade" em vez de com um programador de linguagem assembly ruim.

Portanto, embora existam boas razões históricas para a escolha de pilhas para crescer (e algumas delas são mantidas se você programar em linguagem assembly e não se incomodar em configurar um quadro de pilha adequado), elas se tornam menos visíveis.


fonte
2
"Agora em muitos sistemas hoje em dia, há um registro separado para quadros de pilha", você está atrasado. Os formatos mais avançados de informações de depuração removeram amplamente a necessidade de ponteiros de quadros atualmente.
Peter Green