
Пролог
В этом тексте я написал про то как наблюдать за расходованием стековой памяти прямо во время исполнения прошивки на микроконтроллере.
Терминология
RAM память — оперативная память в которой хранятся глобальные переменные.
Стек — Часть RAM памяти. В стеке хранятся локальные переменные, адрес возврата, значения регистров процессора. Обычно стек растет от большего адреса к меньшему адресу. При вызове прерываний в стек сохраняется регистровый файл процессора.
Стековый кадр — часть RAM памяти на стеке, которая образуется при вызове Си‑функции. В стековом кадре хранятся аргументы функции, локальные переменные данной функции и адрес возврата.
компоновщик (Linker) — консольная утилита, которая из множества объектных файлов склеивает один монолитный *.elf файл с финальной программой.
Реализация технологии раскраски стека
В микроконтроллерных прошивках вся RAM память делится на область глобальных переменных, стек и кучу. Надо отметить, что стек это динамическая часть памяти. При этом по умолчанию не существует никаких ограничений на бесконтрольное разрастание стека. В связи с этим, задача слежения за переполнением стека целиком и полностью ложится на плечи программиста. Следить за переполнением стека можно по-разному.
№ | Способ наблюдения за стеком | сложность |
1 | Ограничить стек снизу интервалом MPU | ** |
2 | Раскрасить стек и периодически вычислять степень максимального заполнения | * |
3 | Активировать prolog функции компилятора и делать проверку переполнения стека в прологе | *** |
4 | Смотреть за адресами локальных переменных в прерываниях по аппаратному таймеру | ** |
В этом тексте поговорим о раскраске стека.
Как можно заметить, в процедуре Reset_Handler сразу после инициализации регистра указателя на верхушку стека происходит обнуление стековой RAM памяти. Это делает процедура LoopFillZero_STACK
ldr r0, =0xE000ED08
ldr r1, =__vector_table
str r1, [r0]
/* Initialize the stack pointer */
ldr r0,=__Core0_StackTop
mov sp,r0 ; sp=r13
/* Clear Stack */
ldr r2, =__Core0_StackLimit
ldr r4, =__Core0_StackTop
movs r3, #0
b LoopFillZero_STACK
FillZero_STACK:
str r3, [r2]
adds r2, r2, #4
LoopFillZero_STACK:
cmp r2, r4 /* Compare (immediate) subtracts an immediate value from a register value. */
bcc FillZero_STACK /*branch if carry clear */
Переменные __Core0_StackTop и __Core0_StackLimit определяются компоновщиком в скрипте компоновщика. Они содержат адрес начала и условного конца стековой памяти.
.stack_dummy :
{
. = ALIGN(8);
__Core0_StackLimit = .;
. += STACK_SIZE;
. = ALIGN(8);
__Core0_StackTop = .;
} > DTCM_STACK
Как же понять в каком направлении у нас растет стековая память? Самое простое это написать функцию которая сама и ответит на этот вопрос.
static bool stack_dir(int32_t* main_local_addr) {
bool res = false;
int32_t fun_local = 0;
if(((void*)main_local_addr) < ((void*)&fun_local)) {
LOG_INFO(SYS, "Stack grows from small addr to big addr -> ");
} else {
LOG_INFO(SYS, "Stack grows from big addr to small addr <- "); /*hangOn here*/
}
return res;
}
bool explore_stack_dir(void) {
bool res = false;
int32_t main_local = 0;
res = stack_dir(&main_local);
return res;
}
У меня на микроконтроллере эта функция сообщает, что при увеличении стека вызова функций адреса локальных переменных уменьшаются. Значит стек растет вниз.

В процессоре ARM Cortex-M стек растет от большего адреса к меньшему. Вот так.

Поэтому считать израсходованный стек можно определив соотношение непрерывных нулей к ненулевым значениям в стековой памяти.

