01 · El panorama
Una vista general del mapa
Los temas 7 y 8 mostraron las piezas: el enlazador agrupa el código compilado en secciones, el linker script las asigna a regiones de memoria y el código de arranque copia .data y limpia .bss. Este tema junta todo en una sola imagen, la del mapa de memoria del firmware, y agrega las dos zonas que no aparecen en el archivo objeto porque se crean en tiempo de ejecución: la pila y el heap.
Cuatro cosas conviene leer en esta figura antes de seguir:
- Hay dos mapas, uno por memoria, y se llenan con reglas distintas. La flash se llena de abajo hacia arriba con lo que el enlazador coloca. La RAM se llena desde ambos extremos: lo estático desde abajo, la pila desde el tope.
- Lo estático (
.datay.bss) tiene tamaño conocido al enlazar. El enlazador lo sabe y puede avisar si no entra. - El heap y la pila no tienen tamaño conocido al enlazar: crecen en tiempo de ejecución, uno hacia el otro, y comparten el hueco que quedó. Por eso son el origen de los errores más difíciles de memoria.
.dataexiste dos veces: una copia con los valores iniciales en la flash y la ubicación definitiva en la RAM. El arranque (tema 8) copia una sobre la otra.
02 · El vocabulario
Las secciones, una por una
Cada declaración en C produce contenido en alguna sección. Esta tabla es la que conviene saber de memoria:
| Sección | Contenido | Memoria | Ejemplo en C |
|---|---|---|---|
.isr_vector | Tabla de vectores: pila inicial y rutinas de interrupción | Flash | El arreglo de punteros del tema 8 |
.text | Instrucciones de las funciones | Flash | void led_alternar(void) {...} |
.rodata | Datos de solo lectura: constantes, tablas y literales | Flash | const uint8_t senos[256], "Error" |
.data | Variables globales y estáticas con valor inicial distinto de cero | RAM (con copia en flash) | int umbral = 50; |
.bss | Variables globales y estáticas sin valor inicial, o con valor cero | RAM (no ocupa flash) | uint16_t lecturas[100]; |
.noinit | Variables que el arranque no toca | RAM | Con un atributo de sección (apartado 8) |
| Heap | Memoria dinámica pedida con malloc | RAM (no es una sección del ejecutable) | uint8_t *p = malloc(64); |
| Pila | Variables locales, direcciones de retorno, registros guardados | RAM (no es una sección del ejecutable) | int i; dentro de una función |
Dos cosas de esta tabla sorprenden la primera vez. La primera: .bss no ocupa espacio en la flash. Un arreglo de 4 KB sin inicializar agrega 4 KB al uso de RAM pero cero bytes al archivo binario, porque el programa de arranque lo llena de ceros y no hay nada que guardar. En cambio, un arreglo de 4 KB con valores iniciales agrega 4 KB a la RAM y 4 KB a la flash (la copia). La segunda: el heap y la pila no están en el ejecutable. No hay ninguna sección que los contenga: son zonas de la RAM que el programa usa mientras corre.
Algunos casos que confunden, resueltos de una vez:
int g_valor = 7; /* .data : tiene valor inicial distinto de cero */
int g_cero = 0; /* .bss : el valor cero no se guarda, se genera al limpiar la zona */
int g_sin; /* .bss : sin valor inicial */
const int k_limite = 5; /* .rodata : constante, vive solo en la flash */
static int s_cuenta; /* .bss : estática de archivo */
char *p_texto = "hola"; /* p_texto (4 bytes) en .data; el texto "hola" (5 bytes) en .rodata */
char a_texto[] = "hola"; /* a_texto (5 bytes) en .data: una COPIA modificable del literal */
const char c_texto[] = "hola"; /* c_texto (5 bytes) en .rodata: solo lectura */
void f(void) {
int local; /* pila (o un registro) */
static int veces; /* .bss: una estática local vive para siempre, no en la pila */
int *dinamico = malloc(16); /* 'dinamico' en la pila; los 16 bytes a los que apunta, en el heap */
}
La diferencia entre p_texto y a_texto vale la pena recordarla: con el puntero, el texto queda en la flash y ocupa 5 bytes más 4 del puntero; con el arreglo, el texto se copia a la RAM y ocupa 5 bytes de RAM (más los 5 de la copia en flash). Para un mensaje que no se modifica, el puntero a const char es lo que corresponde.
03 · Dos direcciones para una sección
La flash: LMA y VMA
El enlazador maneja dos direcciones para cada sección, y la distinción es la clave para entender .data:
| Sigla | Nombre | Significado | Para .data del ejemplo |
|---|---|---|---|
| VMA | Virtual Memory Address (dirección de ejecución) | Dónde estará la sección cuando el programa corra; es la dirección que el código usa para acceder a las variables | 0x20000000 (en la RAM) |
| LMA | Load Memory Address (dirección de carga) | Dónde se guarda la sección en el archivo y en la flash, para poder cargarla | 0x08000EF4 (en la flash) |
Para .text y .rodata, VMA y LMA coinciden: el código se ejecuta y se lee donde está guardado. Para .data son distintas, y por eso el script usa > RAM AT> FLASH: «ubicala en la RAM (VMA) pero guardala en la flash (LMA)». El programa de arranque (tema 8) completa el puente copiando de la LMA a la VMA, con los símbolos _sidata (origen), _sdata y _edata (destino).
De ahí sale un cálculo importante, el que hay que repetir cada vez que se piensa en cuánto ocupa un programa:
| Memoria | Fórmula | Con el ejemplo del tema 7 |
|---|---|---|
| Flash usada | .isr_vector + .text + .rodata + .data (la copia) | 0x130 + 0xB4C + 0x278 + 0x18 = 3852 bytes (5,9 % de 64 KB) |
| RAM estática | .data + .bss | 0x18 + 0x158 = 368 bytes (1,8 % de 20 KB) |
| RAM disponible para heap y pila | RAM total − RAM estática | 20 480 − 368 = 20 112 bytes |
Observá que .data aparece en las dos filas: ocupa lugar en la flash y en la RAM. Y que ni el heap ni la pila figuran en esa cuenta: lo que sobra es lo que se disputan.
04 · El reparto de la RAM
La RAM: estática, heap y pila
La RAM se llena de ambos lados, y entre las dos zonas queda un hueco que ninguno de los dos «posee»:
- Desde la dirección más baja hacia arriba: primero
.data, luego.bss(y.noinitsi existe). Después viene el heap, que empieza justo donde termina lo estático (el símbolo_endoend) y crece hacia direcciones altas cada vez quemallocpide más. - Desde la dirección más alta hacia abajo: la pila. Su punto de partida es el símbolo
_estack(el tope de la RAM), que el hardware carga en el puntero de pila al arrancar (palabra 0 de la tabla de vectores). Cada llamada a una función y cada variable local la hacen bajar. - En el medio: el hueco libre. Mientras el heap crece hacia arriba y la pila baja, el hueco se achica. Si se cruzan, hay un desastre silencioso: cada uno pisa los datos del otro.
¿Por qué esta disposición, con dos zonas que crecen una hacia la otra? Porque nadie sabe de antemano cuál va a necesitar más. Un programa con muchas llamadas anidadas y casi sin malloc usa pila; uno con listas dinámicas y llamadas superficiales usa heap. Si cada una tuviera una zona fija, habría que dimensionar las dos para el peor caso de cada una, desperdiciando RAM. Con el hueco compartido, lo que una no usa lo aprovecha la otra. El precio es que nada impide el cruce.
En los AVR la idea es la misma con otras direcciones: la RAM de un ATmega328P va de 0x0100 a 0x08FF (los registros y los periféricos ocupan las direcciones anteriores), .data y .bss están abajo, el heap arranca en __heap_start y la pila parte de RAMEND y baja. Lo que cambia son los nombres y el hecho de que cada byte vale mucho más.
05 · Lo que crece sin que lo pidas
La pila en detalle
La pila es una región de la RAM manejada con un puntero, el SP (stack pointer). En los Cortex-M es «completa y descendente»: el SP apunta al último dato apilado, y apilar significa restar primero al SP y después escribir. Cada llamada a función crea sobre ella un marco (stack frame) con:
- La dirección de retorno (en los ARM, el registro de enlace
LRguardado). - Los registros que la función debe preservar si los usa.
- Las variables locales que no entraron en registros, incluyendo arreglos locales.
- Los argumentos que no entraron en registros, y el relleno de alineación (a 8 bytes en ARM).
Al volver, el marco se descarta restaurando el SP. No hay nadie que lo limpie: los bytes quedan con su contenido, y por eso una variable local sin inicializar contiene basura (tema 8).
De qué depende cuánta pila se usa
La profundidad máxima de la pila depende de tres factores que se suman, y que conviene separar para estimarlos:
- El camino de llamadas más profundo del programa principal: la suma de los marcos de las funciones anidadas en el peor camino. Una función que declara
char buf[256]local cuesta 256 bytes más sus propios marcos, mientras esté activa. - Las interrupciones. Cuando llega una, el hardware apila automáticamente un marco de excepción de 8 palabras (R0 a R3, R12, LR, PC y xPSR: 32 bytes; con coma flotante y apilado perezoso puede llegar a 104 bytes), y luego se ejecuta la rutina con su propio marco. Si pueden anidarse (prioridades, tema 15), se suman. El peor caso es una interrupción que llega justo cuando el programa estaba en su punto más profundo.
- La recursión. Cada llamada recursiva agrega un marco completo. Una función recursiva con 32 bytes de marco y profundidad 100 necesita 3200 bytes: una profundidad que depende de los datos de entrada, y que por eso casi siempre está prohibida en firmware (tema 39).
Si el sistema usa un RTOS (tema 20), la cuenta se multiplica: cada tarea tiene su propia pila, reservada en la RAM al crearla, además de la pila principal que usan el arranque y las interrupciones. Con 6 tareas de 512 bytes, son 3 KB solo para pilas, y es una de las razones por las que los RTOS ocupan más RAM de lo que parece.
Qué pasa cuando se agota
Cuando la pila baja más de lo que debía, no hay excepción ni mensaje: el SP simplemente apunta a una zona que pertenece a otra cosa. Escribir un marco ahí corrompe lo que había (el heap, variables de .bss o .data si no hay nada en el medio) y el programa se comporta de manera extraña, a veces mucho después y lejos de la causa. Los síntomas típicos: valores de variables que cambian solos, funciones que retornan a lugares absurdos, fallos duros (HardFault) intermitentes. El tema 31 trata la detección; por ahora lo importante es la prevención: estimar y medir.
$ arm-none-eabi-gcc -c -fstack-usage -Os sensor.c
$ cat sensor.su
sensor.c:14:6:leer_sensor 32 static
sensor.c:27:6:formatear 64 static
sensor.c:41:6:procesar_buffer 144 dynamic,bounded
La opción -fstack-usage del compilador genera un archivo .su con el marco de cada función: static significa que el tamaño es fijo y conocido; dynamic, que depende de datos (por ejemplo, un arreglo de largo variable), y bounded que, aun así, tiene un tope. Con esas cifras y el grafo de llamadas se calcula el peor camino, exactamente como hace la Parte 2 del programa de Python de este tema. Es una estimación que nunca reemplaza a la medición real, pero detecta los problemas gruesos antes de ejecutar.
06 · La memoria que se pide en marcha
El heap en detalle
El heap es la zona de la RAM desde donde malloc entrega bloques. En un microcontrolador con la biblioteca newlib, el mecanismo es simple y vale la pena conocerlo: malloc administra bloques, pero cuando se queda sin espacio le pide más al sistema con la función _sbrk, que mueve hacia arriba un único puntero (el «tope del heap») que empieza en _end. El tema 30 trata a fondo malloc, la fragmentación y las alternativas; acá importa el aspecto del mapa: quién impide que ese puntero llegue a la pila.
La respuesta honesta es que, en la versión por defecto, nadie. Una implementación mínima y razonable de _sbrk sí lo comprueba:
/* syscalls.c: _sbrk consciente de la pila (para newlib) */
extern char _end; /* primer byte libre después de .bss: lo define el linker script */
#define MARGEN_PILA 256 /* bytes que se dejan siempre para la pila */
void *_sbrk(ptrdiff_t incremento) {
static char *tope = &_end; /* el tope del heap, inicializado una sola vez */
char *anterior = tope;
char *sp = (char *)__builtin_frame_address(0); /* aproximación del puntero de pila actual */
if (tope + incremento > sp - MARGEN_PILA) {
errno = ENOMEM;
return (void *)-1; /* malloc devolverá NULL: falla visible, no corrupción silenciosa */
}
tope += incremento;
return anterior;
}
Con esta comprobación, el choque entre heap y pila se convierte de corrupción silenciosa en un malloc que devuelve NULL, algo que el programa puede detectar y manejar. Tiene un límite que hay que decir en voz alta: compara contra el SP de ahora, no contra el que tendrá la pila en el peor momento. Por eso se deja un margen, y por eso la solución más robusta es otra: no usar el heap en la fase de funcionamiento (tema 30) o reservar su tamaño de forma estática, como en el siguiente apartado.
07 · Decidirlo en el enlazador
El linker script completo
El script del tema 7 tenía solo flash, .data y .bss. La versión completa de un proyecto real agrega dos cosas: una sección .noinit para lo que no debe tocarse al arrancar y una reserva de espacio para heap y pila, que convierte «ojalá entren» en un error de enlace si no entran:
/* Reserva mínima: si lo estático no deja este espacio, el ENLAZADOR falla, no el producto en el campo */
_Min_Heap_Size = 0x400; /* 1 KB para el heap (0 si no se usa malloc) */
_Min_Stack_Size = 0x400; /* 1 KB para la pila, medido o estimado */
SECTIONS
{
/* ... .isr_vector, .text, .rodata, .data, .bss como en el tema 7 ... */
.noinit (NOLOAD) : /* NOLOAD: no se guarda en la flash ni se inicializa */
{
. = ALIGN(4);
*(.noinit*)
. = ALIGN(4);
} > RAM
._user_heap_stack : /* sección ficticia: solo reserva lugar */
{
. = ALIGN(8);
PROVIDE ( end = . ); /* 'end' y '_end': dónde empieza el heap */
PROVIDE ( _end = . );
. = . + _Min_Heap_Size;
. = . + _Min_Stack_Size;
. = ALIGN(8);
} > RAM
}
La sección ._user_heap_stack no contiene nada: solamente avanza el contador de ubicación para «ocupar» 2 KB de RAM. Si .data, .bss y esos 2 KB no caben en los 20 KB de la RAM, el enlazador termina con region 'RAM' overflowed. Es una barrera de seguridad barata: el aviso aparece al compilar y no en la puerta del cliente. Con los números del ejemplo, 368 bytes estáticos más 2048 reservados son 2416 de 20 480; el programa puede crecer su parte estática hasta 18 432 bytes antes de que el enlace falle.
Hay una limitación que conviene entender: la reserva garantiza que el mínimo entra, no que la pila no pueda crecer más allá de lo reservado (sigue disponiendo de todo el hueco). Es un seguro contra la falta de espacio estático, no contra el desborde dinámico. Para lo segundo hay que medir, y es lo próximo.
08 · Tomar el control
Ubicar datos a mano
El enlazador asigna cada cosa por reglas automáticas, pero a veces hace falta una excepción. GCC permite elegir la sección de una variable o función con un atributo, y el linker script decide qué pasa con esa sección. Dos usos frecuentes:
Datos que sobreviven a un reinicio: .noinit
Una variable en .bss se pone en cero cada vez que arranca el programa; una en .noinit no se toca. Si lo único que pasó fue un reinicio (por software, por el watchdog), la RAM conserva sus valores. Es una forma barata de dejar un dato para el arranque siguiente, por ejemplo la causa del reinicio o un contador de reinicios (tema 37):
#define MARCA_VALIDA 0xCAFEF00Du
typedef struct { uint32_t marca; uint32_t reinicios; uint32_t ultimo_error; } Diagnostico;
__attribute__((section(".noinit"))) static Diagnostico diag; /* el arranque no la limpia */
void diagnostico_iniciar(void) {
if (diag.marca != MARCA_VALIDA) { /* primer encendido (o corte de energía): la RAM tiene basura */
diag.marca = MARCA_VALIDA;
diag.reinicios = 0;
diag.ultimo_error = 0;
} else {
diag.reinicios++; /* hubo un reinicio sin pérdida de energía: el dato sobrevivió */
}
}
La marca es obligatoria: tras un corte de energía la SRAM contiene valores aleatorios, y sin una marca que distinga «datos míos» de «basura» no hay forma de saber cuál es cuál. Y .noinit no es memoria no volátil: no sobrevive a un apagado ni a un brown-out profundo, solo a reinicios en los que la alimentación se mantuvo.
Funciones que corren desde la RAM: .ramfunc
El apartado del tema 28 sobre la flash dijo que, mientras se borra, la CPU no puede leer de ella. La solución es que el código de borrado y escritura esté en la RAM. Se logra con un atributo y una sección del script que, como .data, vive en la RAM pero se guarda en la flash:
/* En el código */
__attribute__((section(".ramfunc"), noinline)) void borrar_y_programar(uint32_t dir, uint16_t valor);
/* En el linker script, junto a .data */
.ramfunc :
{
. = ALIGN(4);
*(.ramfunc*)
. = ALIGN(4);
} > RAM AT> FLASH /* se ejecuta desde la RAM, se guarda en la flash */
El mismo mecanismo permite poner en RAM una rutina de interrupción crítica para que no dependa de las esperas de la flash. Y se puede usar para reservar una página de flash para datos (la del tema 28): se la declara en MEMORY como una región aparte, para que el enlazador no coloque código allí.
Dos opciones del compilador y del enlazador completan el control y bajan el tamaño del programa sin tocar el código: -ffunction-sections -fdata-sections hace que cada función y cada variable tenga su propia sección, y -Wl,--gc-sections permite al enlazador descartar las que nadie usa. Sin ellas, una sola función que llama una biblioteca arrastra el archivo objeto entero.
09 · Mirar lo que pasó
Medir y leer el mapa
Cuatro herramientas bastan, de la más general a la más detallada. Todas están en la toolchain (tema 5) y se corren después de enlazar:
| Herramienta | Qué muestra | Cuándo usarla |
|---|---|---|
arm-none-eabi-size | Totales por categoría: text, data, bss | Siempre; es la primera mirada y la que se pone en la compilación automática |
arm-none-eabi-nm --size-sort -S | Cada símbolo con su dirección y su tamaño, ordenados | Para encontrar «qué variable o función ocupa tanto» |
Archivo .map (-Wl,-Map=...) | Todo lo que hizo el enlazador: orden, direcciones, de qué objeto vino cada pieza | Para auditar y para entender por qué algo quedó donde quedó |
-fstack-usage y pintado de la pila | El marco de cada función; y la profundidad real alcanzada | Para estimar y para medir la pila |
La salida de size para el ejemplo del tema 7 es la siguiente. Leer bien esa línea evita el error más común de principiante:
$ arm-none-eabi-size firmware.elf
text data bss dec hex filename
3828 24 344 4196 1064 firmware.elf
text(3828) es todo lo que vive en la flash y no cambia:.isr_vector+.text+.rodata(0x130 + 0xB4C + 0x278). Incluye las constantes aunque su nombre diga «text».data(24) es.data: ocupa RAM y flash (la copia).bss(344) es.bss: ocupa solo RAM.dec(4196) es la suma de las tres, y no significa nada útil: mezcla flash con RAM. Lo que importa son los dos totales de cada memoria: flash = text + data = 3852 y RAM estática = data + bss = 368.
Para saber cuánta pila se usó realmente existe una técnica clásica y muy efectiva, el pintado de la pila (stack painting): al arrancar, antes de main, se llena toda la zona libre de la pila con un patrón reconocible (por ejemplo 0xA5). Después de correr el equipo bajo carga durante un tiempo, se busca desde el fondo de la zona el primer byte que ya no tenga el patrón: lo que quedó intacto es el margen real, y lo que fue pisado, la marca de agua (high-water mark) de uso máximo. La Parte 4 del programa de Python lo reproduce. Es la medición que hay que repetir con la peor carga antes de liberar un producto.
10 · Comprobar que funciona
Verificación y diagnóstico
| Qué verificar | Cómo | Resultado esperado |
|---|---|---|
| Uso de flash y de RAM estática | size y las fórmulas del apartado 3 | Flash por debajo del 80-90 %; RAM estática con lugar para heap y pila |
| Que el enlace falle si no entra | Agrandar un arreglo .bss más allá de la RAM | Error region 'RAM' overflowed al enlazar, no un binario defectuoso |
| Direcciones de las secciones | Revisar el .map | .isr_vector en el inicio de la flash; .data con LMA en flash y VMA en RAM; _end alineado a 8 |
| Marco de cada función | -fstack-usage y revisar los .su | Ninguna función con un marco inesperadamente grande; ninguna dynamic sin bounded |
| Marca de agua de la pila | Pintar, correr con la peor carga y medir | Al menos el 25-30 % de la pila sin tocar |
| Hueco entre heap y pila | Mostrar el tope del heap y el SP más bajo observado | Positivo, con margen, incluso con todas las interrupciones |
Variable en .noinit | Reiniciar por software, y luego cortar la energía | Tras el reinicio el valor sobrevive; tras el corte, la marca no coincide y se reinicializa |
| Síntoma | Causa probable | Qué hacer |
|---|---|---|
| Variables que cambian solas, fallos intermitentes | La pila creció hasta pisar datos (o el heap la pisó) | Pintar la pila y medir; revisar arreglos locales grandes, recursión e interrupciones anidadas |
| Funciona en la mesa y falla al activar una función poco frecuente | La función que usa más pila (por ejemplo printf con coma flotante) no se ejercitó en las pruebas | Forzar el peor camino durante la medición de la pila |
| El enlazador dice «RAM overflowed» con pocos datos | Un arreglo grande (búfer, tabla) en .bss o .data, o la reserva de heap y pila muy alta | Buscar con nm --size-sort; mover constantes a flash; bajar _Min_Heap_Size |
| El binario es más grande que el código escrito | Funciones de biblioteca arrastradas (printf, malloc), sin --gc-sections | Activar -ffunction-sections y --gc-sections; revisar el .map para ver de dónde viene cada pieza |
| Las variables inicializadas valen cero o basura | No se copió .data: símbolos del script incorrectos | Verificar _sidata, _sdata, _edata (tema 8) |
Un dato en .noinit «se pierde» tras un corte de energía | Es lo esperado: es RAM, no memoria no volátil | Validar siempre con una marca; usar EEPROM o flash para lo que deba durar |
malloc devuelve NULL con mucha RAM aparentemente libre | Fragmentación del heap, o el _sbrk protege un margen de pila | Ver el tema 30; revisar el tope del heap y el margen |
11 · Analogía con programación
Probar el modelo en Python
El programa tiene cuatro partes y trabaja con las mismas cifras del tema 7: la colocación de secciones por el enlazador (con alineación y los cálculos de size), el peor caso de pila a partir de un grafo de llamadas y las interrupciones, el hueco entre el heap y la pila con distintos usos del heap, y el pintado de la pila con su marca de agua.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
# El mapa de memoria: el enlazador coloca secciones, y la pila y el heap se disputan el resto de la RAM.
FLASH_INI, FLASH_TAM = 0x08000000, 64 * 1024
RAM_INI, RAM_TAM = 0x20000000, 20 * 1024
ESTACK = RAM_INI + RAM_TAM # tope de la pila (0x20005000)
def alinear(x, a): return (x + a - 1) // a * a
# ---- Parte 1: el enlazador coloca las secciones, una tras otra, respetando la alineación -----------------
OBJETOS = { # (tamaño en bytes) de cada sección por objeto
".isr_vector": [("startup.o", 0x130)],
".text": [("startup.o", 0xC0), ("main.o", 0x4B0), ("sensor.o", 0x35C), ("uart.o", 0x280)],
".rodata": [("main.o", 0x78), ("sensor.o", 0x200)],
".data": [("main.o", 0x10), ("uart.o", 0x8)],
".bss": [("main.o", 0x40), ("sensor.o", 0x18), ("uart.o", 0x100)],
}
flash, ram, tamanos = FLASH_INI, RAM_INI, {}
print("Parte 1: el enlazador coloca las secciones")
for nombre, piezas in OBJETOS.items():
en_ram = nombre in (".data", ".bss")
ini = ram if en_ram else flash
total = sum(t for _, t in piezas)
tamanos[nombre] = total
extra = ""
if nombre == ".data": extra = f" (VMA en RAM, copia en flash: LMA {flash:#010x})"
print(f" {nombre:<12} {ini:#010x} {total:#7x} bytes{extra}")
if en_ram: ram = alinear(ini + total, 4)
else: flash = alinear(ini + total, 4)
if nombre == ".rodata": flash_fin_rodata = flash
if nombre == ".data": flash = alinear(flash + total, 4) # la copia de .data ocupa flash
text = tamanos[".isr_vector"] + tamanos[".text"] + tamanos[".rodata"]
data, bss = tamanos[".data"], tamanos[".bss"]
print(f" size: text={text} data={data} bss={bss} dec={text + data + bss} hex={text + data + bss:x}")
print(f" flash usada = text + data = {text + data} bytes ({(text + data) / FLASH_TAM * 100:.1f} % de 64 KB)")
heap_ini = alinear(RAM_INI + data + bss, 8)
print(f" RAM estática = data + bss = {data + bss} bytes; el heap empieza en {heap_ini:#010x}; libre para heap y pila: {ESTACK - heap_ini} bytes")
# ---- Parte 2: peor caso de pila a partir del grafo de llamadas --------------------------------------------
MARCO = {"main": 48, "leer_sensor": 32, "adc_leer": 24, "formatear": 64, "vsnprintf": 280, "uart_isr": 16, "procesar_byte": 40}
LLAMA = {"main": ["leer_sensor", "formatear"], "leer_sensor": ["adc_leer"], "formatear": ["vsnprintf"],
"adc_leer": [], "vsnprintf": [], "uart_isr": ["procesar_byte"], "procesar_byte": []}
MARCO_EXCEPCION = 32 # el hardware apila 8 palabras al entrar a una interrupción (sin FPU)
def peor(f):
"""Pila máxima que puede llegar a necesitar f, incluyendo lo que llame."""
hijos = [peor(h) for h in LLAMA[f]]
prof, camino = max(hijos, key=lambda r: r[0]) if hijos else (0, [])
return MARCO[f] + prof, [f] + camino
print()
print("Parte 2: la pila en el peor caso")
p_main, camino = peor("main")
p_isr, camino_isr = peor("uart_isr")
print(f" camino más profundo: {' -> '.join(camino)} = {p_main} bytes")
print(f" una interrupción justo ahí: {MARCO_EXCEPCION} (marco de excepción) + {p_isr} ({' -> '.join(camino_isr)}) = {MARCO_EXCEPCION + p_isr}")
peor_caso = p_main + MARCO_EXCEPCION + p_isr
print(f" pila total en el peor caso: {peor_caso} bytes; con 25 % de margen: {int(peor_caso * 1.25)} bytes")
# ---- Parte 3: heap que crece hacia arriba, pila que crece hacia abajo; ¿se pisan? ------------------------
print()
print("Parte 3: el hueco entre el heap y la pila")
for heap_usado in (0, 4096, 12288, 19000, 19800):
tope_heap = heap_ini + heap_usado
sp_peor = ESTACK - peor_caso
hueco = sp_peor - tope_heap
print(f" heap usado {heap_usado:>6} B: tope del heap {tope_heap:#010x}, SP en el peor caso {sp_peor:#010x}, hueco {hueco:>6} B -> {'COLISIÓN' if hueco < 0 else 'ok'}")
# ---- Parte 4: pintar la pila y medir cuánto se usó (marca de agua) -------------------------------------
print()
print("Parte 4: pila pintada con 0xA5 y marca de agua")
TAM_PILA = 1024
pila = bytearray([0xA5] * TAM_PILA) # la pila ocupa los últimos 1024 bytes de la RAM, y crece hacia abajo
def usar(profundidad): # la pila llega a 'profundidad' bytes: esos bytes quedan escritos
for i in range(profundidad): pila[TAM_PILA - 1 - i] = 0x00
for etapa, profundidad in (("arranque y main", 48), ("formatear + vsnprintf", 392), ("una interrupción encima", 480)):
usar(profundidad)
sin_tocar = 0
while sin_tocar < TAM_PILA and pila[sin_tocar] == 0xA5: sin_tocar += 1
usados = TAM_PILA - sin_tocar
print(f" tras «{etapa}»: usados {usados:>4} B, libres {sin_tocar:>4} B ({sin_tocar / TAM_PILA * 100:.0f} % de la pila)")
Resultado, parte 1: el enlazador ubica .isr_vector en 0x08000000, .text en 0x08000130, .rodata en 0x08000C7C, y .data en la RAM (0x20000000) con su copia (LMA) en 0x08000EF4; .bss sigue en 0x20000018. El cálculo de size da text=3828 data=24 bss=344 (suma 4196, que es 0x1064): la flash usada es 3852 bytes (5,9 % de 64 KB), la RAM estática 368 bytes, el heap empieza en 0x20000170 y quedan 20 112 bytes para heap y pila. Parte 2: el camino más profundo es main → formatear → vsnprintf con 392 bytes; una interrupción en ese instante suma 32 (marco de excepción) + 56 (uart_isr → procesar_byte) = 88; el peor caso es de 480 bytes, y con 25 % de margen, 600. Parte 3: con ese peor caso de pila, el hueco es de 19 632 bytes con el heap vacío, 15 536 con 4096 usados, 7344 con 12 288, 632 con 19 000 y −168 con 19 800: colisión. Parte 4: la pila pintada con 0xA5 muestra 48 bytes usados tras el arranque (976 libres, 95 %), 392 tras formatear + vsnprintf (632 libres, 62 %) y 480 con una interrupción encima (544 libres, 53 %).
Probá agregar una función con char buffer[512] local (un marco de 512 bytes) en el camino de main y mirá cómo cambia el peor caso, o hacé RAM_TAM = 2 * 1024 para ver los mismos números en un chip de 2 KB: con el heap vacío el hueco cae a 1200 bytes, y los casos de heap de 4096 bytes o más pasan a COLISIÓN. Cambiar un número y ver la consecuencia en el acto es lo que convierte el mapa en una herramienta de diseño.
12 · Exploración interactiva
La RAM
El laboratorio es la RAM de 20 KB del ejemplo, con lo estático (368 bytes, el .data y .bss del tema 7), el heap que crece hacia arriba y la pila que crece hacia abajo desde 0x20005000. Movés el uso del heap y la profundidad de la pila, y mirás el hueco que queda en el medio y las direcciones del tope del heap y del SP.
Heap, pila y el hueco entre ambos
Oscuro: .data + .bss · Verde claro: heap · Rayado: hueco libre · Naranja: pila (de derecha a izquierda). Se cruzaron.
Movés los controles y mirá el hueco.
Recorrido guiado
- Con los valores iniciales (heap de 4096 y pila de 392), el hueco es de unos 15 KB. Fijate en las dos direcciones: el tope del heap (
0x20001170) y el SP (0x20004E78). - Apretá «Pila: solo main» y después «formatear + vsnprintf»: el SP baja 344 bytes. Casi todo ese costo es de la función de formateo (280 de los 344 bytes): una sola llamada cuesta casi seis veces lo que cuesta
main. - Marcá «Una interrupción llega en el punto más profundo»: el SP baja otros 88 bytes. Es el peor caso de pila real: 480 bytes. Los 88 bytes extra son los que no aparecen si solo se mira el programa principal.
- Subí el heap hasta 19 800 con la pila en 480: el hueco se vuelve negativo y el programa lo avisa. En el equipo real no habría aviso, solo corrupción.
- Bajá el heap a 0 y apretá «Pila: recursión de 100 niveles (3200 B)»: la pila ahora ocupa casi el 16 % de la RAM. Subí lentamente el valor hasta 8000: una recursión descontrolada se come la RAM entera sin tocar el heap.
- Con el heap en 12 288 y la pila en 392, mirá el hueco (unos 7 KB). Es un diseño con margen. Ahora comparalo con el de 19 000: sigue sin colisión, pero el margen es de apenas 720 bytes (el 3,5 % de la RAM), y cualquier cambio de código puede romperlo. Un hueco que depende de que nada cambie no es un margen.
Desafío: ¿cómo se garantiza que la pila nunca invada el heap?
No se puede garantizar solo con el mapa; hace falta ayuda del hardware o del diseño. Opciones, de menor a mayor robustez: (1) medir con pintado de pila bajo la peor carga y dejar un margen del 25-30 %; (2) poner un «canario», una palabra con un valor conocido en el límite de la pila, y verificarla periódicamente (por ejemplo, en el tick del sistema) para detectar que fue pisada; (3) usar la MPU (Memory Protection Unit) de los Cortex-M para marcar una zona de protección debajo de la pila: cualquier escritura ahí dispara una falla en el acto, y no mucho después (tema 31); (4) ubicar la pila al fondo de la RAM, de modo que al desbordar caiga fuera de la memoria y produzca un fallo inmediato en lugar de corrupción silenciosa (algunos chips lo permiten con la pila debajo de .data y .bss); y (5) eliminar la causa: sin heap y sin recursión, el peor caso de pila se puede calcular y la colisión es imposible.
13 · Método de trabajo
Método: presupuesto de memoria
- Antes de elegir el chip, estimá. Flash: código esperado más constantes más una página de datos. RAM: variables, búferes, pila (peor camino más interrupciones, más el de cada tarea si hay RTOS) y heap si se usa. Sumá un margen del 30 %.
- Tené el mapa a mano desde el primer día. Agregá
sizea la compilación automática y mirá cómo evolucionan flash y RAM en cada cambio; los crecimientos bruscos señalan quién los causó. - Hacé que el enlazador proteja. Reservá heap y pila en el script con
_Min_Heap_Sizey_Min_Stack_Size: si lo estático crece demasiado, el enlace falla. - Calculá la pila en el peor caso con
-fstack-usagey el grafo de llamadas. Incluí la interrupción de mayor costo cayendo en el punto más profundo. Evitá la recursión y los arreglos locales grandes. - Medí la pila de verdad: pintala al arrancar, corré el peor caso del producto (las funciones de formato, las comunicaciones, los fallos) y leé la marca de agua. Dejá un 25-30 % libre.
- Revisá qué ocupa más. Con
nm --size-sort, mirá los cinco símbolos más grandes de RAM y de flash: casi siempre hay uno que se puede mover, reducir o eliminar. - Decidí sobre el heap. Si no usás
malloc, ponelo en cero en el script y evitá toda una clase de problemas. Si lo usás, protegé_sbrky planeá para la fragmentación (tema 30). - Documentá el presupuesto en el repositorio: cuánto se reserva a cada zona y por qué. El próximo que agregue una función tiene que saber cuánto margen queda.
14 · Comprensión
Confusiones frecuentes
«La suma dec de size es lo que ocupa mi programa»
Mezcla dos memorias. La flash usada es text + data y la RAM estática es data + bss; el total dec no corresponde a ninguna memoria real. Además, la RAM estática no incluye la pila ni el heap, que son lo que más RAM consume en muchos programas.
«Si size dice que uso el 20 % de la RAM, me sobra el 80 %»
Ese 20 % es solo lo estático. Lo que queda es lo que se disputan el heap y la pila, y un programa con recursión o printf con coma flotante puede necesitar mucho de eso. La RAM «libre» según size es un tope, no una reserva.
«Un arreglo grande sin inicializar es gratis porque no ocupa flash»
No ocupa flash, pero ocupa RAM de forma permanente desde que arranca el programa, y la RAM suele ser el recurso más escaso. Que no cueste espacio en el binario no significa que no cueste.
«Las variables locales no cuentan para el presupuesto de memoria»
Cuentan, y mucho si son grandes. Un char buf[512] local ocupa 512 bytes de pila mientras la función esté activa, y en un AVR con 2 KB es la cuarta parte de la RAM. No aparecen en size ni en el .map, y por eso se estiman con -fstack-usage y se miden con el pintado de la pila.
«Si el programa corre, la pila alcanza»
Solo si ya pasó por su peor camino. Una pila desbordada no avisa: corrompe. El programa puede funcionar meses hasta que ocurre la combinación (una función de formato, una interrupción, un error de comunicación) que lo lleva al límite. Hay que provocar el peor caso a propósito.
«.noinit es una memoria no volátil gratis»
Es RAM: sobrevive a un reinicio mientras la alimentación se mantenga, y se pierde en un apagado. Sin una marca que la valide, tampoco se sabe si lo que contiene es un dato o basura de un encendido nuevo. Para lo que debe durar, hay que usar EEPROM o flash.
«char *p = "texto" y char a[] = "texto" son lo mismo»
El puntero deja el texto en la flash (solo lectura) y ocupa lo del puntero en RAM; el arreglo copia el texto a la RAM (modificable) y gasta tanta RAM como caracteres. Intentar escribir en el literal del puntero es un error de comportamiento indefinido.
«La pila y el heap se reparten la RAM por mitades»
No hay reparto fijo: cada una crece hasta donde la lleve el programa, desde extremos opuestos. Puede que la pila use el 2 % y el heap el 90 %, o al revés. Por eso lo que importa es el hueco que queda entre ambas en el peor momento.
15 · Práctica guiada
Ejercicios
1. Leer la salida de size
Un firmware para un STM32F103C8 (64 KB de flash, 20 KB de RAM) muestra text=12000 data=400 bss=3600. ¿Cuánta flash y cuánta RAM estática usa? ¿Qué porcentaje es de cada memoria? ¿Cuánto queda para el heap y la pila?
Ver solución
Flash: text + data = 12 000 + 400 = 12 400 bytes, o sea 12 400 / 65 536 = 18,9 %. RAM estática: data + bss = 400 + 3600 = 4000 bytes, o sea 4000 / 20 480 = 19,5 %. Para el heap y la pila quedan 20 480 − 4000 = 16 480 bytes. Atención al error típico: sumar las tres cifras (16 000) y creerlo un uso de memoria; esa suma no corresponde a ninguna memoria.
2. ¿Dónde está cada cosa?
Indicá la sección de cada una (variables globales): (a) int g = 7;, (b) int h;, (c) const int k = 5;, (d) char *p = "hola"; (el puntero y el texto), (e) char s[] = "hola";.
Ver solución
(a) .data: valor inicial distinto de cero. (b) .bss: sin valor inicial. (c) .rodata: constante, solo flash. (d) El puntero p en .data (inicializado con una dirección distinta de cero, 4 bytes) y el texto "hola" en .rodata (5 bytes con el terminador). (e) s en .data: 5 bytes en RAM, con los caracteres iniciales copiados desde la flash al arrancar. Gasta más RAM que (d), a cambio de poder modificarse.
3. Recursión
Una función recursiva con un marco de 32 bytes se llama a sí misma hasta una profundidad de 100. ¿Cuánta pila usa? ¿A qué profundidad se agota una pila de 1 KB?
Ver solución
100 × 32 = 3200 bytes. Una pila de 1024 bytes alcanza para 1024 / 32 = 32 niveles; el nivel 33 ya sale de ella y empieza a escribir sobre lo que hay debajo. Y esto sin contar el marco de main, el de las funciones de arriba ni una interrupción: el límite real es todavía menor. Por eso en firmware se evita la recursión, o se limita explícitamente su profundidad con un número calculado.
4. El hueco
En el ejemplo del tema (heap desde 0x20000170, tope de pila en 0x20005000), el heap usa 12 288 bytes y la pila, en su peor caso, 480. ¿En qué direcciones están el tope del heap y el SP, y cuánto es el hueco?
Ver solución
Tope del heap: 0x20000170 + 12 288 (0x3000) = 0x20003170. SP: 0x20005000 − 480 (0x1E0) = 0x20004E20. Hueco: 0x20004E20 − 0x20003170 = 0x1CB0 = 7344 bytes, un 36 % de la RAM. Es un margen sano para el ejemplo.
5. La reserva del script
El script reserva _Min_Heap_Size = 0x400 y _Min_Stack_Size = 0x400. Con .data + .bss = 368 bytes y 20 KB de RAM, ¿cuánto puede crecer la RAM estática antes de que el enlace falle? ¿Qué garantiza y qué no esa reserva?
Ver solución
Se reservan 2048 bytes (1024 + 1024). El enlace falla cuando lo estático supera 20 480 − 2048 = 18 432 bytes. Garantiza que siempre hay al menos 1 KB de heap y 1 KB de pila reservados: lo estático no puede comérselos sin que el enlazador avise. No garantiza que la pila no crezca más de 1 KB (la pila sigue pudiendo usar todo el hueco), ni que el heap y la pila no se crucen en ejecución; para eso hay que medir.
6. Un dato que sobrevive
Querés guardar el número de reinicios del watchdog para mostrarlo luego. Proponés una variable en .noinit. ¿Qué dos problemas tiene la idea, y cómo se resuelven?
Ver solución
Primero, en el primer encendido (o tras un corte de energía) la RAM contiene basura, así que el contador valdría cualquier cosa: se resuelve con una marca (una palabra mágica) que se verifica al arrancar; si no coincide, se inicializa el contador en cero. Segundo, .noinit no sobrevive a un corte de energía, así que el contador se reinicia con cada apagado: si hace falta un total de por vida, no sirve y hay que usar EEPROM o flash, escribiendo solo cuando el contador cambie (y con el cuidado del desgaste del tema 28). Para «cuántas veces se reinició el watchdog desde que se encendió», en cambio, .noinit con marca es exactamente la herramienta correcta.
16 · Síntesis
Ideas para recordar
- El mapa de memoria es el reparto del programa entre flash y RAM, con direcciones concretas. El enlazador lo calcula para lo estático; la pila y el heap, que se crean en ejecución, quedan en el resto de la RAM.
- Las secciones son
.isr_vector,.texty.rodata(flash),.data(RAM, con copia en flash) y.bss(RAM, no ocupa flash), más.noinitopcional. La pila y el heap no son secciones del ejecutable. .datatiene dos direcciones: la LMA (donde se guarda, en flash) y la VMA (donde se usa, en RAM); el arranque copia de una a otra.- Flash usada =
text + data; RAM estática =data + bss. El totaldecdesizeno significa nada útil. La RAM estática no incluye pila ni heap. - La RAM se llena desde ambos extremos: lo estático y el heap hacia arriba, la pila hacia abajo desde
_estack. Entre ambos queda el hueco compartido, y si se cruzan, la corrupción es silenciosa. - La pila depende del camino de llamadas más profundo, de las interrupciones (32 bytes de marco de excepción más su propio marco) y de la recursión; con RTOS, cada tarea suma su propia pila. Se estima con
-fstack-usagey se mide pintándola. - Se puede ubicar datos a mano:
.noinitpara lo que sobrevive a un reinicio (con una marca de validez) y.ramfuncpara código que corre desde RAM. El script puede reservar heap y pila para que el enlace falle si no entran. - Medir manda sobre suponer:
sizeda los totales,nmlos símbolos, el.mapla auditoría y el pintado de pila el uso real. El presupuesto de memoria se hace desde el diseño, con margen.
¿Listo para el tema siguiente?
Antes de avanzar, tomá un proyecto tuyo (o el esqueleto del tema 12), compilalo con -Wl,-Map=firmware.map y respondé con números: cuánta flash y cuánta RAM estática usa, en qué dirección empieza el heap, cuáles son los cinco símbolos más grandes, cuál es el peor camino de pila según -fstack-usage y cuánto hueco queda. Si podés escribir esas seis cifras, ya sabés leer el mapa.
En el próximo tema nos metemos en la zona más delicada del mapa: la asignación estática y dinámica. Qué le pasa a malloc en un sistema con poca RAM, por qué se fragmenta el heap y qué alternativas (pools de memoria y buffers circulares) permiten evitarlo.