Функції та стек

Функції та стек

Функція — це іменований блок інструкцій, який можна використовувати більше ніж один раз. Ви переходите до неї, вона виконує своє завдання й повертає керування туди, звідки ви її викликали. Це дає змогу написати фрагмент логіки один раз і використовувати його з багатьох місць замість повторення тих самих рядків у всій програмі.

Використовуйте , щоб викликати функцію: вона переходить до мітки й зберігає адресу повернення в ra. Функція завершується , який переходить назад за адресою в ra. і — це псевдоінструкції: зручні назви, які асемблер розгортає в справжні інструкції, і .

Аргументи розміщуються в a0, a1 і наступних регістрах. Значення, що повертається, розміщується в a0. Такий порядок — це угода про виклики, спільна домовленість, яка дає змогу окремим частинам коду працювати разом.

Функція square()
li a0, 6 # аргумент
call square # виклич, і ra пам'ятає, куди повернутися
li a7, 1 # square залишив свій результат у a0
ecall
li a7, 10
ecall
square:
mul a0, a0, a0 # a0 = a0 * a0
ret # повернутися до ra

Простежте виклик. розміщує аргумент 6 у a0. переходить до мітки square і зберігає адресу повернення в ra. Усередині функції обчислює 6 * 6 = 36 і залишає результат у a0 — регістрі, який використовується для значень, що повертаються. переходить назад до інструкції одразу після виклику, де і друкують 36, після чого програма завершується.

Виклик функції та повернення
викликач
call A
...
A
addi sp, sp, -4
sw ra, 0(sp)
call B
lw ra, 0(sp)
addi sp, sp, 4
ret
B
mul a0, a0, a0
ret
STACK
sp →
0x7ffffff0
0x7fffffec

Викликач збирається викликати A.

Спостерігайте, як виділений рядок здійснює повний шлях туди й назад. caller виконує , що автоматично зберігає точку повернення до caller у регістрі ra. Перш ніж A зможе виконати , виникає проблема: інструкція ось-ось перезапише ra новою адресою повернення, знищивши ту, яка потрібна A, щоб повернутися додому. Щоб це виправити, A звільняє місце в стеку () і зберігає там початковий ra (). Тепер можна безпечно виконати . B виконується й повертається звичайною інструкцією , приземляючись назад усередину A. Нарешті A відновлює початковий ra зі стеку (), очищає вказівник стеку (), і його власний використовує це відновлене значення, щоб успішно повернутися до caller. Якби A пропустив цей крок, інструкція назавжди перезаписала б ra, і фінальний у A перейшов би не туди, замість того щоб повернутися додому.

Стек

Існує лише кілька регістрів, тож функціям, які викликають інші функції, потрібне тимчасове місце. Стек — це пам'ять, яку ви позичаєте через вказівник стеку sp: віднімайте, щоб звільнити місце, зберігайте те, що потрібно зберегти, відновлюйте це перед .

Стек — це структура Last-In, First-Out (LIFO): найостанніше збережене значення вилучається першим. У RISC-V стек росте вниз у пам'яті. Віднімання від sp резервує новий простір, який безпечно використовувати. Коли ви вилучаєте значення назад, ви маєте робити це в порядку, зворотному до того, у якому ви їх зберігали, інакше потік виконання програми буде порушено.

Збереження та відновлення ra під час виклику
addi sp, sp, -4 # звільнити місце для одного слова
sw ra, 0(sp) # зберегти адресу повернення
# ... викликати інші функції тут ...
lw ra, 0(sp) # відновити її
addi sp, sp, 4 # повернути місце назад
ret

Кожен / перезаписує ra, тож якщо ваша функція викликає іншу функцію, внутрішній виклик знищує вашу адресу повернення, і ваш власний переходить не туди. Щоб запобігти цьому, резервує 4 байти в стеку, а зберігає ra там. Після завершення внутрішніх викликів відновлює адресу повернення, а повертає цей простір перед .

⚠
Якщо ви щось викликали, відновіть ra перед і завжди повертайте sp до значення, яке він мав на початку. Стек, який не відповідає своєму початковому стану, — звичайна причина збою в програмах на асемблері.