Сборка RISC-V - компоновка стека - вызов функции
В настоящее время я работаю с реализацией процессора RISC-V. Мне нужно запустить частично собранный код сборки. (Наконец, будет динамическое внедрение кода.) Для этого мне нужно понять основы вызовов функций в сборке RISC-V.
Я нашел эту тему очень полезной: путаница в стеке вызовов функций
Но я все еще борюсь с макетом стека для вызова функции. Пожалуйста, рассмотрите следующий c-код:
void some_func(int a, int b, int* c){
int cnt = a;
for(;cnt > 0;cnt--){
*c += b;
}
}
void main(){
int a = 5;
int b = 6;
int c = 0;
some_func(a,b,&c);
}
Эта программа реализует базовое умножение на последовательность дополнений. Производный код сборки (riscv64-unknown-elf-gcc -nostartfiles mul.c -o mul && riscv64-unknown-elf-objdump -D mul) выглядит следующим образом:
0000000000010000 <some_func>:
10000: fd010113 addi sp,sp,-48
10004: 02813423 sd s0,40(sp)
10008: 03010413 addi s0,sp,48
1000c: fca42e23 sw a0,-36(s0)
10010: fcb42c23 sw a1,-40(s0)
10014: fcc43823 sd a2,-48(s0)
10018: fdc42783 lw a5,-36(s0)
1001c: fef42623 sw a5,-20(s0)
10020: 0280006f j 10048 <some_func+0x48>
10024: fd043783 ld a5,-48(s0)
10028: 0007a703 lw a4,0(a5)
1002c: fd842783 lw a5,-40(s0)
10030: 00f7073b addw a4,a4,a5
10034: fd043783 ld a5,-48(s0)
10038: 00e7a023 sw a4,0(a5)
1003c: fec42783 lw a5,-20(s0)
10040: fff7879b addiw a5,a5,-1
10044: fef42623 sw a5,-20(s0)
10048: fec42783 lw a5,-20(s0)
1004c: fcf04ce3 bgtz a5,10024 <some_func+0x24>
10050: 00000013 nop
10054: 02813403 ld s0,40(sp)
10058: 03010113 addi sp,sp,48
1005c: 00008067 ret
0000000000010060 <main>:
10060: fe010113 addi sp,sp,-32
10064: 00113c23 sd ra,24(sp)
10068: 00813823 sd s0,16(sp)
1006c: 02010413 addi s0,sp,32
10070: 00500793 li a5,5
10074: fef42623 sw a5,-20(s0)
10078: 00600793 li a5,6
1007c: fef42423 sw a5,-24(s0)
10080: fe042223 sw zero,-28(s0)
10084: fe440793 addi a5,s0,-28
10088: 00078613 mv a2,a5
1008c: fe842583 lw a1,-24(s0)
10090: fec42503 lw a0,-20(s0)
10094: f6dff0ef jal 10000 <some_func>
10098: 00000013 nop
1009c: 01813083 ld ra,24(sp)
100a0: 01013403 ld s0,16(sp)
100a4: 02010113 addi sp,sp,32
100a8: 00008067 ret
Важные шаги, требующие пояснения: (some_func(int,int,int))
10060: fe010113 addi sp,sp,-32
10064: 00113c23 sd ra,24(sp)
10068: 00813823 sd s0,16(sp)
1006c: 02010413 addi s0,sp,32
и: (main())
10000: fd010113 addi sp,sp,-48
10004: 02813423 sd s0,40(sp)
10008: 03010413 addi s0,sp,48
Из моего понимания: указатель стека перемещен, чтобы освободить место для адреса возврата и параметров. (Здесь может быть особый случай.) Как обрабатываются переданные аргументы, когда они находятся в стеке? Как они получены обратно? В общем, методология мне понятна, но как бы я вручную кодировал этот сегмент, чтобы работать.
Что касается смежной темы, стек должен выглядеть примерно так
| ??? |
| params for some_func() <???> |
| ra of some_func() |
| locals of main() <int c> |
| locals of main() <int b> |
| locals of main() <int a> |
| params for main() <None> |
Но это в значительной степени так. Кто-нибудь может указать, как это устроено, и как эти два списка (вызов функции) связаны друг с другом?
1 ответ
Первые несколько параметров, разрешающие тип, передаются в регистрах, поэтому они даже не появляются в стеке. Кроме этого, неясно, что вы действительно хотите знать. Если вы получите некоторые аргументы, которые находятся в стеке, они останутся там даже после того, как вы настроите указатель стека, так что вы все равно сможете обратиться к ним относительно скорректированного указателя стека или указателя кадра (здесь $s0
по-видимому).
Важные шаги, которые требуют уточнения:
10060: fe010113 addi sp,sp,-32 # allocate space
10064: 00113c23 sd ra,24(sp) # save $ra
10068: 00813823 sd s0,16(sp) # save $s0
1006c: 02010413 addi s0,sp,32 # set up $s0 as frame pointer
То, что вы хотите знать, определяется соглашениями о вызовах RISC-V.
Основные моменты:
Аргументы функции обычно передаются в a0
к a7
регистры, а не в стеке. Аргумент передается через стек, только если в стеке нет места.a*
регистров осталось.
Некоторые регистры сохраняются для вызывающего абонента, а другие - для вызываемого (см. Таблицу 26.1, Глава 26 Руководство программиста сборки RISC-V в базовой спецификации RISC-V, ратифицировано 08.06.2019). Это означает, что перед вызовом функции вызывающая сторона должна сохранить все сохраненные регистры вызывающей стороны в стек, если она хочет сохранить их содержимое. Точно так же вызываемая функция должна сохранить все сохраненные регистры вызываемого объекта в стек, если она хочет использовать их в своих целях.