1. Introduction
Em sistemas embarcados, a organização da memória pode ser bem descrita pelo conhecido provérbio: "A place for everything, and everything in its place" (Um lugar para tudo, e tudo em seu lugar). Cada elemento do programa deve possuir um local bem definido na memória e no caso das variáveis, a escolha entre alocação estática ou dinâmica depende diretamente do contexto de aplicação e dos objetivos do sistema estabelecidos em fase de projeto
Embora a alocação dinâmica de memória seja amplamente utilizada em sistemas de propósito geral, é reconhecido que esse mecanismo introduz uma série de problemas quando aplicado a sistemas que impõem requisitos estritos de determinismo. Em sistemas determinísticos, espera-se que, para uma mesma sequência de entradas e condições iniciais, o software produza sempre a mesma sequência de estados internos e saídas observáveis, respeitando limites temporais conhecidos.
A literatura clássica sobre gerenciamento de memória aponta que a alocação dinâmica tradicional viola esses princípios ao introduzir comportamentos dependentes do histórico de execução, afetando tanto a previsibilidade temporal quanto o uso de memória. Esses efeitos são particularmente críticos em sistemas que utilizam RTOS (Real-Time Operating Systems), nos quais a capacidade de prever o comportamento em tempo de projeto é um requisito fundamental.
Neste artigo, são analisados e demonstrados, de forma prática em hardware real, efeitos colaterais amplamente documentados associados ao uso de alocação dinâmica de memória em ambientes de tempo real. Esses efeitos são observados por meio de experimentos simples e controlados, conduzidos com o sistema operacional FreeRTOS executando em um microcontrolador ARM Cortex-R5F (TI AM243x), utilizando inspeção direta da memória e dos metadados do heap por meio de debug.
2. Um pouco de teoria
A alocação dinâmica é adequada a sistemas de propósito geral, nos quais falhas de software normalmente não resultam em consequências graves. Isso inclui desktops e sistemas embarcados mais complexos que dispõem de uma MMU e executam um sistema operacional. Nesses sistemas, erros relacionados ao uso do heap (Espaço na memória destinado a este tipo de alocação) tendem a ser detectados e confinados pelo sistema operacional, por meio de mecanismos como isolamento de memória entre processos e tratamento de acessos inválidos, evitando que uma falha comprometa todo o sistema.
Nesses ambientes, essa estratégia de alocação de memória é utilizada para lidar com cargas de trabalho imprevisíveis em tempo de execução, resultantes da ampla gama de operações possíveis em sistemas de natureza generalista, como o uso de estruturas de dados de tamanho variável e a presença de múltiplos processos concorrentes. Em contraste, a alocação estática é a abordagem dominante em sistemas embarcados determinísticos e safety-critical (alta criticidade), como Software Level DAL-A (DO178C), nos quais a previsibilidade é um requisito fundamental e todo o comportamento possível em tempo de execução deve ser conhecido, analisado e controlado ainda em fase de projeto, sem espaço para surpresas durante a execução do sistema.
Um ponto importante a destacar em programas escritos em C é o destino dos dados na memória.
Objetos alocados estaticamente, como variáveis globais ou declaradas com
static, têm sua alocação direcionada pelo linker para regiões
fixas de memória, tipicamente nas seções .data (quando inicializados
com valores diferentes de zero) ou .bss (quando não inicializados ou
inicializados com zero), possuindo endereços determinados em tempo de projeto e
duração de vida equivalente à execução completa do software.
Variáveis automáticas ou locais, por sua vez, são alocadas na stack, com
criação e destruição implícitas associadas à entrada e saída de funções, oferecendo
previsibilidade temporal, porém com espaço limitado.
Já os dados alocados dinamicamente residem no heap, uma região de memória
gerenciada em tempo de execução pelo alocador da libc (por exemplo, por meio de
malloc()), cujo conteúdo e organização evoluem de acordo com o histórico
de alocações e liberações.
Diferentemente das regiões estáticas e da stack, o heap não possui layout
fixo nem duração de vida implícita para seus objetos, exigindo liberação explícita pelo programa e introduzindo variabilidade temporal, fragmentação e dependência do
estado global do sistema, características que tornam seu comportamento
intrinsecamente menos previsível em comparação às demais regiões de memória.
2.1 Exemplos de alocações
Alocação estática de memória
#define BUFFER_SIZE 128
static int buffer[BUFFER_SIZE];
void process(void)
{
buffer[0] = 42;
}
-
Através de
#define BUFFER_SIZE 128, é definida uma constante conhecida em tempo de compilação, permitindo ao compilador e ao linker calcular antecipadamente a quantidade exata de memória necessária para o armazenamento do vetor. -
Na declaração
static int buffer[BUFFER_SIZE];, o vetorbufferé alocado estaticamente, possuindo duração de armazenamento estática e ocupando uma região fixa da memória. Dependendo de sua inicialização, essa variável será posicionada pelo linker na seção.bss(se não inicializada) ou.data(se inicializada), conforme definido no linker script do sistema. -
Como consequência dessa alocação estática, o endereço de
bufferé resolvido em tempo de linkedição e permanece inalterado durante toda a execução do programa, não havendo qualquer chamada a funções de alocação dinâmica nem dependência do estado do heap. -
O acesso realizado em
buffer[0] = 42;corresponde a uma escrita direta em um endereço de memória válido e previamente reservado, não estando sujeito a falhas em tempo de execução, verificações de ponteiro ou variações decorrentes do histórico de execução do sistema. - Esse modelo de uso de memória permite que o consumo total de RAM e o tempo de execução associado ao acesso ao vetor sejam completamente conhecidos e analisados ainda em fase de projeto, viabilizando análises formais de uso de memória e de Worst-Case Execution Time (WCET), requisito fundamental em sistemas determinísticos e safety-critical.
Alocação dinâmica de memória
#include <stdlib.h>
void process(void)
{
int *buffer =
(int *)malloc(
128 * sizeof(int)
);
if (buffer != NULL)
{
buffer[0] = 42;
free(buffer);
}
}
-
A inclusão de
#include <stdlib.h>torna disponível a interface padrão de alocação dinâmica, em particular a funçãomalloc(), cuja implementação depende do runtime, do sistema operacional ou do RTOS utilizado. -
A expressão
malloc(128 * sizeof(int))solicita memória em tempo de execução, calculando dinamicamente a quantidade de bytes a ser alocada. Diferentemente da alocação estática, o tamanho do bloco e o momento da alocação são definidos apenas durante a execução do programa. -
A chamada a
malloc()consulta o estado interno do heap, procurando por um bloco livre suficientemente grande para atender à requisição, o que implica dependência direta do histórico de alocações e liberações realizadas anteriormente. -
A verificação
if (buffer != NULL)é necessária porque a alocação dinâmica pode falhar em tempo de execução, mesmo quando a quantidade total de memória livre é teoricamente suficiente, por exemplo em decorrência de fragmentação externa do heap. -
O acesso realizado em
buffer[0] = 42;somente é seguro após a confirmação do sucesso da alocação, evidenciando que o uso da memória depende de verificações adicionais e de caminhos condicionais no fluxo de execução. -
A chamada
free(buffer)devolve o bloco de memória ao heap, alterando novamente o estado interno do alocador e influenciando futuras requisições de memória, reforçando a dependência do comportamento do sistema em relação ao histórico de execução. - Como consequência, tanto o consumo de memória quanto o tempo de execução associado às operações de alocação e liberação não são totalmente previsíveis em fase de projeto, dificultando análises formais de uso de memória e de Worst-Case Execution Time (WCET), o que torna esse modelo mais adequado a sistemas de propósito geral do que a sistemas determinísticos ou safety-critical.
2.2 malloc() e free()
É importante mencionar que a linguagem C não possui uma storage class específica para alocação dinâmica de memória como ocorre com auto, static ou extern. A alocação dinâmica não é uma propriedade das variáveis da linguagem, sendo realizada por meio de alocadores, funções como o malloc(), disponibilizados pela biblioteca padrão C (libc).
Essa função gerencia uma região de heap fornecida pelo ambiente de execução. Em sistemas com sistema operacional completo, como ambientes baseados em Linux, uma chamada a malloc() é inicialmente tratada pelo alocador da libc em espaço de usuário, que consulta suas estruturas internas para localizar um bloco livre adequado. Caso o heap disponível não seja suficiente, o alocador recorre a system calls de baixo nível, como sbrk() ou mmap(), para solicitar ao kernel a criação ou a expansão de regiões de memória virtual associadas ao processo. A partir dessas regiões, a libc organiza e gerencia o heap da aplicação e, por fim, retorna à aplicação um ponteiro para um endereço virtual válido, correspondente a um bloco de memória reservado para uso em tempo de execução. Dessa forma, a utilização de malloc() pressupõe a existência de um sistema operacional e de um kernel capazes de fornecer esses serviços.
Em sistemas embarcados que não dispõem dessas interfaces de baixo nível — como sistemas bare-metal —, a alocação dinâmica não existe por padrão. Nesses casos, para que o malloc() funcione, é necessário implementar manualmente os chamados hooks de alocação, ou mesmo um gerenciador de memória customizado, geralmente a partir de regiões de heap definidas no linker script. Mesmo em sistemas embarcados que utilizam RTOS, a presença dessas system calls não é garantida. Nesses ambientes, quando a alocação dinâmica é suportada, ela costuma operar sobre uma região de heap estaticamente definida em tempo de projeto, reforçando seu caráter limitado e dependente do runtime.
O heap é organizado internamente como uma estrutura dinâmica baseada em uma lista ligada de regiões livres de memória. Cada região livre contém informações de controle armazenadas em seus próprios metadados, incluindo o tamanho total do bloco e uma referência para a próxima região livre adjacente em termos de endereçamento. Quando uma solicitação de alocação é realizada, o alocador percorre essa lista a partir do início do heap até encontrar um bloco livre com tamanho suficiente. Caso o bloco selecionado seja maior do que o necessário, ele é dividido em duas partes: uma porção é marcada como alocada e entregue à aplicação, enquanto a porção remanescente permanece livre e é reinserida na lista ligada como um novo elemento. À medida que alocações e liberações ocorrem, a lista de regiões livres evolui continuamente, podendo conter múltiplos blocos não contíguos, o que evidencia a natureza dinâmica do heap e sua dependência direta do histórico de uso da memória ao longo da execução do sistema.
Os metadados do heap são estruturas internas mantidas pelo alocador para descrever o tamanho, o estado e a relação entre blocos de memória dinâmica. Essas informações são constantemente atualizadas durante chamadas a malloc() e free(), permitindo operações como divisão e fusão de blocos. Na prática, uma requisição como malloc(128) não corresponde à ocupação exata de 128 bytes no heap. O alocador precisa reservar um bloco maior, capaz de armazenar também seus metadados internos e de atender aos requisitos de alinhamento da arquitetura. Conceitualmente, o layout efetivo de um bloco alocado pode ser representado da seguinte forma:
[ metadados do alocador ][ 128 bytes solicitados ][ padding / alinhamento ]
Dessa forma, o alocador não busca um bloco livre de exatamente 128 bytes, mas sim um bloco contíguo suficientemente grande para acomodar toda essa estrutura. Como consequência, mesmo que existam 128 bytes livres no heap, a alocação pode falhar caso não exista um bloco contíguo que comporte os metadados e o alinhamento necessários, evidenciando tanto a fragmentação interna — causada pelo espaço adicional não utilizável pela aplicação — quanto a fragmentação externa, resultante da dispersão de blocos livres no heap. Esse mecanismo introduz sobrecarga de memória, dependência do histórico de execução e variabilidade temporal no comportamento do alocador.
Após a alocação dinâmica de memória é necessária para devolver ao alocador um bloco previamente obtido por meio de malloc(), permitindo que essa região volte a ser marcada como livre e possa ser reutilizada em futuras alocações. Sem essa liberação explícita, o bloco permanece permanentemente ocupado, reduzindo progressivamente o espaço disponível no heap e podendo levar a falhas de alocação em sistemas de longa duração. Em ambientes embarcados e de tempo real, onde o heap é limitado e não há recuperação automática de memória, a omissão de free() resulta inevitavelmente em vazamentos de memória e degradação do comportamento do sistema.
Problemas da alocação dinâmica em sistemas determinísticos
Em sistemas determinísticos, espera-se que o comportamento do software seja completamente previsível, tanto em termos temporais quanto espaciais, permitindo que todos os cenários relevantes sejam analisados ainda em fase de projeto. A alocação dinâmica de memória entra em conflito direto com esse princípio, pois introduz decisões e estados que só podem ser resolvidos em tempo de execução.
Diferentemente da alocação estática, na qual o layout de memória é fixo e conhecido,
o uso de malloc() depende do estado global do heap no momento da chamada.
Esse estado é resultado do histórico de alocações e liberações anteriores, tornando
impossível garantir que duas execuções do mesmo código, sob as mesmas entradas
funcionais, apresentem comportamento idêntico em termos de uso de memória e tempo
de execução.
Além disso, o custo temporal das operações de alocação e liberação não é constante. O alocador pode precisar percorrer estruturas internas, dividir ou fundir blocos e, em alguns casos, solicitar novas regiões de memória ao ambiente de execução. Essa variabilidade inviabiliza a determinação confiável do Worst-Case Execution Time (WCET), requisito essencial em sistemas de tempo real determinísticos e safety-critical.
Também podem ocorrer fragmentações interna e externa no heap. A fragmentação interna ocorre quando o alocador precisa reservar um bloco maior do que o tamanho efetivamente solicitado pela aplicação, seja devido a requisitos de alinhamento ou à granularidade mínima de alocação, fazendo com que parte da memória dentro do bloco alocado permaneça inutilizada. Já a fragmentação externa manifesta-se quando a memória livre encontra-se distribuída em múltiplos blocos não contíguos intercalados entre regiões ocupadas, impossibilitando novas alocações que exijam um bloco contínuo de determinado tamanho. Nessa situação, mesmo quando a quantidade total de memória livre é suficiente, a ausência de regiões contíguas adequadas pode levar a falhas de alocação em tempo de execução. Esses efeitos não podem ser previstos ou eliminados apenas por análise estática, pois dependem diretamente da evolução dinâmica do heap ao longo da execução do sistema.
Por fim, a necessidade de liberação explícita via free() transfere para
a aplicação a responsabilidade de gerenciar corretamente a duração de vida dos
objetos dinâmicos. Erros como vazamentos de memória, liberações múltiplas ou uso de
ponteiros inválidos introduzem comportamentos incorretos difíceis de detectar e
reproduzir, especialmente em sistemas de longa duração. Em conjunto, esses fatores
tornam a alocação dinâmica incompatível com os requisitos de previsibilidade,
analisabilidade e confiabilidade exigidos por sistemas determinísticos.
3. Ambiente Experimental
Esta seção descreve o ambiente experimental utilizado para a condução dos testes apresentados neste trabalho, incluindo a plataforma de hardware, o sistema operacional de tempo real, a configuração do heap e as ferramentas de desenvolvimento e debug empregadas. O objetivo é garantir reprodutibilidade e contextualizar os resultados observados nas seções subsequentes.
3.1 Plataforma de hardware
Os experimentos foram conduzidos em uma placa de desenvolvimento baseada no microcontrolador TI AM243x, equipada com um núcleo ARM Cortex-R5F. Essa arquitetura foi escolhida por ser amplamente utilizada em aplicações industriais e embarcadas de tempo real, oferecendo comportamento determinístico, suporte a debug via JTAG e execução direta em memória RAM.
O Cortex-R5F opera sem suporte a memória virtual, utilizando endereçamento físico direto. Essa característica permite observar com precisão o layout da memória e a evolução do heap ao longo da execução do sistema, sem interferência de mecanismos de tradução de endereços ou paginação.
3.2 Sistema operacional de tempo real
O sistema operacional adotado foi o FreeRTOS, amplamente utilizado em sistemas embarcados de tempo real. O FreeRTOS fornece múltiplas implementações de alocadores de heap, permitindo a configuração explícita da estratégia de gerenciamento de memória dinâmica utilizada pelo sistema.
Para os experimentos descritos neste artigo, foi utilizada a implementação
heap_4, que organiza o heap como uma lista ligada de blocos livres,
com suporte à divisão de blocos e coalescência de regiões adjacentes quando possível.
3.3 Configuração do heap
O heap do FreeRTOS foi definido estaticamente em tempo de projeto por meio do
parâmetro configTOTAL_HEAP_SIZE. A região de heap reside inteiramente
em memória RAM contínua, com limites fixos e conhecidos, conforme especificado no
linker script do projeto.
Durante os experimentos, todas as alocações dinâmicas foram realizadas por meio da
API pvPortMalloc() e liberadas com vPortFree(). Essa abordagem
permite observar diretamente o comportamento interno do alocador, incluindo a
modificação de metadados, a reorganização da lista ligada de blocos livres e os
efeitos de fragmentação interna e externa.
3.4 Ambiente de desenvolvimento e ferramentas de debug
O desenvolvimento e o debug do sistema foram realizados utilizando o Code Composer Studio (CCS), versão 20.3.0, ambiente oficial da Texas Instruments para desenvolvimento de software embarcado. O CCS foi utilizado tanto para compilação quanto para análise em tempo de execução.
O debug foi conduzido por meio de interface JTAG, permitindo a inspeção direta da memória RAM. Em particular, foi utilizado o memory browser do depurador para monitorar a região correspondente ao heap do FreeRTOS durante a execução das chamadas de alocação e liberação.
Foram observados diretamente os endereços retornados pelas chamadas a
pvPortMalloc(), a disposição dos blocos alocados e livres, bem como
a evolução dos metadados que implementam a lista ligada interna do heap.
Essas observações foram realizadas sem instrumentação adicional que pudesse
alterar o comportamento do sistema.
3.5 Considerações e limitações
Os experimentos apresentados não têm como objetivo caracterizar desempenho temporal ou estabelecer limites de tempo de execução das operações de alocação. O foco do estudo é exclusivamente estrutural, voltado à compreensão do funcionamento interno do heap e dos efeitos colaterais da alocação dinâmica de memória em sistemas de tempo real.
Os resultados devem, portanto, ser interpretados como representativos do comportamento estrutural da alocação dinâmica em sistemas embarcados determinísticos, e não como uma avaliação de desempenho absoluto ou de tempo de execução.
4. Avaliação Experimental
Esta seção descreve os experimentos conduzidos em hardware real com o sistema operacional de tempo real FreeRTOS executando em um microcontrolador TI AM243x (ARM Cortex-R5F). O objetivo é observar diretamente, por meio de debug e inspeção da memória, o comportamento interno do heap e os efeitos colaterais associados à alocação dinâmica de memória, conforme discutido na fundamentação teórica.
4.1 Experimento 1 — Evolução incremental do heap
Objetivo. Analisar como o heap evolui ao longo de chamadas sucessivas de alocação, evidenciando a modificação progressiva de seus metadados e a dependência do histórico de alocações.
Metodologia.
Foram realizadas chamadas sequenciais a pvPortMalloc() com tamanhos
distintos, sem liberações intermediárias. Após cada chamada, a região de memória
correspondente ao heap foi inspecionada por meio do memory browser,
permitindo a observação direta dos metadados e da reorganização interna do heap.
4.2 Experimento 2 — Alocação abaixo do tamanho mínimo
Objetivo. Evidenciar a granularidade mínima de alocação do heap e o desperdício de memória associado a solicitações inferiores a esse limite.
Metodologia.
Foi realizada uma chamada a pvPortMalloc() solicitando um número de
bytes inferior ao tamanho mínimo suportado pelo alocador. O layout da memória
foi novamente inspecionado via debug após a alocação.
4.3 Experimento 3 — Liberação de bloco intermediário
Objetivo. Analisar o efeito da liberação de um bloco intermediário na estrutura do heap e a formação de regiões livres não contíguas.
Metodologia.
Após a realização de múltiplas alocações consecutivas, um bloco intermediário
foi liberado por meio da chamada a vPortFree(). O estado do heap foi
inspecionado imediatamente após a liberação.
4.4 Experimento 4 — Reutilização de bloco livre e fragmentação interna
Objetivo. Demonstrar como o alocador reutiliza blocos previamente liberados e sob quais condições a divisão desses blocos não ocorre.
Metodologia.
Após a liberação de um bloco de tamanho maior, foi realizada uma nova chamada
a pvPortMalloc() solicitando um tamanho inferior ao bloco livre
disponível. O comportamento do alocador foi analisado por inspeção direta
dos metadados.
5. Resultados e Análise Experimental
5.1 Experimento 1 — Entendendo o funcionamento do Heap
Ao realizar chamadas sucessivas a pvPortMalloc(), observa-se via debug que o estado do heap evolui continuamente. Antes da primeira chamada, o heap está completamente livre. Após a alocação de 32 bytes, nota-se a alteração no primeiro bloco de metadados (a estrutura BlockLink_t). O endereço retornado para a aplicação é o início do segundo bloco (o payload), e observa-se a modificação em duas words subsequentes que marcam o próximo pxNextFreeBlock e o xBlockSize remanescente.
Na segunda chamada, o bloco alocado é posicionado logo após o anterior. O alocador localiza o espaço livre porque o heap se comporta como uma lista ligada (linked list). O valor que marca o início do próximo bloco livre é movido para o final deste novo bloco alocado. Um ponto importante observado no debug é que o xBlockSize gravado nos metadados é sempre maior que o solicitado, pois inclui o tamanho da BlockLink_t e o alinhamento portBYTE_ALIGNMENT evidenciando a presença de metadados.
Na terceira chamada, o processo se repete: o ponteiro de início do bloco livre avança novamente. O debug mostra que o heap deixa posições de memória "vazias" entre os blocos para manter o alinhamento da arquitetura Cortex-R5, o que já introduz uma forma sutil de fragmentação e ao mesmo tempo moveu o ponteiro que marca o inicio do bloco livre para frente.
5.1 Experimento 2 - Malloc de um valor menor que o mínimo
Neste experimento, solicitou-se um valor abaixo do mínimo permitido pelo heap_4. O resultado foi a ocupação de 2 words (8 bytes), resultando em um desperdício imediato. Isso ocorre devido à macro heapMINIMUM_BLOCK_SIZE, que impede que blocos livres fiquem tão pequenos que não consigam guardar seus próprios metadados no futuro. Este fenômeno caracteriza a fragmentação interna: a memória está alocada para o ponteiro, mas é inutilizável pela aplicação.
5.1 Experimento 3 - Liberação de bloco alocado intermediário com free()
Ao liberar um bloco central (p2), o alocador executa a função prvInsertBlockIntoFreeList(). O campo pxNextFreeBlock do bloco anterior é atualizado para apontar para este novo espaço vago. No debug, nota-se que não há movimentação de dados; o bloco apenas "entra" de volta na lista ligada. Como os blocos vizinhos estão ocupados, não ocorre a coalescência, criando uma região livre isolada. Isso é a fragmentação externa: o espaço existe, mas está "preso" entre blocos ocupados.
5.1 Experimento 4 - novo malloc() solicitando espaço de 48 bytes
Na tentativa de alocar 48 bytes, o alocador reutilizou o bloco de 64 bytes deixado por p2. Embora houvesse uma "sobra" de 16 bytes, o heap_4.c decidiu não dividir o bloco (splitting). Isso ocorre porque a sobra seria menor ou igual ao heapMINIMUM_BLOCK_SIZE, o que tornaria impossível criar um novo nó de lista ligada válido ali. O bloco completo foi entregue à aplicação, gerando mais um caso de fragmentação interna e provando o indeterminismo espacial do heap.
6. Conclusão
Os experimentos em hardware real validaram que a implementação heap_4 do FreeRTOS prioriza a eficiência e o baixo overhead através de uma lista ligada de blocos livres, porém ainda sim existe uma quaintdade grande de overhead entre operações sucessivas. A inspeção direta da memória revelou que o alinhamento de 8 bytes e a estrutura BlockLink_t impõem um consumo de RAM superior ao solicitado, resultando em fragmentação interna inerente ao design do alocador.
A análise prática de vPortFree() e das alocações subsequentes demonstrou que a fragmentação externa e a ausência de divisão (splitting) de blocos pequenos tornam o layout da memória altamente dependente do histórico de execução. Esse comportamento confirma que o heap_4 não oferece garantias estritas de previsibilidade espacial e temporal.
Em suma, este estudo reforça que, embora robusto, o uso de alocação dinâmica em sistemas de tempo real crítico exige cautela. O indeterminismo observado justifica a preferência por alocação estática ou memory pools em projetos certificados, onde a variabilidade introduzida pela fragmentação representa um risco ao determinismo do sistema.
Code Availability
O código-fonte do experimento, está disponível no GitHub: