Я стараюсь изо всех сил узнать о стеке вызовов и о том, как стековые кадры
структурированы в ARM Cortex-M0
Итак, исходя из этой цитаты, сначала у cortex-m0 нет стековых фреймов, процессоры действительно очень глупы. Компилятор генерирует стековые фреймы, которые являются компилятором, а не набором команд. Понятие функции - вещь компилятора, а не нечто более низкое. Компилятор использует соглашение о вызовах или некоторый базовый набор правил, разработанных таким образом, чтобы для этого языка функции вызывающей и вызываемой функций точно знали, где находятся параметры, возвращаемые значения, и никто не удаляет другие данные.
Авторы компилятора могут делать все, что хотят, если они работают и соответствуют правилам набора команд, как в логике, а не на языке ассемблера. (Автор на ассемблере может создавать любой язык ассемблера по своему усмотрению, мнемонику независимо от того, что машинный код соответствует правилам логики). И они делали это, производители процессоров начали давать рекомендации, скажем так, и компиляторы им соответствуют. Речь идет не о том, чтобы делить объекты между компиляторами, а о том, что это 1) мне не нужно придумывать свой собственный 2) мы доверяем поставщику ip свой процессор и надеемся, что их соглашение о вызовах было разработано для производительности и других причин, которые мы хотим .
gcc до сих пор пытался соответствовать ABI ARM по мере своего развития и развития gcc.
Когда у вас есть «много» регистров, то, что много значит, является вопросом мнения, но вы увидите, что соглашение будет использовать сначала регистры, а затем стек для переданных параметров. Вы также увидите, что некоторые регистры будут обозначены как энергозависимые внутри функции, чтобы повысить производительность по сравнению с тем, как много нужно использовать память (стек).
Используя отладчик и точку останова, вы ищите не в том месте, где ваш оператор был для того, чтобы понять, что такое стек вызовов и фреймы стека, что является компилятором, а не то, как исключения обрабатываются в логике. Если это не то, чем вы были после того, как ваш вопрос не был достаточно точным, чтобы понять.
Компиляторы, такие как GCC, имеют оптимизаторы, и, несмотря на то, что они создают путаницу в отношении мертвого кода, изучение из оптимизированной версии проще, чем неоптимизированной версии. Давайте погрузимся в
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
return(a+b);
}
Оптимизированный
<fun>:
0: 1840 adds r0, r0, r1
2: 4770 bx lr
не
00000000 <fun>:
0: b580 push {r7, lr}
2: b082 sub sp, #8
4: af00 add r7, sp, #0
6: 6078 str r0, [r7, #4]
8: 6039 str r1, [r7, #0]
a: 687a ldr r2, [r7, #4]
c: 683b ldr r3, [r7, #0]
e: 18d3 adds r3, r2, r3
10: 0018 movs r0, r3
12: 46bd mov sp, r7
14: b002 add sp, #8
16: bd80 pop {r7, pc}
Прежде всего, почему функция по адресу ноль? Поскольку я разобрал объект, а не связанный двоичный файл, возможно, я сделаю это позже. А зачем разбирать против компиляции на сборку? Если дизассемблер хорош, то вы на самом деле можете увидеть, что было произведено, а не сборку, которая будет содержать, конечно же, скомпилированный код, много языка без инструкций, а также псевдокод, который изменяется после окончательной сборки.
ИМК стека - это когда есть второй указатель, указатель кадра. Вы часто видите это с наборами команд, которые имеют инструкции или ограничения, которые склоняются к этому. Например, набор инструкций может иметь регистр указателя стека, но вы не можете адресовать его, может быть другой указатель регистра фрейма, и это возможно. Таким образом, типичной записью будет сохранение указателя кадра в стеке, поскольку вызывающая сторона, возможно, использовала его для своего кадра, и мы хотим вернуть его как найденный, затем скопировать адрес указателя стека в указатель кадра, а затем переместить указатель стека настолько, насколько это необходимо для этой функции, чтобы прерывать или вызывать другие функции указатель стека находится на границе между используемым и неиспользуемым пространством стека, как это должно быть всегда. В этом случае указатель кадра будет использоваться для доступа к любым переданным в параметрах или адресам возврата в указателе кадра плюс мода смещения (для стеков, растущих вниз) и в направлении отрицательного смещения для локальных данных.
Теперь похоже, что компилятор использует указатель фрейма, что за пустая трата времени, давайте попросим его не делать:
00000000 <fun>:
0: b082 sub sp, #8
2: 9001 str r0, [sp, #4]
4: 9100 str r1, [sp, #0]
6: 9a01 ldr r2, [sp, #4]
8: 9b00 ldr r3, [sp, #0]
a: 18d3 adds r3, r2, r3
c: 0018 movs r0, r3
e: b002 add sp, #8
10: 4770 bx lr
поэтому сначала компилятор определил, что в стеке нужно сохранить 8 байтов. Неоптимизированный почти все занимает место в стеке, переданные параметры, а также локальные переменные, в этом случае не было никаких локальных элементов, поэтому мы просто передали единицы, два 32-битных числа и 8 байтов. Соглашение о вызовах использовало попытки использовать r0 для первого параметра и r1 для второго, если они подходят, в этом случае они делают. таким образом, кадр стека формируется, когда 8 вычитается из указателя стека, указатель кадра стека в этом случае является указателем стека. Соглашение о вызовах, используемое здесь, позволяет r0-r3 быть изменчивым в функции. Компилятору не нужно возвращаться к вызывающей стороне с этими регистрами, поскольку они были найдены, они могут быть использованы внутри функции по желанию. В этом случае компилятор выбрал извлечение из стека операндов сложения, используя регистры рядом с регистрами, а не первыми для освобождения. Как только r0 и r1 сохранены в стеке, тогда «пул» свободных регистров можно было бы начать с r0, r1, r2, r3. Так что да, похоже, что он не работает, но это то, что есть, это функционально правильно, и это задача компилятора создавать код, который функционально реализует скомпилированный код. Соглашение о вызовах, используемое этим компилятором, гласит, что возвращаемое значение помещается в r0, если оно соответствует, что и делает.
Таким образом, кадр стека настроен, 8 вычитается из sp. Переданные параметры сохраняются в стек. Теперь функция начинается с извлечения переданных параметров из стека, добавления их и помещения результата в регистр возврата.
Затем для возврата используется bx lr, ищите эту инструкцию вместе с pop (для armv6m, для armv4t pop нельзя использовать для переключения режимов, поэтому компиляторы будут, если они могут перейти к lr, затем bx lr).
armv4t thumb, не могу использовать pop для возврата в случае, если этот код смешан с arm, поэтому возвращение появляется в энергозависимом регистре и выполняет bx lr, вы не можете сразу же войти в lr большим пальцем. Вполне возможно, что вы сможете сказать компилятору, что я не смешиваю это с кодом arm, поэтому его можно сохранить с помощью pop для возврата. Зависит от компилятора.
00000000 <fun>:
0: b580 push {r7, lr}
2: b082 sub sp, #8
4: af00 add r7, sp, #0
6: 6078 str r0, [r7, #4]
8: 6039 str r1, [r7, #0]
a: 687a ldr r2, [r7, #4]
c: 683b ldr r3, [r7, #0]
e: 18d3 adds r3, r2, r3
10: 0018 movs r0, r3
12: 46bd mov sp, r7
14: b002 add sp, #8
16: bc80 pop {r7}
18: bc02 pop {r1}
1a: 4708 bx r1
чтобы увидеть указатель кадра
00000000 <fun>:
0: b580 push {r7, lr}
2: b082 sub sp, #8
4: af00 add r7, sp, #0
6: 6078 str r0, [r7, #4]
8: 6039 str r1, [r7, #0]
a: 687a ldr r2, [r7, #4]
c: 683b ldr r3, [r7, #0]
e: 18d3 adds r3, r2, r3
10: 0018 movs r0, r3
12: 46bd mov sp, r7
14: b002 add sp, #8
16: bd80 pop {r7, pc}
сначала вы сохраняете указатель кадра в стек как вызывающий или вызывающий, и т. Д., Возможно, он использует его, его регистр, который мы должны сохранить. теперь некоторые соглашения о вызовах вступают в силу с самого начала. Мы знаем, что компилятор знает, что мы не вызываем другую функцию, поэтому нам не нужно сохранять адрес возврата (хранится в регистре ссылок r14), поэтому зачем помещать его в стек, зачем тратить пространство и такты? Что ж, соглашение не так давно изменилось, чтобы стек должен быть выровнен на 64 бита, так что вы в основном помещаете и извлекаете пары регистров (четное количество регистров). Иногда они используют более одной инструкции для пары, как мы видим в возвращении armv4t. Таким образом, компилятору нужно было выдвинуть другой регистр, он мог бы, и вы увидите, что он просто выбирает какой-то регистр, который он не использует, и помещает его в стек, возможно, мы сможем сделать это здесь чуть позже. В этом случае, будучи armv6-m, вы можете переключать режимы с помощью pop, чтобы было безопасно генерировать возврат с использованием pop pc, поэтому вы сохраняете инструкцию, используя регистр связи здесь, а не какой-либо другой регистр. Небольшая оптимизация, несмотря на то, что код не оптимизирован.
сохранить указатель кадра, затем связать указатель кадра с указателем стека, в этом случае он сначала перемещает указатель стека и сопоставляет указатель кадра с указателем стека, а затем использует указатель кадра для доступа к стеку. О, как расточительно, даже для неоптимизированного кода. Но, возможно, этот компилятор по умолчанию использует указатель фрейма, когда ему приказано скомпилировать вот так.
Пока здесь один из ваших вопросов, и я до сих пор прокомментировал это косвенно. Полноразмерные процессоры arm от armv4t до armv7 поддерживают как инструкции arm, так и команды thumb. Не все поддерживают все, что было в эволюции, но вы можете сослаться на инструкции «рука-палец» как часть правил, определенных логикой для этого ядра. Конструкция ARM для поддержки этого заключается в том, что инструкции на руку должны быть выровнены по слову, младшие два бита адреса инструкции на руку всегда равны нулю. Желаемый 16-битный набор команд, также выровненный, всегда будет иметь младший бит адреса ноль. Так почему бы не использовать lsbit адреса как способ переключения режимов. И это то, что они решили сделать. Сначала с несколькими инструкциями, а затем стало больше, что разрешено архитектурой armv7, если адрес ветви (сначала посмотрите bx, обмен ветвями) имеет lsbit 1, то процессор переключается в режим большого пальца, когда он начинает выборку инструкции по этому адресу, счетчик программы не сохраняет этот, он удаляется командой, это просто сигнал, используемый для указания инструкции на переключение режимов. если lsbit равен 0, то процессор переключается в режим охраны. Если он уже был в указанном режиме, он просто остается в этом режиме.
Теперь идут эти ядра Cortex-M, которые являются машинами только для большого пальца, без режима руки. Инструменты на месте, все работает без причин для изменения, если вы пытаетесь перейти в режим охраны на кортекс-м, вы получаете ошибку.
Теперь посмотрите на приведенный выше код, иногда мы возвращаемся с bx lr, а иногда с pop pc, в обоих случаях lr содержит «адрес возврата». чтобы сработал случай bx lr, необходимо установить lsbit для lr. Вызывающая сторона не может знать, какую инструкцию мы будем использовать для возврата, и вызывающая сторона не должна, но, скорее всего, использует bl для выполнения вызова, поэтому логика фактически устанавливает бит, а не компилятор. Вот почему ваш обратный адрес отключен на один байт.
Если вы хотите узнать о компиляторах и фреймах стека, хотя неоптимизированный определенно использует стек, как вы можете видеть, оптимизированный код, если у вас есть компилятор с достойной оптимизацией, может быть легче понять вывод компиляторов, когда вы научитесь не делать мертвый код.
00000000 <fun>:
0: 1840 adds r0, r0, r1
2: 4770 bx lr
r0 и r1 - передаваемые параметры, r0 - куда возвращается значение, регистр связи - адрес возврата. Это то, на что вы надеетесь, что компилятор создаст такую функцию.
Теперь давайте попробуем что-нибудь более сложное.
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
return(more_fun(a,b));
}
00000000 <fun>:
0: b510 push {r4, lr}
2: f7ff fffe bl 0 <more_fun>
6: bd10 pop {r4, pc}
Несколько вещей, во-первых, почему оптимизатор не сделал этого:
fun:
b more_fun
Я не знаю.
почему он говорит, что bl 0, больше веселья не в нуле? Это объект, не связанный код, после того, как связанный компоновщик изменит эту инструкцию bl, чтобы она указала на more_fun ().
В-третьих, у нас уже есть компилятор для отправки регистра, который мы не использовали. Он нажимает и выталкивает r4, так что он может поддерживать стек в соответствии с соглашением о вызовах, используемым этим компилятором. Он мог бы выбрать почти любой из регистров, и вы можете найти версию gcc или llvm / clang, которая использует скажем r3 вместо r4. gcc уже некоторое время использует r4. Это первый в списке регистров, который вы должны сохранить первым в списке регистров, который, если они хотят сохранить что-то в вызове, который они будут использовать (как мы увидим через секунду). так что, может быть, поэтому, кто знает, спросите автора.
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
more_fun(a,b);
return(a);
}
00000000 <fun>:
0: b510 push {r4, lr}
2: 0004 movs r4, r0
4: f7ff fffe bl 0 <more_fun>
8: 0020 movs r0, r4
a: bd10 pop {r4, pc}
Теперь мы делаем успехи. Таким образом, мы сообщаем компилятору, что он должен сохранить переданный параметр через вызов функции. Каждая функция запускает правила заново, поэтому каждая вызываемая функция может удалить r0-r3, поэтому, если вы используете r0-r3 для чего-то, вам нужно их где-то сохранить. Таким образом, очень разумный выбор, вместо сохранения переданного параметра в стеке и, возможно, необходимости делать несколько дорогостоящих циклов памяти для доступа к нему. Вместо этого сохраните значение вызываемого или вызываемого и т. Д. В стеке и используйте регистр в нашей функции для сохранения этого параметра, так как в этом проекте сохраняется много потерянных циклов. В любом случае нам нужно было выровнять стек, так что все это сработало, сохранив r4 и сохранив обратный адрес, так как мы сами делаем вызов, который его ударит. Сохраните нужный нам параметр после вызова в r4. Сделайте вызов, поместите возвращаемое значение в регистр возврата и вернитесь. Очистка стека, как вы идете. Таким образом, кадр стека здесь минимален, если вообще. Не очень много пользуюсь стеком.
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b )
{
b<<=more_fun(a,b);
return(a+b);
}
00000000 <fun>:
0: b570 push {r4, r5, r6, lr}
2: 0005 movs r5, r0
4: 000c movs r4, r1
6: f7ff fffe bl 0 <more_fun>
a: 4084 lsls r4, r0
c: 1960 adds r0, r4, r5
e: bd70 pop {r4, r5, r6, pc}
мы сделали это снова, мы заставили компилятор сохранить регистр, который мы не использовали для сохранения выравнивания. И мы используем больше стека, но вы бы назвали это стековым фреймом? Мы заставили компилятор сохранять оба входящих параметра посредством вызова подпрограммы.
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d )
{
b<<=more_fun(b,c);
c<<=more_fun(c,d);
d<<=more_fun(b,d);
return(a+b+c+d);
}
0: b5f8 push {r3, r4, r5, r6, r7, lr}
2: 000c movs r4, r1
4: 0007 movs r7, r0
6: 0011 movs r1, r2
8: 0020 movs r0, r4
a: 001d movs r5, r3
c: 0016 movs r6, r2
e: f7ff fffe bl 0 <more_fun>
12: 0029 movs r1, r5
14: 4084 lsls r4, r0
16: 0030 movs r0, r6
18: f7ff fffe bl 0 <more_fun>
1c: 0029 movs r1, r5
1e: 4086 lsls r6, r0
20: 0020 movs r0, r4
22: f7ff fffe bl 0 <more_fun>
26: 4085 lsls r5, r0
28: 19a4 adds r4, r4, r6
2a: 19e4 adds r4, r4, r7
2c: 1960 adds r0, r4, r5
2e: bdf8 pop {r3, r4, r5, r6, r7, pc}
что это займет? мы по крайней мере получили его, чтобы сохранить r3, чтобы выровнять стек. Могу поспорить, что мы можем подтолкнуть его сейчас
extern unsigned int more_fun ( unsigned int, unsigned int );
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e, unsigned int f )
{
b<<=more_fun(b,c);
c<<=more_fun(c,d);
d<<=more_fun(b,d);
e<<=more_fun(e,d);
f<<=more_fun(e,f);
return(a+b+c+d+e+f);
}
00000000 <fun>:
0: b5f0 push {r4, r5, r6, r7, lr}
2: 46c6 mov lr, r8
4: 000c movs r4, r1
6: b500 push {lr}
8: 0011 movs r1, r2
a: 0007 movs r7, r0
c: 0020 movs r0, r4
e: 0016 movs r6, r2
10: 001d movs r5, r3
12: f7ff fffe bl 0 <more_fun>
16: 0029 movs r1, r5
18: 4084 lsls r4, r0
1a: 0030 movs r0, r6
1c: f7ff fffe bl 0 <more_fun>
20: 0029 movs r1, r5
22: 4086 lsls r6, r0
24: 0020 movs r0, r4
26: f7ff fffe bl 0 <more_fun>
2a: 4085 lsls r5, r0
2c: 9806 ldr r0, [sp, #24]
2e: 0029 movs r1, r5
30: f7ff fffe bl 0 <more_fun>
34: 9b06 ldr r3, [sp, #24]
36: 9907 ldr r1, [sp, #28]
38: 4083 lsls r3, r0
3a: 0018 movs r0, r3
3c: 4698 mov r8, r3
3e: f7ff fffe bl 0 <more_fun>
42: 9b07 ldr r3, [sp, #28]
44: 19a4 adds r4, r4, r6
46: 4083 lsls r3, r0
48: 19e4 adds r4, r4, r7
4a: 1964 adds r4, r4, r5
4c: 4444 add r4, r8
4e: 18e0 adds r0, r4, r3
50: bc04 pop {r2}
52: 4690 mov r8, r2
54: bdf0 pop {r4, r5, r6, r7, pc}
56: 46c0 nop ; (mov r8, r8)
Хорошо, вот как это будет ...
extern unsigned int more_fun ( unsigned int, unsigned int );
extern void not_dead ( unsigned int *);
unsigned int fun ( unsigned int a, unsigned int b )
{
unsigned int x[16];
unsigned int ra;
for(ra=0;ra<16;ra++)
{
x[ra]=more_fun(a+ra,b);
}
not_dead(x);
return(ra);
}
00000000 <fun>:
0: b5f0 push {r4, r5, r6, r7, lr}
2: 0006 movs r6, r0
4: b091 sub sp, #68 ; 0x44
6: 0004 movs r4, r0
8: 000f movs r7, r1
a: 466d mov r5, sp
c: 3610 adds r6, #16
e: 0020 movs r0, r4
10: 0039 movs r1, r7
12: f7ff fffe bl 0 <more_fun>
16: 3401 adds r4, #1
18: c501 stmia r5!, {r0}
1a: 42b4 cmp r4, r6
1c: d1f7 bne.n e <fun+0xe>
1e: 4668 mov r0, sp
20: f7ff fffe bl 0 <not_dead>
24: 2010 movs r0, #16
26: b011 add sp, #68 ; 0x44
28: bdf0 pop {r4, r5, r6, r7, pc}
2a: 46c0 nop ; (mov r8, r8)
И у вас есть фрейм стека, но на самом деле он не имеет указателя фрейма и не использует этот стек для доступа к вещам. Надо продолжать работать, чтобы увидеть это, очень выполнимо. Но, надеюсь, теперь вы понимаете мою точку зрения. Ваш вопрос о том, как стековые фреймы структурированы в скомпилированном коде, в частности, как компилятор может реализовать это для конкретной цели.
Кстати, это то, что сделал Clang с этим кодом.
00000000 <fun>:
0: b5b0 push {r4, r5, r7, lr}
2: af02 add r7, sp, #8
4: b090 sub sp, #64 ; 0x40
6: 460c mov r4, r1
8: 4605 mov r5, r0
a: f7ff fffe bl 0 <more_fun>
e: 9000 str r0, [sp, #0]
10: 1c68 adds r0, r5, #1
12: 4621 mov r1, r4
14: f7ff fffe bl 0 <more_fun>
18: 9001 str r0, [sp, #4]
1a: 1ca8 adds r0, r5, #2
1c: 4621 mov r1, r4
1e: f7ff fffe bl 0 <more_fun>
22: 9002 str r0, [sp, #8]
24: 1ce8 adds r0, r5, #3
26: 4621 mov r1, r4
28: f7ff fffe bl 0 <more_fun>
2c: 9003 str r0, [sp, #12]
2e: 1d28 adds r0, r5, #4
30: 4621 mov r1, r4
32: f7ff fffe bl 0 <more_fun>
36: 9004 str r0, [sp, #16]
38: 1d68 adds r0, r5, #5
3a: 4621 mov r1, r4
3c: f7ff fffe bl 0 <more_fun>
40: 9005 str r0, [sp, #20]
42: 1da8 adds r0, r5, #6
44: 4621 mov r1, r4
46: f7ff fffe bl 0 <more_fun>
4a: 9006 str r0, [sp, #24]
4c: 1de8 adds r0, r5, #7
4e: 4621 mov r1, r4
50: f7ff fffe bl 0 <more_fun>
54: 9007 str r0, [sp, #28]
56: 4628 mov r0, r5
58: 3008 adds r0, #8
5a: 4621 mov r1, r4
5c: f7ff fffe bl 0 <more_fun>
60: 9008 str r0, [sp, #32]
62: 4628 mov r0, r5
64: 3009 adds r0, #9
66: 4621 mov r1, r4
68: f7ff fffe bl 0 <more_fun>
6c: 9009 str r0, [sp, #36] ; 0x24
6e: 4628 mov r0, r5
70: 300a adds r0, #10
72: 4621 mov r1, r4
74: f7ff fffe bl 0 <more_fun>
78: 900a str r0, [sp, #40] ; 0x28
7a: 4628 mov r0, r5
7c: 300b adds r0, #11
7e: 4621 mov r1, r4
80: f7ff fffe bl 0 <more_fun>
84: 900b str r0, [sp, #44] ; 0x2c
86: 4628 mov r0, r5
88: 300c adds r0, #12
8a: 4621 mov r1, r4
8c: f7ff fffe bl 0 <more_fun>
90: 900c str r0, [sp, #48] ; 0x30
92: 4628 mov r0, r5
94: 300d adds r0, #13
96: 4621 mov r1, r4
98: f7ff fffe bl 0 <more_fun>
9c: 900d str r0, [sp, #52] ; 0x34
9e: 4628 mov r0, r5
a0: 300e adds r0, #14
a2: 4621 mov r1, r4
a4: f7ff fffe bl 0 <more_fun>
a8: 900e str r0, [sp, #56] ; 0x38
aa: 350f adds r5, #15
ac: 4628 mov r0, r5
ae: 4621 mov r1, r4
b0: f7ff fffe bl 0 <more_fun>
b4: 900f str r0, [sp, #60] ; 0x3c
b6: 4668 mov r0, sp
b8: f7ff fffe bl 0 <not_dead>
bc: 2010 movs r0, #16
be: b010 add sp, #64 ; 0x40
c0: bdb0 pop {r4, r5, r7, pc}
Теперь вы использовали термин стек вызовов. Соглашение о вызовах, используемое этим компилятором, гласит, что используйте r0-r3, когда это возможно, для передачи первых параметров, а затем используйте стек после этого.
unsigned int fun ( unsigned int a, unsigned int b, unsigned int c, unsigned int d, unsigned int e )
{
return(a+b+c+d+e);
}
00000000 <fun>:
0: b510 push {r4, lr}
2: 9c02 ldr r4, [sp, #8]
4: 46a4 mov r12, r4
6: 4463 add r3, r12
8: 189b adds r3, r3, r2
a: 185b adds r3, r3, r1
c: 1818 adds r0, r3, r0
e: bd10 pop {r4, pc}
, поэтому, имея более четырех параметров, первые четыре находятся в r0-r3, а затем «стек вызовов», если предположить, что это то, на что вы ссылались, это пятый параметр. Набор команд thumb использует bl в качестве основной инструкции вызова, которая использует r14 в качестве адреса возврата, в отличие от других наборов команд, которые могут использовать стек для хранения адреса возврата, arm использует регистр. А популярные соглашения о вызовах рук используют регистры для первых нескольких операндов, а затем используют стек после этого.
Вы хотели бы взглянуть на другие наборы команд, чтобы увидеть больше стека вызовов
00000000 <_fun>:
0: 1d80 0008 mov 10(sp), r0
4: 6d80 000a add 12(sp), r0
8: 6d80 0006 add 6(sp), r0
c: 6d80 0004 add 4(sp), r0
10: 6d80 0002 add 2(sp), r0
14: 0087 rts pc