Как работает рекурсивная функция в MIPS? - PullRequest
1 голос
/ 15 марта 2020

Я новичок ie в MIPS (поскольку я начал изучать сборку MIPS для своего колледжа), и у меня возникла проблема с пониманием того, как рекурсивная функция работает в MIPS.

Например, У меня есть эта программа (в C), чтобы написать ее в MIPS:

int fact (int n)
{
   if (n < 1) return 0;
   else return n * fact(n - 1);
}

Может ли кто-нибудь помочь мне с этим или другим примером рекурсивной функции и объяснить, как она работает?

1 Ответ

2 голосов
/ 15 марта 2020

Первое, что я хотел бы поделиться, это то, что сложность в переводе этого на MIPS связана с наличием простого вызова функции, а не с тем, что рекурсия задействована - то, что fact рекурсивно, ИМХО красный сельдь . С этой целью я проиллюстрирую нерекурсивную функцию, которая имеет всю сложность заявленной вами рекурсивной функции:

int fact (int n)
{
   if (n < 1) return 0;
   else return n * other(n - 1);    // I've changed the call to "fact" to function "other"
}

Мое изменение больше не является рекурсивным! Однако код MIPS для этой версии будет выглядеть идентично коду MIPS для вашего fact (за исключением, конечно, что jal fact, который меняет jal other). Это предназначено для иллюстрации того, что сложность в переводе связана с вызовом внутри функции и не имеет никакого отношения к тому, кто вызывается. (Хотя YMMV с методами оптимизации.)


Чтобы понять вызов функции, вам необходимо понять:

  • счетчик программы: как программа взаимодействует со счетчиком программы, особенно Конечно, в контексте вызова функции ..
  • передача параметров
  • регистрация соглашений, обычно

В C у нас есть явные параметры. Эти явные параметры, конечно, также появляются на ассемблере / машинном языке, но есть также параметры, переданные в машинном коде, которые не видны в коде C. Примерами этого являются значение адреса возврата и указатель стека.


Здесь необходим анализ функции (независимо от рекурсии):

Параметр n будет в $a0 при входе в функцию. Значение n требуется после вызова функции (до other), потому что мы не можем умножить, пока этот вызов функции не вернет правый операнд *.

Следовательно, n ( левый операнд на *) должен пережить вызов функции на other, а на $a0 этого не произойдет - поскольку наш собственный код перенаправит $a0 для вызова other(n-1), так как n-1 must go в $a0 для этого.

Кроме того, (в C, неявный) параметр $ra содержит значение адреса возврата, необходимое для возврата нашему абоненту. Вызов other аналогичным образом переназначит регистр $ra, уничтожив его предыдущее значение.

Следовательно, для этой функции (вашей или моей) нужны два значения, чтобы пережить вызов функции, который находится внутри ее body (например, вызов other).

Решение простое: значения, которые нам нужны (которые живут в регистрах, которые перепрофилированы или уничтожены чем-то, что мы делаем, или потенциально вызываемый) нужно переместить или скопировать в другое место: куда-нибудь, что переживет вызов функции.

Для этого можно использовать память, и мы можем получить немного памяти для этих целей, используя стек.

На основе на этом мы должны сделать стековый фрейм, в котором есть место для двух вещей, которые нам нужны (и в противном случае были бы уничтожены) после вызова other. Запись $ra должна быть сохранена (и позже перезагружена), чтобы мы могли использовать ее для возврата; Кроме того, первоначальное значение n необходимо сохранить, чтобы мы могли использовать его для умножения. (Кадры стека обычно создаются в прологе функции и удаляются в эпилоге функции.)


Как это часто бывает в машинном коде (или даже в программировании в целом), существуют и другие способы обработки вещей, хотя суть та же. (Это хорошо, и оптимизирующий компилятор, как правило, ищет наилучший способ, учитывая конкретные обстоятельства.)


Наличие или отсутствие рекурсии не меняет фундаментальный анализ, который нам нужен для перевода этого в сборку. / машинный язык. Рекурсия значительно увеличивает вероятность переполнения стека, но в остальном не меняет этот анализ.


Приложение

Чтобы быть ясным, рекурсия накладывает требование использовать динамически расширяемый стек вызовов - хотя все современные компьютерные системы предоставляют такой стек для вызовов, поэтому это требование легко забыть или скрыть в современных системах.

Для программ без рекурсии стек вызовов не является обязательным - локальные переменные могут быть выделены глобальным переменным, закрытым для функций (включая адрес возврата), и это было сделано в некоторых старых системах, таких как PDP-8, которые не предлагали Speci c Аппаратная поддержка для стека вызовов.


Системы, которые используют память стека для передачи параметров и / или регистр не работает, может не требовать анализа, описанного в этом ответе, так как переменные уже хранятся в памяти, которая выдерживает вызовы вложенных функций.

Именно разделение регистров на современных машинах с большим количеством регистров создает требование для вышеуказанного анализа. Эти машины, богатые регистрами, передают параметры и возвращаемые значения (в основном) в регистрах ЦП, что эффективно, но требует иногда делать копии, когда регистры переназначаются из одной функции в другую.

...