Pilha e pilha são conceitos de software, não conceitos de hardware. O que o hardware fornece é memória. Definir zonas de memória, uma das quais é chamada de "pilha" e outra é chamada de "pilha", é uma opção do seu programa.
O hardware ajuda nas pilhas. A maioria das arquiteturas possui um registro dedicado chamado ponteiro da pilha. Seu uso pretendido é que, quando o programa faz uma chamada de função, os parâmetros da função e o endereço de retorno são enviados para a pilha e são exibidos quando a função termina e retorna ao chamador. Pressionar a pilha significa escrever no endereço fornecido pelo ponteiro da pilha e diminuir o ponteiro da pilha de acordo (ou incrementar, dependendo da direção em que a pilha cresce). Estalar significa aumentar (ou diminuir) o ponteiro da pilha; o endereço de retorno é lido a partir do endereço fornecido pelo ponteiro da pilha.
Algumas arquiteturas (embora não o ARM) possuem uma instrução de chamada de sub-rotina que combina um salto com a gravação no endereço fornecido pelo ponteiro da pilha e uma instrução de retorno de sub-rotina que combina a leitura do endereço fornecido pelo ponteiro da pilha e o salto para esse endereço. No ARM, o salvamento e a restauração de endereços são feitos no registro LR, as instruções de chamada e retorno não usam o ponteiro da pilha. No entanto, existem instruções para facilitar a gravação ou a leitura de vários registros no endereço fornecido pelo ponteiro da pilha, para pressionar e exibir argumentos da função.
Para escolher o tamanho da pilha e da pilha, a única informação relevante do hardware é a quantidade total de memória que você possui. Você faz sua escolha, dependendo do que deseja armazenar na memória (permitindo código, dados estáticos e outros programas).
Um programa normalmente usa essas constantes para inicializar alguns dados na memória que serão usados pelo restante do código, como o endereço da parte superior da pilha, talvez um valor em algum lugar para verificar se há excesso de pilha, limites para o alocador de heap etc.
No código que você está visualizando , a Stack_Size
constante é usada para reservar um bloco de memória na área de código (por meio de uma SPACE
diretiva no assembly ARM). O endereço superior desse bloco recebe o rótulo __initial_sp
e é armazenado na tabela de vetores (o processador usa essa entrada para definir o SP após uma redefinição do software), além de ser exportado para uso em outros arquivos de origem. A Heap_Size
constante é similarmente usada para reservar um bloco de memória e os rótulos para seus limites ( __heap_base
e __heap_limit
) são exportados para uso em outros arquivos de origem.
; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Stack_Size EQU 0x00000400
AREA STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem SPACE Stack_Size
__initial_sp
; <h> Heap Configuration
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>
Heap_Size EQU 0x00000200
AREA HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem SPACE Heap_Size
__heap_limit
…
__Vectors DCD __initial_sp ; Top of Stack
DCD Reset_Handler ; Reset Handler
DCD NMI_Handler ; NMI Handler
…
EXPORT __initial_sp
EXPORT __heap_base
EXPORT __heap_limit
Os tamanhos da pilha e da pilha são definidos pelo seu aplicativo, e não em nenhum lugar da folha de dados do microcontrolador.
A pilha
A pilha é usada para armazenar os valores das variáveis locais dentro das funções, os valores anteriores dos registradores da CPU usados para as variáveis locais (para que possam ser restaurados na saída da função), o endereço do programa para retornar ao sair dessas funções, além de algumas despesas gerais para o gerenciamento da própria pilha.
Ao desenvolver um sistema incorporado, você estima a profundidade máxima de chamadas que espera ter, soma os tamanhos de todas as variáveis locais nas funções nessa hierarquia e, em seguida, adiciona algum preenchimento para permitir a sobrecarga mencionada acima e depois adiciona um pouco mais para quaisquer interrupções que possam ocorrer durante a execução do seu programa.
Um método de estimativa alternativo (onde a RAM não é restrita) é alocar muito mais espaço da pilha do que você precisará, preencher a pilha com um valor sentinela e monitorar o quanto você realmente usa durante a execução. Vi versões de depuração dos tempos de execução da linguagem C que farão isso automaticamente para você. Então, quando você terminar de desenvolver, poderá reduzir o tamanho da pilha, se desejar.
A pilha
Calcular o tamanho da pilha de que você precisa pode ser mais complicado. A pilha é usada para variáveis dinamicamente alocados, por isso, se você usa
malloc()
efree()
em um programa em linguagem C, ounew
edelete
em C ++, que é onde essas variáveis viver.No entanto, especialmente em C ++, pode haver alguma alocação de memória dinâmica oculta em andamento. Por exemplo, se você tiver objetos alocados estaticamente, a linguagem exigirá que seus destruidores sejam chamados quando o programa sair. Estou ciente de pelo menos um tempo de execução em que os endereços dos destruidores são armazenados em uma lista vinculada alocada dinamicamente.
Portanto, para estimar o tamanho do heap necessário, observe toda a alocação dinâmica de memória em cada caminho na árvore de chamadas, calcule o máximo e adicione um pouco de preenchimento. O tempo de execução do idioma pode fornecer diagnósticos que você pode usar para monitorar o uso total de heap, fragmentação etc.
fonte
Além das outras respostas, gostaria de acrescentar que, ao acumular RAM entre a pilha e o espaço de pilha, você também precisa considerar o espaço para dados estáticos não constantes (por exemplo, arquivos globais, estática de funções e todo o programa globais da perspectiva C e provavelmente outros para C ++).
Como a alocação de pilha / heap funciona
Vale notar que o arquivo de montagem de inicialização é uma maneira de definir a região; a cadeia de ferramentas (seu ambiente de construção e ambiente de tempo de execução) se preocupa principalmente com os símbolos que definem o início do espaço de pilha (usado para armazenar o ponteiro inicial da pilha na Tabela de vetores) e o início e o fim do espaço de heap (usado pela dinâmica alocador de memória, normalmente fornecido pela libc)
No exemplo do OP, apenas 2 símbolos são definidos, um tamanho de pilha em 1kiB e um tamanho de heap em 0B. Esses valores são usados em outros lugares para realmente produzir os espaços de pilha e heap
No exemplo do @Gilles, os tamanhos são definidos e usados no arquivo de montagem para definir um espaço de pilha iniciando em qualquer lugar e com duração do tamanho, identificado pelo símbolo Stack_Mem e define um rótulo __initial_sp no final. Da mesma forma para a pilha, onde o espaço é o símbolo Heap_Mem (tamanho de 0,5 kB), mas com rótulos no início e no fim (__heap_base e __heap_limit).
Eles são processados pelo vinculador, que não alocará nada no espaço de pilha e no espaço de pilha porque essa memória está ocupada (pelos símbolos Stack_Mem e Heap_Mem), mas pode colocar essas memórias e todos os globais onde for necessário. As etiquetas acabam sendo símbolos sem tamanho nos endereços fornecidos. O __initial_sp é usado diretamente para a tabela de vetores no momento do link, e o __heap_base e __heap_limit pelo seu código de tempo de execução. Os endereços reais dos símbolos são atribuídos pelo vinculador com base em onde os colocou.
Como mencionei acima, esses símbolos não precisam vir de um arquivo startup.s. Eles podem vir da configuração do vinculador (arquivo Scatter Load no Keil, linkerscript no GNU), e naqueles você pode ter um controle mais refinado sobre o posicionamento. Por exemplo, você pode forçar a pilha a estar no início ou no final da RAM, ou manter seus globais afastados da pilha, ou o que quiser. Você pode até especificar que o HEAP ou STACK apenas ocupe a RAM restante após a colocação dos globais. OBSERVE, porém, que você deve ter cuidado para adicionar mais variáveis estáticas que sua outra memória diminuirá.
No entanto, cada cadeia de ferramentas é diferente, e como gravar o arquivo de configuração e quais símbolos o seu alocador de memória dinâmico usará terão de vir da documentação do seu ambiente específico.
Dimensionamento da pilha
Quanto à forma de determinar o tamanho da pilha, muitas cadeias de ferramentas podem fornecer uma profundidade máxima da pilha analisando as árvores de chamadas de funções do seu programa, SE você não usa ponteiros de função ou recursão. Se você usá-los, estimando o tamanho de uma pilha e preenchendo-o previamente com valores cardinais (talvez através da função de entrada antes de main) e depois verificando depois que o programa foi executado por um tempo onde estava a profundidade máxima (onde estão os valores cardinais fim). Se você tiver exercitado seu programa totalmente até o limite, saberá com bastante precisão se pode reduzir a pilha ou, se o programa falhar ou se nenhum valor cardinal for deixado, é necessário aumentar a pilha e tentar novamente.
Dimensionamento da pilha
Determinar o tamanho do heap depende um pouco mais do aplicativo. Se você apenas fizer alocação dinâmica durante a inicialização, poderá adicionar o espaço necessário no seu código de inicialização (mais algumas despesas gerais para o gerenciamento de memória). Se você tiver acesso à fonte do seu gerenciador de memória, poderá saber exatamente qual é a sobrecarga e, possivelmente, até escrever código para percorrer a memória e fornecer informações de uso. Para aplicativos que precisam de memória de tempo de execução dinâmico (por exemplo, alocar buffers para quadros Ethernet de entrada), o melhor que posso sugerir é aprimorar com cuidado o tamanho da sua pilha e fornecer ao Heap tudo o que resta depois da pilha e das estáticas.
Nota final (RTOS)
A pergunta do OP foi marcada para bare-metal, mas quero adicionar uma observação para RTOSes. Freqüentemente (sempre?) Cada tarefa / processo / encadeamento (apenas escreverei aqui a tarefa por simplicidade) receberá um tamanho de pilha quando a tarefa for criada, além de pilhas de tarefas, provavelmente haverá um pequeno SO pilha (usada para interrupções e afins)
As estruturas de contabilidade de tarefas e as pilhas precisam ser alocadas de algum lugar, e isso geralmente ocorre no espaço de heap geral do seu aplicativo. Nesses casos, o tamanho inicial da pilha geralmente não importa, porque o sistema operacional o utilizará apenas durante a inicialização. Vi, por exemplo, especificar que TODO o espaço restante durante a vinculação seja alocado ao HEAP e colocar o ponteiro de pilha inicial no final do heap para crescer no heap, sabendo que o SO será alocado a partir do início do heap e alocará a pilha do SO pouco antes de abandonar a pilha initial_sp. Todo o espaço é usado para alocar pilhas de tarefas e outras memórias alocadas dinamicamente.
fonte