- PVSM.RU - https://www.pvsm.ru -
Привет!
В прошлой статье [1] и я сам упоминал, и в комментариях спрашивали — ок, хорошо, методом научного тыка мы подобрали размер стека, вроде ничего не падает, а можно как-то надёжнее оценить, чему он равен и кто вообще столько сожрал?
Отвечаем коротко: да, но нет.
Нет, методами статического анализа невозможно точно измерить размер потребного программе стека — но, тем не менее, эти методы могут пригодиться.
Ответ немного длиннее — под катом.
Как широко известно узкому кругу людей, место в стеке выделяется, по сути, под локальные переменные, которые использует выполняющаяся в данный момент функция — за исключением переменных с модификатором static, которые хранятся в статически выделенной памяти, в области bss, потому что они должны сохранять свои значения между вызовами функций.
При выполнении функции компилятор добавляет в стеке место под нужные ей переменные, при завершении — освобождает это место обратно. Казалось бы, всё просто, но — и это очень жирное но — у нас есть несколько проблем:
Однозначно из этого списка надо вычёркивать рекурсивные вызовы функций, потому что их наличие — повод не считать объём стека, а идти высказывать своё мнение автору кода. Всё остальное, увы, в общем случае вычеркнуть не получается (хотя в частных могут быть нюансы: например, у вам все прерывания могут иметь одинаковый приоритет by design, например, как в RIOT OS, и вложенных прерываний не будет).
Теперь представьте себе картину маслом:
В этой прекрасной конструкции при особенно удачном совпадении всех обстоятельств у вас будет минимум пять одновременно активных функций — A, B, C и два обработчика прерываний. Более того, у одной из них потребление стека не есть константа, ибо в разные проходы это может быть попросту разная функция, да и для понимания возможности или невозможности наложения друг на друга прерываний надо как минимум знать, есть ли у вас вообще прерывания с разными приоритетами, а как максимум — понять, могут ли они накладываться друг на друга.
Очевидно, что для любого автоматического статического анализатора кода эта задача является предельно близкой к непосильной, и её можно выполнить лишь в грубом приближении оценки сверху:
В большинстве случаев получится, с одной стороны, сильно завышенная оценка, а с другой — шанс пропустить какой-нибудь особенно хитрый вызов функции через указатели.
Поэтому в общем случае можно сказать просто: эта задача автоматически не решается. Ручное же решение — человеком, знающим логику данной программы — требует перелопатить довольно много чисел.
Тем не менее, статическая оценка размера стека может быть очень полезна при оптимизации ПО — хотя бы с банальной целью понять, кто сколько жрёт, и не многовато ли.
Для этого в тулчейне GNU/gcc есть два крайне полезных средства:
Если во флаги gcc (например, в Makefile в строку с CFLAGS) добавить -fstack-usage, то для каждого компилируемого файла %filename%.c компилятор создаст файл %filename%.su, внутри которого будет простой и понятный текст.
Возьмём, например, target.su для вот этой гигантской портянки [2]:
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
Здесь мы видим фактическое потребление стека для каждой фигурирующей в ней функции, из которого можем делать какие-то выводы для себя — ну, например, что стоит в первую очередь попробовать оптимизировать, если мы упираемся в нехватку ОЗУ.
При этом, внимание, этот файл не даёт на самом деле точной информации о реальном потреблении стека для функций, из которых вызываются другие функции!
Для понимания полного потребления нам необходимо построить дерево вызовов и суммировать стеки всех входящих в каждую его ветку функций. Сделать это можно, например, утилитой GNU cflow [3], натравив её на один или несколько файлов.
Выхлоп мы тут получим на порядок более развесистый, приведу лишь его часть для того же target.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()
И это даже не половина дерева.
Чтобы понять реальное потребление стека, нам надо взять потребление для каждой из упомянутых в нём функций и просуммировать эти значения для каждой из веток.
И при этом мы ещё не учтём вызовы функций по указателям и прерывания, в т.ч. вложенные (а конкретно в этом коде они могут быть вложенными).
Как нетрудно догадаться, проделывать это при каждом изменении кода, мягко говоря, затруднительно — поэтому так обычно никто и не делает.
Тем не менее, понимать принципы наполнения стека необходимо — из этого могут рождаться определённые ограничения на код проекта, повышающие его надёжность с точки зрения недопущения переполнения стека (например, запрет вложенных прерываний или вызовов функций по указателям), а конкретно -fstack-usage может сильно помочь при оптимизации кода на системах с нехваткой ОЗУ.
Автор: Олег Артамонов
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/stm32/311468
Ссылки в тексте:
[1] прошлой статье: https://habr.com/ru/post/443030/
[2] вот этой гигантской портянки: https://github.com/unwireddevices/dap42/blob/umdk-emb/src/stm32f042/umdk-emb/target.c
[3] GNU cflow: https://www.gnu.org/software/cflow/
[4] Источник: https://habr.com/ru/post/443544/?utm_source=habrahabr&utm_medium=rss&utm_campaign=443544
Нажмите здесь для печати.