Лекция 4
Вопрос про endianness - little endian
Зачем align 0? - для макросов
Задача повторного использования исходного кода.
Два подхода: 1. макроподход: переиспользование кусков исходного кода; 2. функции: аппаратная поддержка переиспользования одного и того же фрагмента кода (хранящегося в единственном экземпляре).
Напомню ещё раз, что имеющиеся на сегодняшний день компьютерные архитектуры - результат решения определённых задач в ограниченных условиях около 50 лет назад.
Куда сохранять адрес возврата? Вариантов решения этой задачи много: 1. специальный регистр - program counter; но в MIPS его нет; 2. сохранять адрес возврата в произвольный, удобный нам, регистр. 3. сохранять адрес возврата куда-то в память.
У подхода 2 есть плюсы: - это быстро - не нужно придумывать, в какую ячейку памяти сохранять значение
В MIPS есть для этого есть команды jal и jr, а также регистр $ra
См. в лекции пример программы, определяющей, существует ли треугольник с заданными сторонами
Недостаток такого подхода - невозможность вызова вложенной подпрограммы
Концевая подпрограмма (?) - программа, из которой далее не будет вызова подпрограмм
Ещё одна проблема - локальность меток
И ещё проблема - передача параметров в подпрограмму
Здесь мы разделяем понятия рекурсивного вызова и вложенного вызова
При вложенном вызове не-концевой подпрограммы можно сохранить адрес возврата в памяти, но с этим есть проблемы
Словом, конвенций можно понапридумывать много и очень разных
Для примера приведу конвенцию для концевой подпрограммы:
Мы заранее договариваемся: 1. какие регистры используются для передачи параметров в подпрограмму 2. не вызывать никаких подпрограмм из себя 3. возвращаем управление с помощью регистра $ra 4. какие регистры используются для возврата результата 5. какие регистры можно модифицировать, а какие - нет
Отсюда и берутся MIPS-регистры t (temporary) и s (saved)
$v0 и $v1 - регистры для возврата значений (иногда нужно два) $a0-$a3 - для передачи параметров
Ещё можно договориться о том, чтобы не использовать регистры $v0 и $v1 в вычислениях, если это возможно
Пример оформленной в соответствии с конвенциями программы см. в лекции
Однако как всё-таки нам сохранять актуально бесконечное количество адресов возврата (т.е. справиться с вложенными вызовами и рекурсией)?
Для этого вводится понятие стека (stack)
Стек - это абстракция типа LIFO (last in, first out)
В архитектуре фон Неймана (и во многих существующих) положить в стек или снять с него можно только по машинному слову за раз
Вот так мы внезапно и открыли для себя динамическую память
В принципе, аппаратных реализаций этого механизма можно придумать очень-очень много; например, завести отдельную память для стека, непохожую на память кучи
Пример этого - регистровое окно (RISC-V, Эльбрус)
В MIPS этого нет, тут адрес возврата просто кладётся в память
При этом отдельной команды для того, чтобы положить/снять со стека, в языке ассемблера MIPS нет! (Потому что эта команда подразумевает две операции: уменьшить stack pointer и положить значение)
Что такое "уменьшить значение регистра"? Нужно прочитать значение регистра, изменить его и положить обратно
Для этого в MIPS есть регистр sp (stack pointer) (тем не менее, это регистр общего назначения), который по конвенции никто никогда не использует для других целей
stack pointer в начале работы программы указывает несколько ниже дна стека (до тела основной программы это место уже есть, чем занять)
Пример работы со стеком см. в лекции
Последовательность действий: 1. уменьшить значение $sp на 4 2. положить значение 3. работать дальше 4. потом не забыть вернуть $sp на место
Словом, работа со стеком в MIPS - это следование конвенциям работы с памятью, при этом специальных директив (команд) для этого нет
Обратите внимание, что обращаться мы можем не только к нулевому, но и вообще к элементам стека
При этом от того, что мы изменяем указатель стека (уменьшаем или увеличиваем), содержимое стека не меняется
Локальные переменные подпрограммы также следует размещать на стеке
Подозреваю, что локальный namespace изобрели позже, чем заполнение/опустошение стека
Наша программа должна сохранить в стеке, как минимум, адрес возврата (пример вызова подпрограммы, сохраняющей адрес возврата, см. в лекции)
Можете смело закрывать книгу, в которой пример рекурсии приводится на вычислении факториала; но мы всё же прибегнем к этому примеру (см. в лекции)
Рекурсия в любом случае дороже, чем цикл
Нам нужно сохранять в самом начале подпрограммы не только адрес возврата, но и все s-регистры, которые мы собираемся использовать
Получается, к функции у нас добавляются понятия пролога (сохранение значений из вызывающей подпрограммы на стеке) и эпилога (восстановления испорченных значений)
Кроме того, пролог и эпилог функции одинаковы для всех функций, которые "портят" значения одних и тех же регистров (имеются в виду s-регистры, $ra и $sp)
Полный текст конвенции см. в тексте занятия.