Функции и стек

Функции и стек

Функция — это именованный блок инструкций, который можно использовать более одного раза. Вы переходите к ней, она выполняет свою задачу и возвращает управление в место, откуда вы её вызвали. Это позволяет написать фрагмент логики один раз и использовать его из многих мест вместо повторения одних и тех же строк по всей программе.

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

Вызов функции и возврат
caller
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 к значению, которое было у него в начале. Стек, не соответствующий своему начальному состоянию, — обычная причина сбоя в программах на ассемблере.