¿Qué sucede cuando los microcontroladores se quedan sin RAM?

Puede que solo sea una coincidencia, pero noté que los microcontroladores que usé se reiniciaron cuando se quedaron sin RAM (Atmega 328 si es específico del hardware). ¿Es eso lo que hacen los microcontroladores cuando se quedan sin memoria? Si no, ¿qué sucede entonces?

¿Por qué/Cómo? El puntero de la pila ciertamente aumenta ciegamente a un rango de memoria no asignado (o se transfiere), pero qué sucede entonces: ¿hay algún tipo de protección que haga que se reinicie o es (entre otros efectos) el resultado de la sobrescritura de crítico? data (que supongo que es diferente del código que creo que se ejecuta directamente desde flash)?

No estoy seguro de que esto deba estar aquí o en Stack Overflow, avíseme si debe moverse, aunque estoy bastante seguro de que el hardware tiene un papel en eso.

Actualizar

Debo señalar que estoy particularmente interesado en el mecanismo real detrás de la corrupción de la memoria (¿es el resultado de la transferencia del SP -> depende eso del mapeo de memoria de uC, etc.)?

Algunos micros se restablecerán si intenta acceder a direcciones no válidas. Es una característica valiosa implementada en el hardware. Otras veces puede terminar saltando a algún lugar arbitrario (digamos que ha golpeado la dirección de retorno de un ISR), tal vez ejecutando datos en lugar de código si la arquitectura lo permite, y tal vez quedando atrapado en un bucle que el perro guardián saca a la luz. de.
Un procesador no puede quedarse sin RAM, no hay ninguna instrucción que haga que se quede sin RAM. Quedarse sin RAM es completamente un concepto de software.

Respuestas (4)

En general, la pila y el montón chocan entre sí. En ese momento todo se complica.

Dependiendo de la MCU, una de varias cosas puede (o sucederá).

  1. Las variables se corrompen
  2. La pila se corrompe
  3. El programa se corrompe

Cuando sucede 1, comienza a tener un comportamiento extraño: las cosas no hacen lo que deberían. Cuando sucede 2, se desata todo tipo de infierno. Si la dirección de retorno en la pila (si hay una) está dañada, nadie sabe a dónde regresará la llamada actual. En ese momento, básicamente, la MCU comenzará a hacer cosas al azar. Cuando 3 vuelva a suceder, quién sabe qué pasaría. Esto solo sucede cuando está ejecutando código fuera de la RAM.

En general, cuando la pila se corrompe, se acabó. Justo lo que sucede depende de la MCU.

Puede ser que intentar asignar la memoria en primer lugar falle para que no ocurra la corrupción. En este caso, la MCU podría generar una excepción. Si no hay un controlador de excepciones instalado, la mayoría de las veces la MCU simplemente se detendrá (un equivalente de while (1);. Si hay un controlador instalado, entonces podría reiniciarse limpiamente).

Si la asignación de memoria continúa, o si lo intenta, falla y simplemente continúa sin memoria asignada, entonces está en el reino de "¿quién sabe?". La MCU podría terminar reiniciándose a través de la combinación correcta de eventos (las interrupciones causaron que terminaron reiniciando el chip, etc.), pero no hay garantía de que eso suceda.

Sin embargo, lo que generalmente puede haber una alta probabilidad de que suceda, si está habilitado, es que el temporizador de vigilancia interno (si existe) agote el tiempo y reinicie el chip. Cuando el programa se ausenta por completo a través de este tipo de bloqueo, las instrucciones para restablecer el temporizador generalmente no se ejecutarán, por lo que expirará y se restablecerá.

Gracias por tu respuesta, es un excelente resumen de los efectos. Tal vez debería haber especificado que me gustaría obtener más detalles sobre el mecanismo real de esas corrupciones: ¿se asigna toda la RAM a la pila y al montón, de modo que el puntero de la pila se desplaza y sobrescribe las variables/direcciones anteriores? ¿O depende menos del mapeo de memoria de cada micro? Opcionalmente (probablemente sea un tema en sí mismo), me interesaría saber cómo se implementan esos controladores de hardware.
Depende principalmente del compilador y de la biblioteca C estándar en uso. A veces también depende de cómo esté configurado el compilador (scripts de vinculación, etc.).
¿Podría ampliar eso, tal vez con un par de ejemplos?
No, realmente no. Algunos sistemas asignan un espacio finito para diferentes segmentos, otros no. Algunos usan secuencias de comandos de vinculación para definir segmentos, otros no. Elija un microcontrolador que le interese e investigue un poco sobre cómo funcionan sus asignaciones de memoria.

