Pergunta

Em software embarcado multithread (escrito em C ou C++), um thread deve receber espaço de pilha suficiente para permitir que ele complete suas operações sem transbordar.O dimensionamento correto da pilha é fundamental em alguns ambientes embarcados de tempo real, porque (pelo menos em alguns sistemas com os quais trabalhei) o sistema operacional NÃO detectará isso para você.

Normalmente, o tamanho da pilha para um novo thread (diferente do thread principal) é designado no momento em que o thread é criado (ou seja,em um argumento para pthread_create() ou algo semelhante).Freqüentemente, esses tamanhos de pilha são codificados para valores que são considerados bons no momento em que o código foi originalmente escrito ou testado.

No entanto, alterações futuras no código muitas vezes quebram as suposições nas quais os tamanhos de pilha codificados foram baseados e, em um dia fatídico, seu thread entra em uma das ramificações mais profundas de seu gráfico de chamadas e transborda a pilha - derrubando todo o sistema. ou corrompendo silenciosamente a memória.

Eu pessoalmente vi esse problema no caso em que o código executado no thread declara instâncias de struct na pilha.Quando a estrutura é aumentada para conter dados adicionais, o tamanho da pilha aumenta de acordo, permitindo potencialmente a ocorrência de estouros de pilha.Imagino que isso possa ser um grande problema para bases de código estabelecidas, onde todos os efeitos da adição de campos a uma estrutura não podem ser conhecidos imediatamente (muitos threads/funções para encontrar todos os locais onde essa estrutura é usada).

Como a resposta usual às questões de "dimensionamento de pilha" é "eles não são portáteis", vamos supor que o compilador, o sistema operacional e o processador sejam quantidades conhecidas para esta investigação.Suponhamos também que a recursão não seja usada, portanto não estamos lidando com a possibilidade de um cenário de "recursão infinita".

Quais são algumas maneiras confiáveis ​​de estimar o tamanho da pilha necessária para um thread?Eu preferiria métodos offline (análise estática) e automáticos, mas todas as ideias são bem-vindas.

Foi útil?

Solução

Avaliação de tempo de execução

Um método online é pintar a pilha completa com um determinado valor, como 0xAAAA (ou 0xAA, qualquer que seja sua largura).Em seguida, você pode verificar o quanto a pilha cresceu ao máximo no passado, verificando quanto da pintura permaneceu intacta.

Dê uma olhada em esse link para uma explicação com ilustração.

A vantagem é que é simples.Uma desvantagem é que você não pode ter certeza de que o tamanho da pilha não excederá a quantidade de pilha usada durante o teste.

Avaliação Estática

Existem algumas verificações estáticas e acho que existe até uma versão hackeada do gcc que tenta fazer isso.A única coisa que posso dizer é que a verificação estática é muito difícil de ser feita no caso geral.

Dê uma olhada também esse pergunta.

Outras dicas

Você pode usar uma ferramenta de análise estática como StackAnalyzer, se o seu alvo atender aos requisitos.

Se você quiser gastar um dinheiro significativo, você pode usar uma ferramenta comercial de análise estática como o Klocwork.Embora o Klocwork tenha como objetivo principal detectar defeitos de software e vulnerabilidades de segurança.No entanto, ele também possui uma ferramenta chamada ‘kwstackoverflow’ que pode ser usada para detectar estouro de pilha em uma tarefa ou thread.Estou usando para o projeto incorporado em que trabalho e tenho tido resultados positivos.Não acho que nenhuma ferramenta como essa seja perfeita, mas acredito que essas ferramentas comerciais são muito boas.A maioria das ferramentas que encontrei lutam com ponteiros de função.Também sei que muitos fornecedores de compiladores, como a Green Hills, agora incorporam funcionalidades semelhantes diretamente em seus compiladores.Esta é provavelmente a melhor solução porque o compilador tem conhecimento profundo de todos os detalhes necessários para tomar decisões precisas sobre o tamanho da pilha.

