Функции и стек
Функция — это именованный блок инструкций, который можно использовать более одного раза. Вы переходите к ней, она выполняет свою задачу и возвращает управление в место, откуда вы её вызвали. Это позволяет написать фрагмент логики один раз и использовать его из многих мест вместо повторения одних и тех же строк по всей программе.
Используйте , чтобы вызвать функцию: она переходит к метке и сохраняет адрес возврата в ra. Функция завершается , который переходит обратно по адресу в ra. и — это псевдоинструкции: удобные имена, которые ассемблер разворачивает в реальные инструкции, и .
Аргументы помещаются в a0, a1 и следующие за ними регистры. Возвращаемое значение помещается в a0. Такое соглашение — это соглашение о вызовах, общий договор, который позволяет отдельным частям кода работать вместе.
li a0, 6 # аргументcall square # вызови, и ra помнит, куда вернутьсяli a7, 1 # square оставляет результат в a0ecallli a7, 10ecallsquare:mul a0, a0, a0 # a0 = a0 * a0ret # возвращается к ra
Проследите вызов. помещает аргумент 6 в a0. переходит к метке square и сохраняет адрес возврата в ra. Внутри функции вычисляет 6 * 6 = 36 и оставляет результат в a0 — регистре, используемом для возвращаемых значений. переходит обратно к инструкции сразу после вызова, где и печатают 36, после чего программа завершается.
Вызывающий собирается вызвать 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 резервирует новое пространство, которое безопасно использовать. Когда вы извлекаете значения обратно, вы должны делать это в порядке, обратном тому, в котором вы их сохраняли, иначе поток выполнения программы будет нарушен.
addi sp, sp, -4 # освобождает место для одного словаsw ra, 0(sp) # сохраняет адрес возврата# ... вызывать другие функции здесь ...lw ra, 0(sp) # восстанавливает егоaddi sp, sp, 4 # освобождает местоret
Каждый / перезаписывает ra, поэтому если ваша функция вызывает другую функцию, внутренний вызов уничтожает ваш адрес возврата, и ваш собственный переходит не туда. Чтобы предотвратить это, резервирует 4 байта в стеке, а сохраняет ra там. После завершения внутренних вызовов восстанавливает адрес возврата, а возвращает это пространство перед .
ra перед и всегда возвращайте sp к значению, которое было у него в начале. Стек, не соответствующий своему начальному состоянию, — обычная причина сбоя в программах на ассемблере.