Pergunta

Como ocorre um estouro de pilha e quais são as melhores maneiras de garantir que isso não aconteça, ou maneiras de evitá-lo, principalmente em servidores web, mas outros exemplos também seriam interessantes?

Foi útil?

Solução

Pilha

Uma pilha, neste contexto, é o buffer último a entrar, primeiro a sair, onde você coloca os dados enquanto o programa é executado.Último a entrar, primeiro a sair (LIFO) significa que a última coisa que você coloca é sempre a primeira que você retira - se você colocar 2 itens na pilha, 'A' e depois 'B', então a primeira coisa que você colocar fora da pilha será 'B' e o próximo será 'A'.

Quando você chama uma função em seu código, a próxima instrução após a chamada de função é armazenada na pilha e qualquer espaço de armazenamento que possa ser substituído pela chamada de função.A função que você chama pode usar mais pilha para suas próprias variáveis ​​locais.Quando terminar, ele libera o espaço de pilha da variável local usado e retorna à função anterior.

Estouro de pilha

Um estouro de pilha ocorre quando você usa mais memória para a pilha do que o programa deveria usar.Em sistemas embarcados você pode ter apenas 256 bytes para a pilha, e se cada função ocupar 32 bytes, então você só poderá ter chamadas de função 8 profundas - a função 1 chama a função 2 quem chama a função 3 quem chama a função 4 ....quem chama a função 8 quem chama a função 9, mas a função 9 sobrescreve a memória fora da pilha.Isso pode substituir memória, código, etc.

Muitos programadores cometem esse erro ao chamar a função A, que então chama a função B, que então chama a função C, que então chama a função A.Pode funcionar na maioria das vezes, mas apenas uma vez a entrada errada fará com que ele permaneça nesse círculo para sempre até que o computador reconheça que a pilha está exagerada.

Funções recursivas também são uma causa para isso, mas se você estiver escrevendo recursivamente (ou seja, sua função chama a si mesma), então você precisa estar ciente disso e usar variáveis ​​estáticas/globais para evitar recursão infinita.

Geralmente, o sistema operacional e a linguagem de programação que você está usando gerenciam a pilha e ela está fora de seu controle.Você deve observar seu gráfico de chamadas (uma estrutura de árvore que mostra a partir de seu principal o que cada função chama) para ver a profundidade de suas chamadas de função e para detectar ciclos e recursões que não são pretendidos.Os ciclos intencionais e a recursão precisam ser verificados artificialmente para evitar erros se eles se chamarem muitas vezes.

Além de boas práticas de programação e testes estáticos e dinâmicos, não há muito que você possa fazer nesses sistemas de alto nível.

Sistemas embarcados

No mundo incorporado, especialmente em código de alta confiabilidade (automotivo, aeronáutico, espacial), você faz extensas revisões e verificações de código, mas também faz o seguinte:

  • Proibir recursão e ciclos - aplicados por políticas e testes
  • Mantenha o código e a pilha distantes (código em flash, pilha em RAM e nunca os dois se encontrarão)
  • Coloque faixas de proteção ao redor da pilha - área vazia da memória que você preenche com um número mágico (geralmente uma instrução de interrupção de software, mas há muitas opções aqui) e centenas ou milhares de vezes por segundo você olha para as faixas de proteção para ter certeza eles não foram substituídos.
  • Use proteção de memória (ou seja, não execute na pilha, não leia ou grave fora da pilha)
  • As interrupções não chamam funções secundárias - elas definem sinalizadores, copiam dados e deixam o aplicativo cuidar de processá-los (caso contrário, você pode se aprofundar em sua árvore de chamada de função, ter uma interrupção e, em seguida, sair de outras funções dentro do interromper, causando a explosão).Você tem várias árvores de chamadas – uma para os processos principais e outra para cada interrupção.Se suas interrupções podem interromper umas às outras...bem, existem dragões...

Linguagens e sistemas de alto nível

Mas em linguagens de alto nível executadas em sistemas operacionais:

  • Reduza o armazenamento de variáveis ​​locais (variáveis ​​locais são armazenadas na pilha - embora os compiladores sejam bastante inteligentes sobre isso e às vezes coloquem grandes locais no heap se sua árvore de chamadas for superficial)
  • Evite ou limite estritamente a recursão
  • Não divida seus programas em funções cada vez menores - mesmo sem contar variáveis ​​locais, cada chamada de função consome até 64 bytes na pilha (processador de 32 bits, economizando metade dos registros da CPU, sinalizadores, etc.)
  • Mantenha sua árvore de chamadas rasa (semelhante à afirmação acima)

Servidores web

