как выполняется функция

Вызов верхней функции порождает вызовы вложенных функций.

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

фотка 1

Для понимания процесса надо отследить 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 , в которых уже хранится что-то важное ,но которые будут задействованы во внутренней функции.

Таким образом функция это действительно кирпичек в пирамиде.

фотка 2

Обратите внимание , что функция printf тоже наверное вызывает какие-то фуекции, но нам этого уже знать не хочется (как там дальше что работает).

Такой у нас уровень абстракции.

Не знаю как вам , а мне лично вполне все понятно.

Единственно не совсем понятно как решает компилятор : когда и какие регистры r0,r1,r2,r3... надо сохранять в стек , а когда не надо . Пусть это будет уже на его совести.