Se você tiver tempo, tenho certeza de que poderá usar uma linguagem de script para criar sua própria ferramenta de análise de estouro de pilha.O script precisaria identificar o ponto de entrada da tarefa ou thread, gerar uma árvore de chamada de função completa e, em seguida, calcular a quantidade de espaço de pilha que cada função usa.Suspeito que provavelmente existam ferramentas gratuitas disponíveis que podem gerar uma árvore completa de chamadas de função, o que deve facilitar.Se você conhece as especificidades da sua plataforma, gerar o espaço de pilha que cada função usa pode ser muito fácil.Por exemplo, a primeira instrução assembly de uma função PowerPC geralmente é a palavra armazenada com instrução de atualização que ajusta o ponteiro da pilha pela quantidade necessária para a função.Você pode calcular o tamanho em bytes desde a primeira instrução, o que torna relativamente fácil determinar o espaço total da pilha usado.

Todos esses tipos de análise fornecerão uma aproximação do limite superior do pior caso para o uso da pilha, que é exatamente o que você deseja saber.Claro, especialistas (como aqueles com quem trabalho) podem reclamar que você está alocando muito espaço na pilha, mas eles são dinossauros que não se importam com a boa qualidade do software :)

Uma outra possibilidade, embora não calcule o uso da pilha, seria usar a unidade de gerenciamento de memória (MMU) do seu processador (se houver) para detectar o estouro da pilha.Fiz isso no VxWorks 5.4 usando um PowerPC.A ideia é simples, basta colocar uma página de memória protegida contra gravação no topo da pilha.Se você estourar, ocorrerá uma execução do processador e você será rapidamente alertado sobre o problema de estouro de pilha.É claro que ele não informa quanto você precisa aumentar o tamanho da pilha, mas se você for bom em depurar arquivos de exceção/núcleo, poderá pelo menos descobrir a sequência de chamada que estourou a pilha.Você pode então usar essas informações para aumentar o tamanho da sua pilha de forma adequada.

-djhaus

Não é gratuito, mas Cobertura faz análise estática da pilha.

A verificação estática (off-line) da pilha não é tão difícil quanto parece.Eu o implementei para nosso IDE incorporado (RapidiTTy) - atualmente funciona para ARM7 (NXP LPC2xxx), Cortex-M3 (STM32 e NXP LPC17xx), x86 e nosso soft-core FPGA compatível com MIPS ISA interno.

Essencialmente, usamos uma análise simples do código executável para determinar o uso da pilha de cada função.A alocação de pilha mais significativa é feita no início de cada função;apenas certifique-se de ver como ele se altera com diferentes níveis de otimização e, se aplicável, conjuntos de instruções ARM/Thumb, etc.Lembre-se também de que as tarefas geralmente têm suas próprias pilhas e os ISRs frequentemente (mas nem sempre) compartilham uma área de pilha separada!

Depois de usar cada função, é bastante fácil construir uma árvore de chamadas a partir da análise e calcular o uso máximo para cada função.Nosso IDE gera agendadores (RTOSes finos efetivos) para você, então sabemos exatamente quais funções estão sendo designadas como 'tarefas' e quais são ISRs, para que possamos dizer o pior caso de uso para cada área da pilha.