Una vista alternativa: los microcontroladores no se quedan sin memoria.

Al menos, no cuando se programa correctamente. La programación de un microcontrolador no es exactamente como la programación de propósito general, para hacerlo correctamente debe ser consciente de sus limitaciones y programar en consecuencia. Hay herramientas para ayudar a asegurar esto. Búscalos y apréndelos, al menos cómo leer las secuencias de comandos y las advertencias del enlazador.

Sin embargo, como dicen Majenko y otros, un microcontrolador mal programado puede quedarse sin memoria y luego hacer cualquier cosa, incluido un bucle infinito (lo que al menos le da al temporizador de vigilancia la oportunidad de restablecerlo. Habilitó el temporizador de vigilancia, ¿no? )

Las reglas de programación comunes para los microcontroladores evitan esto: por ejemplo, toda la memoria se asigna en la pila o se asigna estáticamente (globalmente); "nuevo" o "malloc" están prohibidos. También lo es la recursividad, de modo que la profundidad máxima de anidamiento de subrutinas pueda analizarse y mostrarse para que encaje en la pila disponible.

Por lo tanto, el almacenamiento máximo requerido se puede calcular cuando el programa se compila o vincula, y se compara con el tamaño de la memoria (a menudo codificada en el script del vinculador) para el procesador específico al que se dirige.

Entonces es posible que el microcontrolador no se quede sin memoria, pero su programa sí. Y en ese caso, llegas a

  • reescribirlo, más pequeño, o
  • elija un procesador más grande (a menudo están disponibles con diferentes tamaños de memoria).

Un conjunto común de reglas para la programación de microcontroladores es MISRA-C , adoptado por la industria del motor.

En mi opinión, la mejor práctica es usar el subconjunto SPARK-2014 de Ada. Ada realmente apunta a controladores pequeños como AVR, MSP430 y ARM Cortex razonablemente bien, e inherentemente proporciona un mejor modelo para la programación de microcontroladores que C. Pero SPARK agrega anotaciones al programa, en forma de comentarios, que describen lo que está haciendo el programa.

Ahora las herramientas de SPARK analizarán el programa, incluidas esas anotaciones, y probarán sus propiedades (o informarán sobre posibles errores). No tiene que perder tiempo ni espacio de código lidiando con accesos de memoria erróneos o desbordamientos de enteros porque se ha demostrado que nunca suceden.

Aunque hay más trabajo inicial involucrado con SPARK, la experiencia demuestra que puede llegar a un producto más rápido y más barato porque no pierde el tiempo persiguiendo reinicios misteriosos y otros comportamientos extraños.

Una comparación de MISRA-C y SPARK

