c++ c embedded stack static-analysis

c++ - Estimación del tamaño de pila



embedded stack (10)

En el software integrado de subprocesos múltiples (escrito en C o C ++), un hilo debe tener suficiente espacio de pila para permitirle completar sus operaciones sin desbordarse. El tamaño correcto de la pila es crítico en algunos entornos integrados en tiempo real, porque (al menos en algunos sistemas con los que he trabajado), el sistema operativo NO lo detectará.

Por lo general, el tamaño de pila para un nuevo hilo (que no sea el hilo principal) se designa en el momento en que se crea ese hilo (es decir, en un argumento para pthread_create () o similar). A menudo, estos tamaños de pila están codificados de manera rígida a valores que se sabe que son buenos en el momento en que el código fue escrito o probado originalmente.

Sin embargo, los cambios futuros en el código a menudo rompen las suposiciones en las que se basaron los tamaños de pila codificados, y en un día fatídico, su hilo ingresa a una de las ramas más profundas de su gráfico de llamadas y desborda la pila, derribando todo el sistema o corromper silenciosamente la memoria.

Personalmente he visto este problema en el caso en que el código ejecutado en el hilo declara las instancias de estructura en la pila. Cuando la estructura se aumenta para contener datos adicionales, el tamaño de pila se infla en consecuencia, lo que potencialmente permite que se produzcan desbordamientos de pila. Me imagino que esto podría ser un gran problema para las bases de código establecidas donde los efectos completos de agregar campos a una estructura no se pueden conocer de inmediato (demasiados hilos / funciones para encontrar todos los lugares donde se usa esa estructura).

Dado que la respuesta habitual a las preguntas de "tamaño de pila" es "no son portátiles", supongamos que el compilador, el sistema operativo y el procesador son cantidades conocidas para esta investigación. Supongamos también que no se utiliza la recursión, por lo que no estamos tratando con la posibilidad de un escenario de "recursión infinita".

¿Cuáles son algunas formas confiables de estimar el tamaño de pila necesario para un hilo? Prefiero los métodos que están fuera de línea (análisis estático) y automáticos, pero todas las ideas son bienvenidas.


Aparte de algunas de las sugerencias ya hechas, me gustaría señalar que a menudo en los sistemas integrados usted tiene que controlar el uso de la pila de manera estricta porque debe mantener el tamaño de la pila en un tamaño razonable.

En cierto sentido, usar el espacio de pila es un poco como asignar memoria, pero sin una forma (fácil) de determinar si su asignación fue exitosa, por lo que no controlar el uso de la pila resultará en una lucha eterna para descubrir por qué su sistema se está fallando nuevamente. Entonces, por ejemplo, si el sistema asigna memoria para las variables locales de la pila, asigne esa memoria con malloc () o, si no puede usar malloc (), escriba su propio controlador de memoria (que es una tarea bastante simple).

No no:

void func(myMassiveStruct_t par) { myMassiveStruct_t tmpVar; }

Sí Sí:

void func (myMassiveStruct_t *par) { myMassiveStruct_t *tmpVar; tmpVar = (myMassiveStruct_t*) malloc (sizeof(myMassicveStruct_t)); }

Parece bastante obvio, pero a menudo no lo es, especialmente cuando no puedes usar malloc ().

Por supuesto, todavía tendrá problemas, así que esto es solo algo para ayudar pero no resuelve su problema. Sin embargo, le ayudará a estimar el tamaño de la pila en el futuro, ya que una vez que haya encontrado un buen tamaño para sus pilas y si luego, después de algunas modificaciones de código, se queda sin espacio en la pila, puede detectar una serie de errores o otros problemas (pilas de llamadas demasiado profundas para uno).


Como se discutió en la respuesta a esta pregunta , una técnica común es inicializar la pila con un valor conocido y luego ejecutar el código por un tiempo y ver dónde se detiene el patrón.