É claro que estes números estão quase sempre acima do real máximo.Pense em uma função como sprintf que pode usar um muito de espaço de pilha, mas varia enormemente dependendo da string de formato e dos parâmetros que você fornece.Para essas situações, você também pode usar a análise dinâmica - preencha a pilha com um valor conhecido em sua inicialização, depois execute o depurador por um tempo, faça uma pausa e veja quanto de cada pilha ainda está preenchido com seu valor (teste estilo marca d'água alta) .

Nenhuma das abordagens é perfeita, mas combinar ambas lhe dará uma imagem bastante boa de como será o uso no mundo real.

Como discutido na resposta a essa questão, uma técnica comum é inicializar a pilha com um valor conhecido e depois executar o código por um tempo e ver onde o padrão para.

Este não é um método offline, mas no projeto em que estou trabalhando, temos um comando debug que lê o limite máximo em todas as pilhas de tarefas do aplicativo.Isso gera uma tabela do uso da pilha para cada tarefa e a quantidade de espaço disponível.A verificação desses dados após uma execução de 24 horas com muita interação do usuário nos dá alguma confiança de que as alocações de pilha definidas são "seguras".

Isso funciona usando a técnica bem testada de preencher as pilhas com um padrão conhecido e assumindo que a única maneira de reescrever isso é pelo uso normal da pilha, embora se estiver sendo escrito por qualquer outro meio, um estouro de pilha é o menos das suas preocupações!

Tentamos resolver esse problema em um sistema embarcado no meu trabalho.Ficou uma loucura, há muito código (nossos frameworks e de terceiros) para obter qualquer resposta confiável.Felizmente, nosso dispositivo era baseado em Linux, então voltamos ao comportamento padrão de dar 2 MB a cada thread e deixar o gerenciador de memória virtual otimizar o uso.

Nosso único problema com esta solução foi que uma das ferramentas de terceiros executou um mlock em todo o seu espaço de memória (idealmente para melhorar o desempenho).Isso fez com que todos os 2 MB de pilha para cada thread de seus threads (75-150 deles) fossem paginados.Perdemos metade do nosso espaço de memória até descobrirmos e comentarmos a linha ofensiva.

Nota:O gerenciador de memória virtual do Linux (vmm) aloca RAM em blocos de 4k.Quando um novo thread solicita 2 MB de espaço de endereço para sua pilha, o vmm atribui páginas de memória falsas a todas as páginas, exceto à página superior.Quando a pilha cresce para uma página falsa, o kernel detecta uma falha de página e troca a página falsa por uma página real (que consome outros 4k de RAM real).Dessa forma, a pilha de um thread pode crescer para qualquer tamanho necessário (desde que seja inferior a 2 MB) e o vmm garantirá que apenas uma quantidade mínima de memória seja usada.

Além de algumas das sugestões já feitas, gostaria de salientar que, muitas vezes, em sistemas embarcados, é necessário controlar rigorosamente o uso da pilha, pois é necessário manter o tamanho da pilha em um tamanho razoável.

De certa forma, usar espaço de pilha é um pouco como alocar memória, mas sem uma maneira (fácil) de determinar se sua alocação foi bem-sucedida, portanto, não controlar o uso da pilha resultará em uma luta eterna para descobrir por que seu sistema está travando novamente.Então, por exemplo, se o seu sistema aloca memória para variáveis ​​locais da pilha, aloque essa memória com malloc() ou, se você não puder usar malloc(), escreva seu próprio manipulador de memória (que é uma tarefa bastante simples).

Não não:

void func(myMassiveStruct_t par)
{
  myMassiveStruct_t tmpVar;
}

Sim Sim:

void func (myMassiveStruct_t *par)
{
  myMassiveStruct_t *tmpVar;
  tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t));
}

Parece bastante óbvio, mas muitas vezes não é - especialmente quando você não pode usar malloc().

É claro que você ainda terá problemas, então isso é apenas algo para ajudar, mas não resolve o seu problema.No entanto, isso ajudará você a estimar o tamanho da pilha no futuro, pois depois de encontrar um bom tamanho para suas pilhas e se, após algumas modificações no código, ficar novamente sem espaço na pilha, você poderá detectar uma série de bugs ou outros problemas (pilhas de chamadas muito profundas para um).

Não tenho 100% de certeza, mas acho que isso também pode ser feito.Se você tiver uma porta jtag exposta, poderá conectar-se ao Trace32 e verificar o uso máximo da pilha.Porém, para isso, você terá que fornecer um tamanho de pilha arbitrário inicial bem grande.

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