Si programa en una computadora "grande", entonces probablemente no tenga esa pregunta. Hay mucha pila para desbordarlo, debes intentarlo. En el peor de los casos, hace clic en Aceptar en una ventana como esta y se da cuenta.
Pero si programa microcontroladores, entonces el problema se ve un poco diferente. Primero debes
notar que la pila está llena.
En este artículo hablaré sobre mi propia investigación sobre este tema. Desde que programo principalmente bajo STM32 y bajo Milander 1986, me concentré en ellos.
Introduccion
Imaginemos el caso más simple: escribimos código simple de un solo subproceso sin ningún sistema operativo, es decir Solo tenemos una pila. Y si usted, como yo, programa en uVision Keil, entonces la memoria se distribuye de esta manera:

Y si usted, como yo, considera que la memoria dinámica en los microcontroladores es mala, entonces así:

Por ciertoSi desea prohibir el uso del montón, puede hacer esto:
#pragma import(__use_no_heap_region)
Detalles
aquí OK, cual es el problema? El problema es que Keil coloca la pila
inmediatamente detrás del área de datos estáticos. Y la pila en Cortex-M está creciendo en la dirección de direcciones decrecientes. Y cuando se desborda, simplemente se arrastra del trozo de memoria asignado. Y sobrescribe cualquier variable estática o global.
Especialmente genial si la pila se desborda solo al entrar en la interrupción. O, mejor aún, en una interrupción anidada. Y silenciosamente estropea alguna variable que se usa en una sección de código completamente diferente. Y el programa se bloquea en la afirmación. Si tienes suerte Heisenbag esférico, uno puede buscar toda una semana con una linterna.
Inmediatamente haga una reserva de que si usa un montón, entonces el problema no va a ninguna parte, solo que en lugar de las variables globales, el montón se echa a perder. No mucho mejor.
De acuerdo, el problema está claro. Que hacer
MPU
Lo más simple y obvio es usar MPU (en otras palabras, Unidad de Protección de Memoria). Le permite asignar diferentes atributos a diferentes piezas de memoria; en particular, puede rodear la pila con regiones de solo lectura y capturar MemFault al escribir allí.
Por ejemplo, en stm32f407 MPU es. Desafortunadamente, en muchos otros "junior" stm no lo es. Y en el Milandrovsky 1986VE1 tampoco está allí.
Es decir La solución es buena, pero no siempre asequible.
Control manual
Al compilar, Keil puede generar (y lo hace por defecto) un informe html con un gráfico de llamadas (opción de enlace "--info = stack"). Y este informe también proporciona información sobre la pila utilizada. Gcc también puede hacer eso (opción -fstack-use). En consecuencia, a veces puede ver este informe (o escribir un script que lo haga por usted y llamarlo antes de cada compilación).
Además, al comienzo del informe, se escribe una ruta que conduce al uso máximo de la pila:

El problema es que si su código tiene llamadas de función por punteros o métodos virtuales (y los tengo), entonces este informe puede subestimar en gran medida la profundidad máxima de la pila. Bueno, las interrupciones, por supuesto, no se tienen en cuenta. No es una forma muy confiable.
Colocación de pila complicada
Aprendí sobre este método de
este artículo . El artículo trata sobre el óxido, pero la idea principal es esta:

Cuando se usa gcc, esto se puede hacer usando el "
doble enlace ".
Y en Keil, la ubicación de las áreas se puede cambiar usando su propio script para el enlazador (archivo de dispersión en la terminología de Keil). Para hacer esto, abra las opciones del proyecto y desmarque "Usar diseño de memoria desde el diálogo de destino". Luego, el archivo predeterminado aparecerá en el campo "Archivo de dispersión". Se parece a esto:
; ************************************************************* ; *** Scatter-Loading Description File generated by uVision *** ; ************************************************************* LR_IROM1 0x08000000 0x00020000 { ; load region size_region ER_IROM1 0x08000000 0x00020000 { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x20000000 0x00005000 { ; RW data .ANY (+RW +ZI) } }
¿Qué hacer a continuación? Posibles opciones.
La documentación oficial sugiere definir secciones con nombres reservados: ARM_LIB_HEAP y ARM_LIB_STACK. Pero esto conlleva consecuencias desagradables, al menos para mí: el tamaño de la pila y el montón tendrá que establecerse en el archivo de dispersión.
En todos los proyectos que uso, los tamaños de pila y montón se establecen en el archivo de inicio del ensamblador (que Keil genera al crear el proyecto). Realmente no quiero cambiarlo. Solo quiero incluir un nuevo archivo de dispersión en el proyecto, y todo estará bien. Así que fui un poco diferente:
Spoiler #! armcc -E ; with that we can use C preprocessor #define RAM_BEGIN 0x20000000 #define RAM_SIZE_BYTES (4*1024) #define FLASH_BEGIN 0x8000000 #define FLASH_SIZE_BYTES (32*1024) ; This scatter file places stack before .bss region, so on stack overflow ; we get HardFault exception immediately LR_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load region size_region ER_IROM1 FLASH_BEGIN FLASH_SIZE_BYTES { ; load address = execution address *.o (RESET, +First) *(InRoot$$Sections) .ANY (+RO) } ; Stack region growing down REGION_STACK RAM_BEGIN { *(STACK) } ; We have to define heap region, even if we don't actually use heap REGION_HEAP ImageLimit(REGION_STACK) { *(HEAP) } ; this will place .bss region above the stack and heap and allocate RAM that is left for it RW_IRAM1 ImageLimit(REGION_HEAP) (RAM_SIZE_BYTES - ImageLength(REGION_STACK) - ImageLength(REGION_HEAP)) { *(+RW +ZI) } }
Luego dije que todos los objetos llamados STACK deberían ubicarse en la región REGION_STACK, y todos los objetos HEAP deberían ubicarse en la región REGION_HEAP. Y todo lo demás está en la región RW_IRAM1. Y organizó las regiones en este orden: el comienzo del operativo, la pila, el montón, todo lo demás. El cálculo es que en el archivo de inicio del ensamblador, la pila y el montón se configuran utilizando este código (es decir, como matrices con los nombres STACK y HEAP):
Spoiler Stack_Size EQU 0x00000400 AREA STACK, NOINIT, READWRITE, ALIGN=3 Stack_Mem SPACE Stack_Size __initial_sp Heap_Size EQU 0x00000200 AREA HEAP, NOINIT, READWRITE, ALIGN=3 __heap_base Heap_Mem SPACE Heap_Size __heap_limit PRESERVE8 THUMB
Bien, podrías preguntar, pero ¿qué nos da esto? Y aquí está lo que. Ahora, al salir de la pila, el procesador intenta escribir (o leer) memoria que no existe. Y en STM32, se produce una interrupción debido a una excepción: HardFault.
Esto no es tan conveniente como MemFault debido a la MPU, porque HardFault puede ocurrir debido a muchas razones, pero al menos el error es alto y no silencioso. Es decir ocurre inmediatamente, y no después de un período de tiempo desconocido, como lo fue antes.
¡Lo mejor de todo es que no pagamos nada por eso, sin tiempo de ejecución! Wow Pero hay un problema.
Esto no funciona en Milander.Si Por supuesto, en el Milandra (estoy interesado principalmente en 1986BE1 y BE91), la tarjeta de memoria se ve diferente. En STM32, antes del inicio de la operación, no hay nada, y en Milandra, antes de la operación, está el área externa del autobús.
Pero incluso si no utiliza un bus externo, no recibirá ningún HardFault. O tal vez lo entiendo. O tal vez obtenerlo, pero no de inmediato. No pude encontrar ninguna información sobre este tema (lo que no es sorprendente para Milander), y los experimentos no dieron ningún resultado inteligible. HardFault a
veces ocurría si el tamaño de la pila era un múltiplo de 256. A veces, HardFault ocurría si la pila llegaba demasiado lejos a la memoria inexistente.
Pero ni siquiera importa. Si HardFault no ocurre cada vez, simplemente mover la pila al comienzo de la RAM ya no nos salva. Y para ser honesto, STM tampoco está obligado a lanzar una excepción al mismo tiempo, la especificación central Cortex-M parece no decir nada concreto sobre esto.
Entonces, incluso en STM es más como un truco, simplemente no muy sucio.
Por lo tanto, debe buscar otra forma.
Punto de interrupción de acceso registrado
Si movemos la pila al comienzo de la RAM, el valor límite de la pila siempre será el mismo: 0x20000000. Y podemos simplemente poner un punto de quiebre en el registro en esta celda. Esto puede hacerse con el comando e incluso registrarse en ejecución automática utilizando el archivo .ini:
// breakpoint on stackoverflow BS Write 0x20000000, 1
Pero esta no es una forma muy confiable. Este punto de interrupción se disparará cada vez que se inicialice la pila. Es fácil superarlo accidentalmente haciendo clic en "Eliminar todos los puntos de interrupción". Y él te protegerá solo en presencia de un depurador. No bueno
Protección dinámica de desbordamiento
Una búsqueda rápida sobre este tema me llevó a las opciones de Keil --protect_stack y --protect_stack_all. Las opciones útiles, desafortunadamente, protegen no de desbordar toda la pila, sino de introducir otra función en el marco de la pila. Por ejemplo, si su código va más allá de los límites de una matriz o falla con un número variable de parámetros. Gcc, por supuesto, también puede hacer eso (-fstack-protector).
La esencia de esta opción es la siguiente: se agrega "variable de protección" a cada marco de pila, es decir, un número de protección. Si este número ha cambiado después de salir de la función, se llama a la función del controlador de errores. Detalles
aquíUna cosa útil, pero no exactamente lo que necesito. Necesito una verificación mucho más simple, para que al ingresar cada función, el valor del registro SP (Stack Pointer) se verifique con un valor mínimo previamente conocido. ¿Pero no escriba esta prueba con las manos en la entrada de cada función?
Control dinámico de SP
Afortunadamente, gcc tiene la maravillosa opción "-finstrument-functions", que le permite llamar a una función definida por el usuario cuando ingresa a cada función y cuando sale de cada función. Esto generalmente se usa para generar información de depuración, pero ¿cuál es la diferencia?
Aún más afortunadamente, Keil copia deliberadamente la funcionalidad gcc, y la misma opción está disponible con el nombre "--gnu_instrument" (
detalles ).
Después de eso, solo necesita escribir este código:
Y voila! Ahora, al ingresar cada función (incluidos los manejadores de interrupciones), se realizará una verificación para el desbordamiento de la pila. Y si la pila se desborda, habrá una afirmación.
Una pequeña explicación:- Sí, por supuesto, debe verificar el desbordamiento con cierto margen, de lo contrario existe el riesgo de "saltar" por encima de la pila.
- La imagen $$ REGION_STACK $$ RW $$ Base es una magia especial para obtener información sobre áreas de memoria utilizando las constantes generadas por el enlazador. Detalles (aunque no muy inteligibles en algunos lugares) aquí .
¿La solución es perfecta? Por supuesto que no.
En primer lugar, esta verificación está lejos de ser gratuita, su código aumenta en un 10 por ciento. Bueno, el código funcionará más lentamente (aunque no lo midí). Si es crítico o no, depende de usted; En mi opinión, este es un precio razonable para la seguridad.
En segundo lugar, lo más probable es que esto no funcione cuando se usan bibliotecas precompiladas (pero como no las uso en absoluto, no las verifiqué).
Pero esta solución es potencialmente adecuada para programas de subprocesos múltiples, ya que nosotros mismos hacemos la verificación. Pero realmente no he pensado en esta idea, así que la sostendré por ahora.
Para resumir
Resultó encontrar soluciones de trabajo para stm32 y para Milander, aunque para este último tuve que pagar con algunos gastos generales.
Para mí, lo más importante fue un pequeño cambio en el paradigma del pensamiento. Antes del
artículo antes mencionado, no pensé en absoluto que de alguna manera podría protegerse del desbordamiento de la pila. No percibí esto como un problema que debe resolverse, sino más bien como un cierto fenómeno natural: a veces llueve, y a veces la pila se desborda, bueno, no hay nada que hacer, hay que morder la bala y tolerar.
Y en general, a menudo me doy cuenta de mí mismo (y de otras personas) de esto, en lugar de pasar 5 minutos en Google y encontrar una solución trivial, he estado viviendo con mis problemas durante años.
Eso es todo para mí. Entiendo que no he descubierto nada fundamentalmente nuevo, pero no he encontrado ningún artículo preparado con tal decisión (al menos Joseph Yu mismo no ofrece esto directamente en un
artículo sobre este tema). Espero que en los comentarios me digan si tengo razón o no, y cuáles son las trampas de este enfoque.
UPD: si, al agregar un archivo de dispersión, Keil comienza a emitir una advertencia incomprensible ala "AppData \ Local \ Temp \ p17af8-2 (33): advertencia: # 1-D: la última línea del archivo finaliza sin una nueva línea", pero este archivo en sí no se abre, porque es temporal, luego simplemente agregue el salto de línea con el último carácter en el archivo de dispersión.