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