Пример. Адресация относительно счетчика команд
Вычислите значение непосредственного операнда и запишите машинный код для инструкции bne в программе, приведенной ниже.
0x40 loop: add $t1, $a0, $s0
0x44 lb $t1, 0($t1)
0x48 add $t2, $a1, $s0 0x4C sb $t1, 0($t2) 0x50 addi $s0, $s0, 1 0x54 bne $t1, $0, loop 0x58 lw $s0, 0($sp)
Решение. На рис. 2.19 показан машинный код инструкции bne. Ее целевой адрес ветвления равен 0x40 и находится на 6 инструкций раньше, чем значение PC + 4 (0x58), поэтому непосредственный операнд в этом случае равен – 6.
Рис. 2.19. Машинный код для инструкции bne из примера 6.8
Псевдопрямая адресация
При прямой адресации адрес перехода задаётся внутри инструкции. Инструкции безусловного перехода j и jal в идеале могли бы использовать прямую адресацию для определения 32-битного целевого адреса перехода (англ.: jump target address, JTA), указывающего адрес инструкции, которая будет выполнена следующей.
К сожалению, в формате инструкций типа J нет достаточного количества бит для того, чтобы задать полный 32битный адрес перехода. Шесть старших бит инструкции занимает код операции (поле opcode), поэтому для адреса перехода остаётся только 26 бит.
91
К счастью, два младших бита адреса перехода (JTA1:0) всегда должны быть равны нулю, потому что все инструкции выровнены по словам.
Следующие 26 бит адреса перехода (JTA27:2) берутся из поля addr инструкции. Четыре старших бита адреса перехода (JTA31:28) берутся из четырёх старших бит значения PC + 4. Такой способ адресации называется псевдопрямым.
В приведенном ниже примере кода показана инструкция jal, использующая псевдопрямую адресацию. Целевой адрес безусловного перехода этой инструкции равен 0x004000A0, а ее машинный код показан на рис. 2.20. Четыре старших и два младших бита целевого адреса перехода отбрасываются, а оставшиеся биты помещаются в 26-битное поле адреса (addr).
Пример. Вычисление целевого адреса перехода
0x0040005C jal sum
...
0x004000A0 sum: add $v0, $a0, $a1
Процессор вычисляет целевой адрес перехода инструкции типа J, добавляя два нуля в конец и четыре старших бита значения PC + 4 в начало 26-битного поля addr.
Рис. 2.20. Машинный код инструкции jal
Так как четыре старших бита адреса перехода берутся из значения PC + 4, то длина такого перехода ограничена.
Все инструкции типа J (j и jal) используют псевдопрямую адресацию. Обратите внимание, что инструкция безусловного перехода по регистру (jr) не является инструкцией типа J. Она является инструкцией типа R и выполняет переход по 32битному адресу, находящемуся в регистре rs.
92
2.8. Компиляция, ассемблирование и запуск программы
Ранее мы показали, как небольшие фрагменты кода, написанного на языке высокого уровня, транслируются в ассемблерный и машинный коды. В этом разделе мы рассмотрим, как происходит компиляция и ассемблирование целой программы, написанной на языке высокого уровня, и покажем, как загрузить ее в память компьютера для выполнения.
Мы начнем с рассмотрения карты памяти MIPS, описывающей расположение кода, данных и стека в памяти. Затем на примере покажем этапы выполнения кода программы.
Карта памяти
Так как архитектура MIPS использует 32-битные адреса, то размер адресного пространства составляет 232 байта = 4 гигабайта (Гбайт). Адреса слов кратны 4 и располагаются в промежутке от 0 до 0xFFFFFFFC. На рис. 2.21 изображена карта памяти MIPS.
Адресное пространство разделено на четыре части, или сегмента: сегмент кода, сегмент глобальных данных, сегмент динамических данных и зарезервированный сегмент.
Рис. 2.21. Карта памяти MIPS
93
Сегмент кода
Сегмент кода (англ.: text segment) содержит машинные команды исполняемой программы. Его размер достаточен для размещения почти 256 Мбайт кода. Обратите внимание, что четыре старших бита адреса в сегменте кода всегда равны нулю, что позволяет использовать инструкцию j для перехода по любому адресу в программе.
Сегмент глобальных данных
Сегмент глобальных данных (англ.: global data segment) содержит глобальные переменные, которые, в отличие от локальных переменных, находятся в области видимости всех функций программы. Глобальные переменные инициализируются при загрузке программы, но до начала ее выполнения. В программе на языке Си эти переменные объявляются вне функции main и доступны для всех функций. Размер этого сегмента позволяет разместить 64 Кбайт глобальных переменных.
Доступ к глобальным переменным осуществляется при помощи глобального указателя ($gp), который инициализируется значением 0x100080000. В отличие от указателя стека ($sp), $gp не меняется во время выполнения программы. Любая глобальная переменная доступна при помощи 16-битного положительного или отрицательного смещения относительно $gp. Во время ассемблирования смещение уже известно, так что для доступа к глобальным переменным можно использовать режим базовой адресации с константными смещениями.
Сегмент динамических данных
Сегмент динамических данных (англ.: dynamic data segment) содержит стек и кучу. В момент запуска программы этот сегмент не содержит данных – они динамически выделяются и освобождаются в нем в процессе выполнения программы. Сегмент динамических данных – это самый большой сег-
94
мент памяти, используемый программой; его размер составляет почти 2 Гбайт.
Cтек используется для сохранения и восстановления регистров, используемых функциями, а также хранения локальных переменных, таких как массивы. Стек растет вниз от верхней границы сегмента динамических данных (0x7FFFFFFC), а доступ к кадрам стека осуществляется в режиме очереди LIFO («последним пришел – первым ушел»).
Куча (англ.: heap) хранит блоки памяти, динамически выделяемые программе во время работы. В языке C выделение памяти осуществляется функцией malloc; в C++ и Java для этого служит функция new. Как и в случае кучи одежды на полу комнаты в общежитии, данные, находящиеся в куче, можно использовать и выбрасывать в произвольном порядке. Куча растет вверх от нижней границы сегмента динамических данных. Если стек и куча прорастут друг в друга, данные программы могут быть повреждены. Функция выделения памяти стремится избежать этой ситуации. Она возвращает ошибку нехватки памяти (англ.: out-of-memory error), если свободной памяти недостаточно для размещения новых динамических данных.
Зарезервированный сегмент
Зарезервированный сегмент используется операционной системой и не может использоваться непосредственно программой. Часть зарезервированной памяти используется для прерываний и для отображения устройств ввода-вывода в адресное пространство.
Трансляция и запуск программы
На рис. 2.22 показаны этапы, необходимые для трансляции в машинный язык и начала выполнения программы, написанной на языке высокого уровня. Высокоуровневый код компилируется в код на языке ассемблера, который затем ассемблируется в машинный код и сохраняется в виде объектного файла. Компоновщик, также называемый редактором связей
95