Depende da 'sandbox' que você possui se você pode controlar ou até mesmo ver a pilha.Há boas chances de você tratar os servidores da Web como faria com qualquer outra linguagem e sistema operacional de alto nível - isso está fora de seu controle, mas verifique o idioma e a pilha de servidores que você está usando.Isto é possível explodir a pilha do seu servidor SQL, por exemplo.

-Adão

Outras dicas

Um estouro de pilha em código real ocorre muito raramente.A maioria das situações em que isso ocorre são recursões em que o encerramento foi esquecido.No entanto, pode ocorrer raramente em estruturas altamente aninhadas, por ex.documentos XML particularmente grandes.A única ajuda real aqui é refatorar o código para usar um objeto de pilha explícito em vez da pilha de chamadas.

A maioria das pessoas dirá que ocorre um estouro de pilha com recursão sem um caminho de saída - embora seja verdade, se você trabalhar com estruturas de dados grandes o suficiente, mesmo um caminho de saída de recursão adequado não o ajudará.

Algumas opções neste caso:

A recursão infinita é uma maneira comum de obter um erro de estouro de pilha.Para prevenir - certifique-se sempre de que existe um caminho de saída que vai ser atingido.:-)

Outra maneira de obter um estouro de pilha (em C/C++, pelo menos) é declarar alguma variável enorme na pilha.

char hugeArray[100000000];

Isso bastará.

Um estouro de pilha ocorre quando Jeff e Joel querem oferecer ao mundo um lugar melhor para obter respostas a questões técnicas.É tarde demais para evitar esse estouro de pilha.Esse "outro site" poderia ter evitado isso por não ser obsceno.;)

Normalmente, um estouro de pilha é o resultado de uma chamada recursiva infinita (dada a quantidade usual de memória em computadores padrão hoje em dia).

Quando você faz uma chamada a um método, função ou procedimento a forma "padrão" ou fazer a chamada consiste em:

  1. Empurrando a direção de retorno da chamada para a pilha (essa é a próxima frase após a chamada)
  2. Normalmente, o espaço para o valor de retorno é reservado na pilha
  3. Colocar cada parâmetro na pilha (a ordem diverge e depende de cada compilador, alguns deles às vezes são armazenados nos registradores da CPU para melhorias de desempenho)
  4. Fazendo a chamada real.

Então, normalmente isso leva alguns bytes dependendo do número e tipo dos parâmetros, bem como da arquitetura da máquina.

Você verá então que se começar a fazer chamadas recursivas a pilha começará a crescer.Agora, a pilha geralmente é reservada na memória de forma que cresça na direção oposta ao heap, portanto, dado um grande número de chamadas sem "voltar" a pilha começa a ficar cheia.

Agora, em tempos antigos, o estouro de pilha poderia ocorrer simplesmente porque você esgotou toda a memória disponível, simplesmente assim.Com o modelo de memória virtual (até 4 GB em um sistema X86) que estava fora do escopo, normalmente, se você receber um erro de estouro de pilha, procure uma chamada recursiva infinita.

Além da forma de estouro de pilha que você obtém de uma recursão direta (por exemplo Fibonacci(1000000)), uma forma mais sutil que experimentei muitas vezes é uma recursão indireta, onde uma função chama outra função, que chama outra, e então uma dessas funções chama a primeira novamente.

Isso geralmente pode ocorrer em funções chamadas em resposta a eventos, mas que podem gerar novos eventos, por exemplo:

void WindowSizeChanged(Size& newsize) {
  // override window size to constrain width
    newSize.width=200;
    ResizeWindow(newSize);
}

Neste caso a chamada para ResizeWindow pode causar o WindowSizeChanged() retorno de chamada seja acionado novamente, o que chama ResizeWindow novamente, até ficar sem pilha.Em situações como essas, muitas vezes você precisa adiar a resposta ao evento até que o stack frame retorne, por exemplo, postando uma mensagem.

O que?Ninguém gosta de quem está preso em um loop infinito?

do
{
  JeffAtwood.WritesCode();
} while(StackOverflow.MakingMadBank.Equals(false));

Considerando que isso foi marcado como "hacking", suspeito que o "estouro de pilha" ao qual ele está se referindo seja um estouro de pilha de chamadas, em vez de um estouro de pilha de nível superior, como os mencionados na maioria das outras respostas aqui.Na verdade, não se aplica a nenhum ambiente gerenciado ou interpretado, como .NET, Java, Python, Perl, PHP, etc., nos quais os aplicativos da web são normalmente escritos, portanto, seu único risco é o próprio servidor da web, que provavelmente está escrito em C ou C++.

Confira este tópico:

https://stackoverflow.com/questions/7308/what-is-a-good-starting-point-for-learning-buffer-overflow

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top