Материал: Архитектура и программирование MIPS-процессоров. Разинкин К.А

Внимание! Если размещение файла нарушает Ваши авторские права, то обязательно сообщите нам

lw

$s0, 8($sp)

# restore $s0 from stack

addi $sp, $sp, 12

# deallocate stack space

jr

$ra

# return to caller

На рис. 2.15 показан стек до, во время и после вызова функции diffofsums из вышеизложенного примера кода

Рис. 2.15. Стек до (a), во время (b) и после (c) вызова функции diffofsums

Функция diffofsums выделяет пространство на стеке для трёх слов, уменьшая указатель стека $sp на 12. Затем она сохраняет текущие значения $s0, $t0 и $t1 в выделенном пространстве. Дальше выполняется остальная часть функции, которая меняет значения этих трёх регистров. В завершение, функция diffofsums восстанавливает значения регистров $s0, $t0 и $t1 из стека, освобождает пространство на стеке и возвращается в main. Когда функция выполняет возврат, в регистре $v0 находится её результат, но другие побочные эффекты отсутствуют: в регистрах $s0, $t0, $t1 и $sp находятся те же значения, которые там и были до вызова функции.

Оберегаемые регистры

В примере кода 6.26 предполагается, что временные регистры $t0 и $t1 следует сохранять и восстанавливать. Если вызывающая функция не использует эти регистры, то усилия по

81

сохранению и восстановлению их значений тратятся впустую. Чтобы избежать этих

издержек, в архитектуре MIPS регистры разделены на две категории: оберегаемые (англ.: preserved) и необерегаемые (англ.: nonpreserved).

Оберегаемые регистры включают $s0–$s7 (отсюда их название: сохраняемые, англ.: saved). Необерегаемые регистры включают $t0–$t9 (отсюда их название: временные, англ.: temporary). Функция должна сохранять и восстанавливать любые оберегаемые регистры, с которыми она собирается работать, но может свободно менять значения необерегаемых регистров.

В представленном ниже примере кода показана улучшенная версия функции diffofsums, которая сохраняет на стеке только регистр $s0. Регистры $t0 и $t1 являются необерегаемыми регистрами, поэтому их сохранять не обязательно

Пример. Функция, сохраняющая оберегаемые регистры на

стеке

Код на языке ассемблера MIPS

# $s0 = result diffofsums:

addi $sp, $sp, −4 # make space on stack to store one regis-

ter

sw

$s0, 0($sp)

# save $s0 on stack

add

$t0, $a0, $a1

# $t0

= f + g

add

$t1, $a2, $a3

# $t1

= h + i

sub $s0, $t0, $t1

# result = (f + g) − (h + i)

add

$v0, $s0, $0

# put return value in $v0

lw

$s0, 0($sp)

# restore $s0 from stack

addi $sp, $sp, 4 # deallocate stack space jr $ra # return to caller

82

Вспомним, что когда одна функция вызывает другую, то первая называется вызывающей функцией, а вторая – вызываемой.

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

В таблице 2.1 приведены все оберегаемые регистры. Регистры $s0–$s7 обычно используют для хранения локальных переменных внутри функции, поэтому они должны быть сохранены. Регистр $ra также следует сохранять, чтобы функция знала, куда возвращаться. Регистры $t0–$t9 используют для хранения временных результатов перед тем, как присвоить эти значения локальным переменным. Вычисления, использующие временные результаты, обычно завершаются до того, как вызывается функция, поэтому эти регистры не оберегаются, а необходимость сохранять их в вызывающей функции возникает крайне редко. Регистры $a0–$a3 часто перезаписываются в процессе вызова функции, поэтому вызывающая функция должна сохранять их, если эти значения могут понадобиться ей после завершения вызванной функции.

Регистры $v0–$v1, очевидно, не следует оберегать, потому что в них вызываемая функция возвращает свой результат.

83

 

Таблица 2.1

Оберегаемые и необерегаемые регистры

Оберегаемые

Необерегаемые

Сохраняемые регистры: $s0–

Временные регистры: $t0–$t9

$s7

 

Адрес возврата: $ra

Регистры аргументов: $a0–$a3

Указатель стека: $sp

Возвращаемые значения: $v0–

 

$v1

Содержимое стека

Стек ниже указателя стека

выше указателя стека

 

Стек выше указателя стека автоматически остаётся в сохранности, если только вызываемая функция не осуществляет запись в память по адресам выше $sp. При таком подходе она не меняет кадры стека (англ.: stack frames) других функций. Сам указатель стека остаётся в сохранности потому, что вызываемая функция перед завершением работы освобождает свой кадр стека, прибавляя к $sp то же значение, которое вычла из него в начале.

Рекурсивные вызовы функций

Функция, которая не вызывает другие функции, называется листовой, или терминальной, функцией (англ.: leaf function); пример – функция diffofsums. Функция, которая вызывает другие функции, называется нелистовой (или, соответственно, нетерминальной, англ.: nonleaf function). Как было замечено ранее, нелистовые функции устроены более сложно, потому что перед вызовом других функций им приходится сохранять необерегаемые регистры на стеке и затем восстанавливать эти регистры. А именно, вызывающая функция сохраняет любые необерегаемые регистры ($t0–$t9 и $a0–$a3), значения которых будут нужны ей после вызова. Вызываемая функция сохраняет любые оберегаемые регистры ($s0–$s7 и $ra), которые собирается изменять.

Рекурсивная функция – это нелистовая функция, вызывающая сама себя. Функция вычисления факториала может

84

быть реализована в виде рекурсивной функции. Вспомним, что factorial(n) = n × (n – 1) × (n – 2) ×… × 2 × 1. Функция factorial

в рекурсивном представлении выглядит как factorial(n) = n × factorial(n – 1).

Факториал от 1 – это просто 1. В примере следующем примере показана функция factorial, записанная в рекурсивном виде. Для удобства предполагаем, что программа начинается с адреса 0x90.

Пример Рекурсивный вызов функции factorial Код на языке высокого уровня

int factorial(int n) { if (n <= 1)

return 1; else

return (n * factorial(n − 1));

}

Код на языке ассемблера MIPS

0x90

factorial: addi $sp, $sp, −8 # make room on stack

0x94

sw

$a0, 4($sp)

# store $a0

0x98

sw

$ra, 0($sp)

# store $ra

0x9C

addi $t0, $0, 2

# $t0 = 2

0xA0

slt

$t0, $a0, $t0 # n <= 1 ?

0xA4

beq $t0, $0, else # no: goto else

0xA8

addi $v0, $0, 1

# yes: return 1

0xAC

addi $sp, $sp, 8

# restore $sp

0xB0

jr

$ra

# return

0xB4

else: addi $a0, $a0, −1 # n = n − 1

0xB8

jal

factorial

# recursive call

0xBC

Iw

$ra, 0($sp)

# restore $ra

0xC0

Iw

$a0, 4($sp)

# restore $a0

0xC4

addi $sp, $sp, 8

# restore $sp

0xC8

mul $v0, $a0, $v0 # n * factorial(n−1)

0xCC

jr

$ra

# return

Функция factorial изменяет регистры $a0 и $ra, поэтому она сохраняет их на стеке. Затем она проверяет условие n < 2. Если условие выполнено, она помещает значение 1 в регистр

85

Источник: https://studfile.net/preview/16568788/