Este no es un método fuera de línea, pero en el proyecto en el que estoy trabajando, tenemos un comando de depuración que lee la marca de límite superior de todas las pilas de tareas dentro de la aplicación. Esto genera una tabla del uso de la pila para cada tarea y la cantidad de espacio disponible. La verificación de estos datos después de una ejecución de 24 horas con mucha interacción del usuario nos da cierta confianza de que las asignaciones de pila definidas son "seguras".

Esto funciona utilizando la técnica bien probada de llenar las pilas con un patrón conocido y suponiendo que la única forma de reescribir esto es mediante el uso normal de la pila, aunque si se está escribiendo por otros medios, se debe utilizar un desbordamiento de pila. menos de tus preocupaciones!


La verificación de la pila estática (fuera de línea) no es tan difícil como parece. Lo he implementado para nuestro IDE integrado ( RapidiTTy ) - actualmente funciona para ARM7 (NXP LPC2xxx), Cortex-M3 (STM32 y NXP LPC17xx), x86 y nuestro soft-core FPGA compatible con MIPS ISA interno.

Esencialmente, usamos un análisis simple del código ejecutable para determinar el uso de pila de cada función. La asignación de pila más significativa se realiza al inicio de cada función; solo asegúrese de ver cómo se altera con diferentes niveles de optimización y, si corresponde, conjuntos de instrucciones ARM / Thumb, etc. ¡Recuerde también que las tareas generalmente tienen sus propias pilas, y los ISR a menudo (pero no siempre) comparten un área de pila separada!

Una vez que tiene el uso de cada función, es bastante fácil construir un árbol de llamadas a partir del análisis y calcular el uso máximo para cada función. Nuestro IDE genera planificadores (RTOS efectivos) para que sepa exactamente qué funciones se están designando como "tareas" y cuáles son ISR, por lo que podemos determinar el uso en el peor de los casos para cada área de pila.

Por supuesto, estas cifras casi siempre superan el máximo real . Piense en una función como sprintf que puede usar una gran cantidad de espacio de pila, pero varía enormemente según la cadena de formato y los parámetros que proporcione. Para estas situaciones, también puede usar el análisis dinámico: llene la pila con un valor conocido en su inicio, luego ejecútela en el depurador por un tiempo, haga una pausa y vea qué cantidad de cada pila aún está llena con su valor (prueba de estilo de marca de agua alta) .

Ninguno de los enfoques es perfecto, pero la combinación de ambos te dará una idea bastante buena de cómo será el uso en el mundo real.


No es gratis, pero Coverity hace un análisis estático de la pila.


No estoy 100% seguro, pero creo que esto también puede hacerse. Si tiene un puerto jtag expuesto, puede conectarse a Trace32 y verificar el uso máximo de la pila. Aunque para esto, tendrás que dar un tamaño de pila arbitrario inicial bastante grande.



Si desea gastar una cantidad considerable de dinero, puede utilizar una herramienta de análisis estático comercial como Klocwork. Aunque Klocwork está dirigido principalmente a detectar defectos de software y vulnerabilidades de seguridad. Sin embargo, también tiene una herramienta llamada ''kw'' que puede usarse para detectar el desbordamiento de pila dentro de una tarea o hilo. Estoy usando para el proyecto integrado en el que trabajo y he tenido resultados positivos. No creo que ninguna herramienta como esta sea perfecta, pero creo que estas herramientas comerciales son muy buenas. La mayoría de las herramientas que he encontrado luchan con los punteros de función. También sé que muchos proveedores de compiladores como Green Hills ahora construyen funcionalidades similares directamente en sus compiladores. Esta es probablemente la mejor solución porque el compilador tiene un conocimiento íntimo de todos los detalles necesarios para tomar decisiones precisas sobre el tamaño de la pila.

