Как говорит Википедия, MIPS – микропроцессор, разработанный компанией MIPS Computer Systems (в настоящее время MIPS Technologies) и впервые реализованный 1985 году. Существует большое количество модификаций этой архитектуры, созданных для специально для 3D-моделирования, быстрой обработки чисел с плавающей запятой, многопотоковых вычислений. Различные варианты этих процессоров использутся в роутерах Cisco и Mikrotik, смартфонах, планшетах и игровых консолях.
Инсрукции MIPS достаточно просты для понимания, и именно с него рекомендуется начинать изучение ассемблера. Чем сейчас, собственно, и займёмся.
Структура программы на MIPS-ассемблере
Вот так выглядит классическая программа на MIPS-ассемблере.
Всё, что начинается на точку – это директивы. Директива .data
означает начало сегмента данных, .text
– начало сегмента кода.
Всё, после чего следует двоеточие, – это метки (v:
, main:
, loop
и endloop
).
Весь текст, следующий после знака #
– это комментарии.
А то, что остаётся – это, собственно, инструкции и псевдоинструкции (макросы).
.data
v: .word -1, -2, -3, -4, -5, -6, -7, -8, -9, -10
.text
.globl main
main:
li $t0, 0 # $t0 = 0 (variable a)
li $t1, 0 # $t1 = 0 (counter i)
li $t2, 10 # $t2 = 10 (count limit l)
loop:
slt $t3, $t1, $t2
beq $t3, $zero, fibucle
la $t3, V
sll $t4, $t1, 2
addu $t3, $t3, $t4
lw $t3, 0($t3)
addu $t0, $t0, $t3
addiu $t1, $t1, 1
b loop
endloop:
Типы в MIPS-ассемблере
Вот сравнительная таблица основных типов в C++ и в MIPS:
Как можно увидеть в таблице, выбор типа в для переменной в MIPS основывается только на объёме памяти, который будет занимать эта переменная. Обратите внимание, что MIPS в этом плане не различает signed- и unsigned-переменные.
Метки (символы)
В коде выше мы использовали несколько меток.
Метки (их ещё называют символами или этикетками) используются для того, чтоб давать «имена» адресам в памяти. Эти символы разделены на 2 больших класса: этикетки данных (адреса глобальных переменных, которые находятся в секции .data
, в этом случае v:
) и метки инструкций (адреса инструкций в секции .text
, например main:
, loop:
).
Данные в секции .data
обычно сохраняются в памяти начиная с адреса 0x10010000. Инструкции же хранятся начиная с адреса 0x00400000. Так как каждая инструкция MIPS-ассемблера занимает ровно 32 бита, следующая таблица «метка-адрес» будет верна для нашей программы:
С помощью меток очень удобно работать с глобальными переменными и другими данными из .data
, но об этом чуть позже.
Основные директивы
Мы уже рассмотрели несколько директив, а именно .data
и .text
, и уже известно, что первая предназначена для хранения данных и объявления глобальных переменных, а вторая – собственно для кода программы. Посмотрим на остальные директивы MIPS:
-
.globl sym
объявляет символ sym глобальным и позволяет обращатся к нему из других файлов;
-
.extern sym size
объявляет, что данные, которые хранятся в sym имеют размер size, и делает sym глобальной меткой (см. предыдущую директиву);
-
.ascii str
сохраняет строку str в памяти, не добавляя нулевой символ () в конец;
-
.asciiz str
сохраняет строку str и добавляет в конец нулевой символ ();
-
.byte b1, b2, ..., bn
последовательно сохраняет в памяти байты b1, b2, ..., bn;
-
.half h1, h2, ..., hn
последовательно сохраняет в памяти 16-битные значения h1, h2, ..., hn;
-
.word w1, w2, ..., wn
последовательно сохраняет в памяти 32-битные значения w1, w2, ..., wn;
-
.dword dw1, dw2, ..., dwn
последовательно сохраняет в памяти 64-битные значения dw1, dw2, ..., dwn;
-
.float f1, f2, ..., fn
сохраняет в памяти числа с плавающей запятой f1, f2, ..., fn;
-
.double d1, d2, ..., dn
сохраняет в памяти числа с плавающей запятой (двойная точность) d1, d2, ..., dn;
-
.space n
выделить n байт в данном сегменте данных;
-
.align n
выровнять все следующие данные до 2^n байт.
По поводу последней директивы: допустим, что в .data
мы написали .align 1
. В таком случае даже если мы запишем в память, например в адрес 0x10010000 какое-то значение размером в 1 байт, следующий байт будет оставлен пустым, и если мы захотим записать ещё один байт в память, он уже получит адрес 0x10010002. В MIPS по умолчанию включено автоматическое выравнивание данных, и поэтому можно записать 16-байтное значение (.half
) только в парный адрес памяти (0x10010000, 0x10010002, но не 0x10010003), 32-байтное значение – только в адрес, кратный 4, а 64-байтное – только в адрес, кратный 8.
Формат данных в .data
Данные в .data
записываются в достаточно свободной манере. Нужно просто указать метку, тип данных и значение. В этом коде несколько примеров корректной записи данных:
.data
var1: .byte 'A', 0xF3, 127, -1, 'n'
var2: .half -10, 0xffff
var3: .word 0x12345678
var4: .float 12.3, -0.1
var5: .double 1.5e-10
var6: .dword 0x1234567812345678
str1: .ascii “i love mipsn"
str2: .asciiz “zero-finished string"
array: .space 100
Немного глубже мы рассмотрим типы данных по мере их использования в коде.
Регистры
Одна основных частей MIPS-процессора – это регистры. В стандартном MIPS-процессоре имеется 32 основных регистра и ещё 32 в первом копроцессоре – модуле, который используется для вычислений с плавающей запятой. Каждый регистр имеет размер 32 байта, соответственно в один регистр целиком помещается одно значение типа int
. Для хранения переменной типа long
необходимо использовать сразу два регистра. К каждому регистру можно обратиться по его порядковому названию и по его общему названию. Общее – немного более human-readable. Имеются следующие регистры:
- $zero ($0) – регистр, всегда содержащий значение 0 и доступный только для чтения;
- $at ($1) – временный регистр процессора;
- $v0-$v1 ($2-$3) – для результатов, возвращаемых функциями;
- $a0-$a3 ($4-$7) – для аргументов функций;
- $t0-$t9 ($8-$15, $24-$25) – для временных данных, можно использовать как угодно;
- $s0-$s8 ($16-$23, $30) – для постоянных данных, можно использовать как угодно;
- $k0-$k1 ($26-$27) – зарезервировано для ядра операционной системы;
- $gp ($28) – поинтер для глобальных переменных, практически не используется;
- $sp ($29) – поинтер стека, его значение всегда равно верхнему адресу стека;
- $ra ($31) – бог солнца адрес инструкции, из которой была вызвана функция;
- $f0 – для результатов, возвращаемых функцями, с плавающей запятой;
- $f4, $f6, $f8, $f10, $f16, $f18 – для временных данных с плавающей запятой;
- $f12, $f14 – для параметров функций с плавающей запятой
Инструкции MIPS
Примечание. C этого момента мы будем рассматривать MIPS-процессор, его инструкции и дополнения на примере замечательного симулятора MIPS под названием MARS, который можно загрузить . Имплементация MIPS в этом симуляторе полностью соответствует стандартам.
В коде в начале статьи мы уже выделили все функциональные части программы и определили инструкции и псевдоинструкции как то, что не является комментарием, символом (меткой) или директивой. Псевдоинструкции также называют макросами, они трансформируются в одну или несколько инструкций во время выполнения кода. Вот пример макроса:
la rdest, addr
переходит в набор инструкций:
lui $at, hi(addr)
ori rdest, $at, lo(addr)
Как видно, MIPS-программы всегда записываются по одной инструкции на строчку.
Типы инструкций
Существует три основных типа инструкций MIPS-ассемблера:
- тип R (register). В роли операндов используются три регистра – регистр назначения (сокр. $rd), первый аргумент ($rs), и второй аргумент ($rt). Пример такой инструкции – сложение трёх регистров:
add $t2, $t0, $t1
. В данном случае в $t2 будет помещён результат сложения значений в $t0 и $t1. - тип I (immediate). Операнды – два регистра и число. Пример инструкции типа I:
addi $t3, $t2, 12
. После выполнения в регистр $t3 будет помещён результат сложения $t2 и числа 12. - Тип J (jump). Единственный операнд – 26-битный адрес, куда нужно перейти. Инструкция
j 128
перейдёт на адрес 128 в.text
.
Также существуют инструкции для копроцессоров, но их мы рассмотрим позже.
Инструкция syscall
syscall
– одна из самых простых, но в то же время одна из самых значимых инструкций MIPS-ассемблера. Это – служебная инструкция, поэтому она рассматривается отдельно от остальных. syscall
используется для обращения к операционной системе для произведения действий, которые процессор сам не в состоянии выполнить. Перед вызовом этой инструкции нужно положить в регистр $v0 служебный код – натуральное число от 1 до 12. В зависимости от кода операционная система будет производить одно или другое действие. Вот список служебных кодов и соответствующие им действия операционной системы, которые поддрерживает MARS:
Весь ввод и вывод происходит через консоль MARS'a.
Арифметические инструкции
Итак, рассмотрим некоторые основные арифметические инструкции. Будут использованы некоторые сокращения: rd
– регистр, куда пишется результат, rs
– первый аргумент, rt
– второй аргумент. Также может встретиться imm16
– 16-битное целое число или imm5
– 5-битное натуральное число.
-
add rd, rs, rt
сумма rs и rt записывается в регистр rd. Аккуратно, может вызвать переполнение.
-
sub rd, rs, rt
rd = rs — rt. Также можно получить переполнение.
-
addu rd, rs, rt
почти то же самое, что и предыдущая инструкция, но эта не может вызвать переполнение. Для арифметических вычислений предпочтительно использовать именно эту инструкцию.
-
subu rd, rs, rt
rd = rs — rt. Также без переполнения, и поэтому рекомендуется к использованию.
-
addi rt, rs, imm16
rt = rs + 16-битное целое число. Как и
add
, может вызывать переполнение. -
addiu rt, rs, imm16
то же самое, но без возможности переполнения. Use it.
Кстати, imm16 по умолчанию интерпретируются как позитивные. Например:
addiu $s1, $zero, 0xFFFF # $s1 = 0x0000FFFF (позитивное значение)
Если нужно добавить негативное значение, то нужно явно это указать:
addiu $s1, $zero, -0xFFFF # $s1 = 0xFFFF0001 (негативное значение в дополнении к 2)
Давайте посмотрим на реальные вычисления с помощью этих инструкций. Возьмём, к примеру, следующий код (на C++):
int f = (g+h) - (i-j);
И переведём этот код в MIPS-инструкции. Сначала нужно вычислить значение справа от знака '=', а потом присвоить его переменной f. Допустим, что переменная f у нас будет находиться в регистре $s0, g – в $s1, h – в $s2, i – в $s3, а j – в $s4. Вот что получается:
addu $t0, $s1, $s2 # t0 = s1 + s2 = g + h
subu $t1, $s3, $s4 # t1 = s3 - s4 = i - j
subu $s0, $t0, $t1 # s0 = f = t0 - t1 = (g+h) - (i-j)
А теперь можно протестировать получившийся код в MARS. Загрузите черновик вот отсюда и откройте его в MARS:
java -jar Mars_4_2.jar
Допишите код вместо комментария. Теперь можно его выполнить. Сначала выберите Run -> Assemble:
Теперь снимите галочку с пункта Hexadecimal Values, чтобы увидеть десятичные значения в регистрах, и выберите Run -> Go:
Значение в $s0 после выполнения программы должно быть равно 12.
Продолжение следует
В следующей статье рассмотрим логические инструкции, а также умножение и деление целых чисел. В ней же попробуем работать с памятью и стеком. А пока предлагаю вам попробовать переписать вот этот код на MIPS-ассемблер:
int a = b + c;
int d = e + f;
int g = a + b;
int h = g + d;
Спасибо за внимание!
Автор: appplemac