+1 esto. Portar malloc()(y su compañero C ++ new) al AVR es una de las peores cosas que la gente de arduino podría haber hecho, y ha llevado a muchos, muchos programadores muy confundidos con código roto tanto en su foro como en el intercambio de pila de arduino. Hay muy, muy pocas situaciones en las que tener mallocun ATmega sea beneficioso.
+1 para filosofía, -1 para realismo. Si las cosas se programaran correctamente, no habría necesidad de esta pregunta. La pregunta era qué sucede cuando los microcontroladores se quedan sin memoria. Cómo evitar que se queden sin memoria es otra cuestión. En otra nota, la recursividad es una herramienta poderosa, tanto para resolver problemas como para quedarse sin pila.
@PkP: tentativamente no estoy de acuerdo con -1 por realismo. Si el codificador integrado puede pensar "¿cómo puedo prevenir..." en lugar de "qué sucede cuando...", entonces estamos progresando. La recursividad, por ejemplo, a menudo se puede linealizar, no hay muchas tareas en un controlador donde sea necesaria una verdadera recursividad. Ahora podríamos simplemente aceptar la derrota y la programación peligrosa (y estoy de acuerdo en que a veces tienes que hacerlo: debemos elegir las batallas con cuidado), pero no lo hagamos por defecto.
@Brian, como no soy idiota, obviamente estoy de acuerdo contigo. Simplemente me gusta pensar en ello desde el punto de vista inverso: me gusta esperar que cuando te des cuenta de las horribles consecuencias de quedarte sin memoria (pila), busques formas de evitar que suceda. De esa manera, tiene un ímpetu real para encontrar buenas prácticas de programación en lugar de simplemente seguir buenos consejos de programación... y cuando golpea la barrera de la memoria, es más probable que aplique las buenas prácticas, incluso a expensas de la conveniencia. Es solo un punto de vista...
@PkP: te escucho alto y claro. Etiqueté esto como una vista alternativa, ¡porque en realidad no responde la pregunta!
Entonces, los microcontroladores SÍ se quedan sin memoria. Sé que no deben hacerlo y hago todo lo posible para evitarlo (empezando por mallocs(), los uso con un valor de longitud máxima para ahorrar algo de RAM, pero el peor de los casos es el mismo que uno asignado estáticamente, así que me pregunto cuando en realidad es preferible), solo tengo curiosidad y este también es un conocimiento útil para la depuración en el futuro. C es mi lenguaje preferido (no soy fanático de Ada), ¿conoces alguna herramienta gratuita y más simple de análisis de código estático (MISRA parece ser patentada)? En realidad, me pregunto: ¿no depende la plataforma de estimación de RAM/enlazador?
Sin embargo, gracias por tu respuesta, ¡esta debería haber sido mi primera oración!
"No soy fan de Ada" Lo escucho mucho, pero rara vez de alguien con experiencia reciente (2005 o 2012, ¡no Ada-83!). ¿Por qué, en tu caso? Lo siento, no soy un experto en herramientas C, aunque escucho cosas positivas sobre FRAMA-C.
No pretendo tener ningún argumento específico en contra, simplemente no me gusta la sintaxis y el estilo de codificación como el de Pascal. Todavía estoy abierto a nuevas alternativas, pero por el momento la combinación de C, C++, VHDL y Python me permite hacer casi todo lo que quiero cómodamente.
Interesante. Un beneficio que Ada tiene para mí es la similitud con VHDL, lo que facilita el cambio entre SW y HW. (Por supuesto, desearía que VHDL no estuviera tan restringido después de usar Ada por un tiempo) Todavía uso C a veces, pero solo si el cliente paga lo suficiente.
@MisterMystère: los microcontroladores generalmente no se quedan sin memoria. Un microcontrolador que tiene 4096 bytes de RAM cuando se enciende por primera vez tendrá 4096 bytes para siempre. Es posible que el código intente acceder erróneamente a direcciones que no existen o espere que dos métodos diferentes de cálculo de direcciones accedan a memoria diferente cuando no lo hacen, pero el controlador simplemente ejecutará las instrucciones que se le den.

Me gusta mucho la respuesta de Majenko y yo mismo hice +1. Pero quiero aclarar esto en un punto agudo:

Cualquier cosa puede pasar cuando un microcontrolador se queda sin memoria.

Realmente no puedes confiar en nada cuando sucede. Cuando la máquina se queda sin memoria de pila, lo más probable es que la pila se corrompa. Y mientras eso sucede, cualquier cosa puede suceder. Los valores de las variables, los derrames, los registros temporales, todos se corrompen, interrumpiendo los flujos del programa. If/then/elses pueden evaluar incorrectamente. Las direcciones de retorno están distorsionadas, lo que hace que el programa salte a direcciones aleatorias. Cualquier código que haya escrito en el programa puede ejecutarse. (Considere un código como: "if [condición] entonces {fire_all_missiles();}"). También se pueden ejecutar un montón de instrucciones que no ha escrito cuando el núcleo salta a una ubicación de memoria no conectada. Todas las apuestas están cerradas.

Gracias por el apéndice, me gustó especialmente la línea fire_all_missiles().

AVR ha reiniciado el vector en la dirección cero. Cuando sobrescribe la pila con basura aleatoria, eventualmente dará vueltas y sobrescribirá alguna dirección de retorno y apuntará a "ninguna parte"; luego, cuando regresa de una subrutina a esa nada, la ejecución se desplazará a la dirección 0 donde generalmente se encuentra un controlador de salto para restablecer.