Вызов верхней функции порождает вызовы вложенных функций.
Смысл любой функции , чтобы вернуть свой результат в вызывающую функцию.

Для понимания процесса надо отследить 2 регистра :
PC Program Counter (счетчик команд)
SP Stack Point (указатель на стек)
PC(счетчик команд) - это номер текущей команды (инструкции) , т.е. по сути это ее адрес в памяти кода.
PCможно поизучать отладчиком, SP можно даже вывести через printf.
Но лучше смотреть файл *.list . Тут все разжевано на уровне ассемблера , а точнее на уровне инструкций процессора (куда уж точнее).
Для разминки такой пример :
uint16_t myFunction(uint16_t *i, uint16_t y)
{
// sp = 0x2001fff8
*(uint16_t *)i = y;
return 0;
}
void main()
{
// sp = 0x2001fff8
uint16_t i2 = myFunction(1 , 7);
}
SP не меняется, почему?..
Это нормально , т.к. SP не было необходимости менять. Для процесса достаточно было двух регистров r1 и r0 . Поэтому компилятор посчитал, что не зачем трогать стек.
Смотрим ассемблер из файла list:
uint16_t i2 = myFunction(1,7);
8002f66: 2107 movs r1, #7
8002f68: 2001 movs r0, #1
8002f6a: f7ff fe2b bl 8002bc4 <myFunction>
// сначала помещаем в регистр lr текущий PC , чтобы знать
// куда вернуться
// меняем PC на 8002bc4
// т.е просто переходим на выполнение функции myFunction
....
08002bc4 <myFunction>:
*(uint16_t *)i = y;
8002bc4: 8001 strh r1, [r0, #0]
// это *(uint16_t *)i = y;
}
8002bc6: 2000 movs r0, #0
// это return 0;
8002bc8: 4770 bx lr
// это возврат из подпрограммы
// т.е. возвращаем PC из lr регистра
А что будет когда не хватит свободных (не занятых) регистров.
Изменим функцию myFunction : добавим внутри нее вызов другой функции (printf):
uint16_t myFunction(uint16_t *i, uint16_t y)
{
printf("1234 = x%0.8X\n",1234);
return 0;
}
И вот теперь видим , что внутри кода функции myFunction сначала в стек сохраняется регистр r3 и lr. А в конце функции они возвращаются : r3 в r3, а lr в PC !
08002bc4 <myFunction>:
{
8002bc4: b508 push {r3, lr}
// printf("1234 = x%0.8X\n",1234);
8002bc6: f240 41d2 movw r1, #1234 ; 0x4d2
8002bca: 4802 ldr r0, [pc, #8] ; (8002bd4 <myFunction+0x10>)
8002bcc: f000 fd6a bl 80036a4 <iprintf>
}
8002bd0: 2000 movs r0, #0
// return 0;
8002bd2: bd08 pop {r3, pc}
8002bd4: 080044d8 .word 0x080044d8
Вот как-то так все работает.
Обратить внимание , что адреса у кода 800..... , это флэш память , т.е. не изменяемая (постоянная), где хранится наш код программы.
Ошибочные фразы, которые я слышал:
1. функция при выполнении загружается в стек - сама функция в стек не загружается. В стек сохраняется текущая позиция кода PC для сохранения возврата на точку вызова и некоторые регистры r0,r1,r2,r3 , в которых уже хранится что-то важное ,но которые будут задействованы во внутренней функции.
Таким образом функция это действительно кирпичек в пирамиде.

Обратите внимание , что функция printf тоже наверное вызывает какие-то фуекции, но нам этого уже знать не хочется (как там дальше что работает).
Такой у нас уровень абстракции.
Не знаю как вам , а мне лично вполне все понятно.
Единственно не совсем понятно как решает компилятор : когда и какие регистры r0,r1,r2,r3... надо сохранять в стек , а когда не надо . Пусть это будет уже на его совести.