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.