Сам стек мы будет измерять при помощи функции core_stack_used. Задача сводится к подсчету количества нулей в массиве.
bool array_max_cont(const uint8_t* const arr,
uint32_t size,
uint8_t patt,
uint32_t* max_cont_patt) {
bool res = false;
if(arr && (0 < size) && max_cont_patt) {
res = true;
uint32_t cur_cont_pat = 0;
uint32_t max_cont_pat = 0;
uint8_t prev_elem = 0xFF;
uint32_t i = 0;
for(i = 0; i < size; i++) {
if(patt == arr[i]) {
cur_cont_pat++;
if(prev_elem != arr[i]) {
cur_cont_pat = 1;
}
} else {
cur_cont_pat = 0;
}
prev_elem = arr[i];
max_cont_pat = MAX(max_cont_pat, cur_cont_pat);
}
*max_cont_patt = max_cont_pat;
}
return res;
}
float core_stack_used(const uint32_t top_stack_val,
const uint32_t exp_size) {
float precent = 0.0f;
LOG_DEBUG(CORE,"StackTop:0x%08X,Size:%u Byte" , top_stack_val, exp_size);
#ifdef HAS_ARRAY_EXT
if(exp_size) {
uint32_t busy = 0;
uint32_t max_cont_patt = 0;
bool res = array_max_cont((uint8_t*)top_stack_val - exp_size,
exp_size, 0, &max_cont_patt);
if(res) {
busy = exp_size - max_cont_patt;
precent = ((float)(100 * busy)) / ((float)exp_size);
}
}
#endif
return precent;
}
extern void __Core0_StackTop;
extern void __Core0_StackLimit;
float core_stack_used_get(void) {
uint32_t exp_size = &__Core0_StackTop - &__Core0_StackLimit;
float stack_used = core_stack_used((uint32_t) &__Core0_StackTop, exp_size);
LOG_DEBUG(CORE, "StackUsed:%s", FloatToStr(stack_used,2));
return stack_used;
}
Далее надо в супер цикле периодически вызывать функцию core_stack_monitor_proc
bool core_stack_monitor_proc(void) {
bool res = false ;
float stack_used=core_stack_used_get();
if(80.0<stack_used){
LOG_ERROR(CORE, "StackUsed80%%+:%s", FloatToStr(stack_used,2));
res = false ;
}else{
if(70.0<stack_used){
LOG_WARNING(CORE, "StackUsed,70%%+:%s", FloatToStr(stack_used,2));
res = false ;
}else{
res = true;
}
}
return res ;
}
Вот пожалуй, и все изменения, которые следует внести в прошивку, чтобы заработала диагностика стека.
Отладка
Что ж, вот мы написали в коде периодическую проверку степени заполнения стековой памяти. Настало время как-н загрузить процессор. Это можно провернуть при помощи вызова специально добавленной рекурсивной функции. Для этого у меня в прошивки есть UART-CLI команда try stack , вызывающая функцию try_recursion.
static bool call_recursion(uint32_t stack_top_addr,
uint32_t cur_depth,
uint32_t max_depth,
uint32_t* stack_size) {
bool res = false;
if(cur_depth < max_depth) {
res = call_recursion(stack_top_addr, cur_depth + 1,
max_depth, stack_size);
} else if(cur_depth == max_depth) {
uint32_t cur_stack_use = stack_top_addr - ((uint32_t)&res);
*stack_size = cur_stack_use;
res = true;
} else {
res = false;
}
return res;
}
bool try_recursion(const uint32_t stack_top_addr,
const uint32_t max_depth,
uint32_t* const stack_size) {
bool res = false;
res = call_recursion(stack_top_addr, 0, max_depth, stack_size);
#ifdef HAS_LOG
LOG_INFO(CORE, "Depth:%u,StackSize:%u,byte", max_depth, *stack_size);
#endif
return res;
}
В данной сборке уже при вызове с аргументом 50 вложений со стороны главной консоли управления прошивкой появляются первые предупреждения, что стек перевалил за 70%.

А при 60 вызовах функциями вложенных функций появляются сообщения, что превышено 80% выделенного компоновщиком стека.

Это красное сообщение в логе является настоятельным сигналом к тому, что следует пересобрать прошивку с другими конфигами компоновщика для минимального размера стека, либо и вовсе крепко переработать всю компьютерную программу.
В случае многоядерного микроконтроллера всё аналогично, только для каждого ядра в отдельности следует создать конфиг для каждого ядра в котором надо указать адрес начала стека и его размер. А алгоритм вычисления тот же самый.

Итог
Удалось реализовать в ARM Cortex-M прошивке автоматическую проверку процентного соотношения заполнения стековой RAM памяти. Это даёт ценнейшую метрику при разработке программного обеспечения. Достаточно теперь прогнать модульные тесты, посмотреть на заполнения стека и выделить большее количество в новом релизе прошивки по мере надобности.
Сокращения
Акроним | Расшифровка |
RAM | Random Access Memory |
MPU | Memory protection unit |
UART | universal asynchronous receiver / transmitter |
CLI | Command Line Interface |
COM | communications port |
Ссылки
Ссылка | URL |
@Amomum | |
Стековый кадр |
Вопросы
--Как сделать так, чтобы при выходе из стекового кадра происходило обнуление освободившегося интервала RAM памяти?