Si tiene tiempo, estoy seguro de que puede usar un lenguaje de scripting para crear su propia herramienta de análisis de desbordamiento de pila. La secuencia de comandos necesitaría identificar el punto de entrada de la tarea o el subproceso, generar un árbol de llamada de función completo y luego calcular la cantidad de espacio de pila que utiliza cada función. Sospecho que probablemente hay herramientas gratuitas disponibles que pueden generar un árbol de llamadas de funciones completo, por lo que debería facilitarlo. Si conoce los detalles específicos de su plataforma que genera el espacio de pila que utiliza cada función puede ser muy fácil. Por ejemplo, la primera instrucción de ensamblaje de una función de PowerPC a menudo es la palabra de almacenamiento con una instrucción de actualización que ajusta el puntero de pila según la cantidad necesaria para la función. Puede tomar el tamaño en bytes desde la primera instrucción, lo que hace que la determinación del espacio total de pila utilizado sea relativamente fácil.

Todos estos tipos de análisis le darán una aproximación del límite superior del caso más desfavorable para el uso de la pila, que es exactamente lo que quiere saber. Por supuesto, los expertos (como los con los que trabajo) pueden quejarse de que estás asignando demasiado espacio de pila, pero son dinosaurios que no se preocupan por la buena calidad del software :)

Otra posibilidad, aunque no calcula el uso de la pila, sería usar la unidad de administración de memoria (MMU) de su procesador (si tiene una) para detectar el desbordamiento de la pila. He hecho esto en VxWorks 5.4 utilizando un PowerPC. La idea es simple, simplemente coloque una página de memoria protegida contra escritura en la parte superior de su pila. Si se desborda, se producirá una excepción del procesador y se le avisará rápidamente del problema de desbordamiento de pila. Por supuesto, no le dice cuánto necesita aumentar el tamaño de la pila, pero si es bueno con los archivos de excepción / núcleo de depuración, al menos puede averiguar la secuencia de llamadas que desbordó la pila. A continuación, puede utilizar esta información para aumentar el tamaño de su pila de forma adecuada.

-djhaus


Tratamos de resolver este problema en un sistema integrado en mi trabajo. Se volvió loco, simplemente hay demasiado código (tanto nuestro propio marco como el de terceros) para obtener una respuesta confiable. Afortunadamente, nuestro dispositivo estaba basado en Linux, por lo que recurrimos al comportamiento estándar de dar a cada hilo 2mb y permitir que el administrador de memoria virtual optimice el uso.

Nuestro único problema con esta solución fue que una de las herramientas de terceros realizó un mlock de memoria en todo su espacio de memoria (idealmente para mejorar el rendimiento). Esto causó que se paginaran todos los 2mb de pila para cada subproceso de sus subprocesos (75-150 de ellos). Perdimos la mitad de nuestro espacio de memoria hasta que lo descubrimos y comentamos la línea ofensiva.

Nota: El administrador de memoria virtual de Linux (vmm) asigna RAM en 4k trozos. Cuando un nuevo hilo solicita 2 MB de espacio de direcciones para su pila, el vmm asigna páginas de memoria falsas a todos, excepto a la página que está más arriba. Cuando la pila se convierte en una página falsa, el kernel detecta un error de página y cambia la página falsa por una real (que consume otros 4k de RAM real). De esta manera, la pila de un hilo puede crecer a cualquier tamaño que necesite (siempre que sea inferior a 2 mb) y la vmm asegurará que solo se utilice una cantidad mínima de memoria.


Evaluación de tiempo de ejecución

Un método en línea es pintar la pila completa con un cierto valor, como 0xAAAA (o 0xAA, cualquiera que sea su ancho). Luego, puede comprobar qué tan grande ha crecido la pila en el pasado al comprobar qué parte de la pintura queda sin tocar.

Eche un vistazo a this enlace para obtener una explicación con ilustración.

La ventaja es que es simple. Una desventaja es que no puede estar seguro de que el tamaño de su pila eventualmente no excederá la cantidad de pila usada durante su prueba.

Evaluación estática

Hay algunas comprobaciones estáticas y creo que incluso existe una versión de gcc pirateada que intenta hacer esto. Lo único que puedo decirles es que la comprobación estática es muy difícil de hacer en el caso general.

También echa un vistazo a this pregunta.