Hola Habr!
En el
último artículo , lo mencioné yo mismo y pregunté en los comentarios: ok, bueno, usando el método de empuje científico, seleccionamos el tamaño de la pila, parece que nada está cayendo, pero ¿podemos evaluar de alguna manera más confiable a qué es igual y quién comió tanto?
Respondemos brevemente: sí, pero no.
No, utilizando los métodos de análisis estático es imposible medir con precisión el tamaño de la pila requerida por el programa, pero, sin embargo, estos métodos pueden ser útiles.
La respuesta es un poco más larga, debajo del corte.
Como es ampliamente conocido por un círculo estrecho de personas, el lugar en la pila se asigna, de hecho, a las variables locales que utiliza la función que ejecuta actualmente, con la excepción de las variables con el modificador estático, que se almacenan en la memoria asignada estáticamente, en el área bss, porque deben guardar sus significados entre llamadas a funciones.
Cuando se ejecuta la función, el compilador agrega espacio en la pila para las variables que necesita, y al finalizar, libera este espacio. Parece que todo es simple, pero, y esto es muy audaz,
pero tenemos varios problemas:
- las funciones llaman dentro de otras funciones que también necesitan una pila
- a veces las funciones llaman a otras funciones no por su referencia directa, sino por un puntero a una función
- en principio, es posible, aunque debe evitarse por todos los medios, una llamada de función recursiva cuando A llama a B, B llama a C y C dentro de sí mismo vuelve a llamar a A
- en cualquier momento puede ocurrir una interrupción, cuyo manejador es la misma función que quiere su propia parte de la pila
- Si tiene una jerarquía de interrupciones, puede ocurrir otra interrupción dentro de la interrupción.
Sin ambigüedades, las llamadas a funciones recursivas deben eliminarse de esta lista, porque su presencia es una ocasión para no considerar el tamaño de la pila, sino para expresar su opinión al autor del código. Todo lo demás, por desgracia, no se puede tachar en el caso general (aunque en particular puede haber matices: por ejemplo, todas las interrupciones para usted pueden tener la misma prioridad por diseño, por ejemplo, como en RIOT OS, y no habrá interrupciones anidadas).
Ahora imagine una pintura al óleo:
- la función A, que come 100 bytes en la pila, llama a la función B, que necesita 50 bytes
- en el momento de la ejecución de B, A, obviamente, aún no ha terminado, por lo que sus 100 bytes no están liberados, por lo que ya tenemos 150 bytes en la pila
- la función B llama a la función C, y lo hace mediante un puntero que, según la lógica del programa, puede apuntar a media docena de funciones diferentes que consumen de 5 a 50 bytes de pila
- en tiempo de ejecución C, se produce una interrupción con un controlador pesado que se ejecuta relativamente largo y consume 20 bytes de pila
- durante el procesamiento de interrupción, se produce otra interrupción de mayor prioridad, cuyo controlador quiere 10 bytes de pila
En este hermoso diseño, con una coincidencia particularmente exitosa de todas las circunstancias, tendrá
al menos cinco funciones activas simultáneamente : A, B, C y dos controladores de interrupción. Además, uno de ellos no tiene una constante de consumo de pila, ya que puede ser una función diferente en diferentes pases, y para comprender la posibilidad o imposibilidad de interrumpirse entre sí, al menos debe saber si tiene interrupciones con diferentes prioridades. y, como máximo, para comprender si pueden superponerse entre sí.
Obviamente, para cualquier analizador automático de código estático, esta tarea es extremadamente abrumadora, y solo se puede realizar en una aproximación aproximada de la estimación superior:
- suma las pilas de todos los manejadores de interrupciones
- resumir pilas de funciones que se ejecutan en la misma rama de código
- intente encontrar todos los punteros a las funciones y sus llamadas, y tome el tamaño máximo de la pila entre las funciones a las que apuntan estos punteros como el tamaño de la pila
En la mayoría de los casos, obtendrá, por un lado, una estimación muy alta y, por otro, la oportunidad de omitir algunas llamadas de funciones particularmente difíciles a través de punteros.
Por lo tanto, en el caso general, simplemente podemos decir:
esta tarea no se resuelve automáticamente . Una solución manual, una persona que conoce la lógica de este programa, requiere excavar bastantes números.
Sin embargo, una estimación estática del tamaño de la pila puede ser muy útil para optimizar el software, al menos con el simple propósito de comprender quién está comiendo cuánto y no demasiado.
Hay dos herramientas extremadamente útiles para esto en la cadena de herramientas GNU / gcc:
- flag -fstack-use
- utilidad cflow
Si agrega -fstack-use a los indicadores gcc (por ejemplo, al Makefile en la línea con CFLAGS), entonces para
cada archivo compilado% filename% .c, el compilador creará el archivo% filename% .su, dentro del cual habrá texto simple y claro.
Tomemos, por ejemplo, target.su para
este gigantesco calzado :
target.c:159:13:save_settings 8 static target.c:172:13:disable_power 8 static target.c:291:13:adc_measure_vdda 32 static target.c:255:13:adc_measure_current 24 static target.c:76:6:cpu_setup 0 static target.c:81:6:clock_setup 8 static target.c:404:6:dma1_channel1_isr 24 static target.c:434:6:adc_comp_isr 40 static target.c:767:6:systick_activity 56 static target.c:1045:6:user_activity 104 static target.c:1215:6:gpio_setup 24 static target.c:1323:6:target_console_init 8 static target.c:1332:6:led_bit 8 static target.c:1362:6:led_num 8 static
Aquí vemos el consumo real de la pila para cada función que aparece en ella, de lo que podemos sacar algunas conclusiones por nosotros mismos, bueno, por ejemplo, lo que vale la pena intentar optimizar en primer lugar, si nos encontramos con una falta de RAM.
Al mismo tiempo, atención, ¡
este archivo en realidad no proporciona información precisa sobre el consumo real de la pila para funciones desde las cuales se llaman otras funciones !
Para comprender el consumo total, necesitamos construir un árbol de llamadas y resumir las pilas de todas las funciones incluidas en cada una de sus ramas. Esto se puede hacer, por ejemplo, con la utilidad
GNU cflow configurándolo en uno o más archivos.
El escape aquí obtenemos un orden de magnitud más pesado, solo daré una parte para el mismo objetivo. C:
olegart@oleg-npc /mnt/c/Users/oleg/Documents/Git/dap42 (umdk-emb) $ cflow src/stm32f042/umdk-emb/target.c adc_comp_isr() <void adc_comp_isr (void) at src/stm32f042/umdk-emb/target.c:434>: TIM_CR1() ADC_DR() ADC_ISR() DMA_CCR() GPIO_BSRR() GPIO_BRR() ADC_TR1() ADC_TR1_HT_VAL() ADC_TR1_LT_VAL() TIM_CNT() DMA_CNDTR() DIV_ROUND_CLOSEST() NVIC_ICPR() clock_setup() <void clock_setup (void) at src/stm32f042/umdk-emb/target.c:81>: rcc_clock_setup_in_hsi48_out_48mhz() crs_autotrim_usb_enable() rcc_set_usbclk_source() dma1_channel1_isr() <void dma1_channel1_isr (void) at src/stm32f042/umdk-emb/target.c:404>: DIV_ROUND_CLOSEST() gpio_setup() <void gpio_setup (void) at src/stm32f042/umdk-emb/target.c:1215>: rcc_periph_clock_enable() button_setup() <void button_setup (void) at src/stm32f042/umdk-emb/target.c:1208>: gpio_mode_setup() gpio_set_output_options() gpio_mode_setup() gpio_set() gpio_clear() rcc_peripheral_enable_clock() tim2_setup() <void tim2_setup (void) at src/stm32f042/umdk-emb/target.c:1194>: rcc_periph_clock_enable() rcc_periph_reset_pulse() timer_set_mode() timer_set_period() timer_set_prescaler() timer_set_clock_division() timer_set_master_mode() adc_setup_common() <void adc_setup_common (void) at src/stm32f042/umdk-emb/target.c:198>: rcc_periph_clock_enable() gpio_mode_setup() adc_set_clk_source() adc_calibrate() adc_set_operation_mode() adc_disable_discontinuous_mode() adc_enable_external_trigger_regular() ADC_CFGR1_EXTSEL_VAL() adc_set_right_aligned() adc_disable_temperature_sensor() adc_disable_dma() adc_set_resolution() adc_disable_eoc_interrupt() nvic_set_priority() nvic_enable_irq() dma_channel_reset() dma_set_priority() dma_set_memory_size() dma_set_peripheral_size() dma_enable_memory_increment_mode() dma_disable_peripheral_increment_mode() dma_enable_transfer_complete_interrupt() dma_enable_half_transfer_interrupt() dma_set_read_from_peripheral() dma_set_peripheral_address() dma_set_memory_address() dma_enable_circular_mode() ADC_CFGR1() memcpy() console_reconfigure() tic33m_init() strlen() tic33m_display_string()
Y eso ni siquiera es la mitad del árbol.
Para comprender el consumo real de la pila, necesitamos tomar el consumo para
cada una de las funciones mencionadas en él y sumar estos valores para cada una de las ramas.
Y aunque todavía no tenemos en cuenta las llamadas de función por punteros e interrupciones, incl. anidados (y específicamente en este código, pueden anidarse).
Como puede suponer, hacer esto cada vez que cambia el código es, por decirlo suavemente, difícil, es por eso que generalmente nadie lo hace.
Sin embargo, es necesario comprender los principios del llenado de la pila: esto puede conducir a ciertas restricciones en el código del proyecto, lo que aumenta su confiabilidad desde el punto de vista de evitar el desbordamiento de la pila (por ejemplo, la prohibición de interrupciones anidadas o llamadas a funciones por punteros), y específicamente el uso de stacks ayuda con la optimización del código en sistemas con falta de RAM.