
Как говорит Википедия, 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, endloop
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:
объявляет символ sym глобальным и позволяет обращатся к нему из других файлов;.globl sym
объявляет, что данные, которые хранятся в sym имеют размер size, и делает sym глобальной меткой (см. предыдущую директиву);.extern sym size
сохраняет строку str в памяти, не добавляя нулевой символ (\0) в конец;.ascii str
сохраняет строку str и добавляет в конец нулевой символ (\0);.asciiz str
последовательно сохраняет в памяти байты b1, b2, ..., bn;.byte b1, b2, ..., bn
последовательно сохраняет в памяти 16-битные значения h1, h2, ..., hn;.half h1, h2, ..., hn
последовательно сохраняет в памяти 32-битные значения w1, w2, ..., wn;.word w1, w2, ..., wn
последовательно сохраняет в памяти 64-битные значения dw1, dw2, ..., dwn;.dword dw1, dw2, ..., dwn
сохраняет в памяти числа с плавающей запятой f1, f2, ..., fn;.float f1, f2, ..., fn
сохраняет в памяти числа с плавающей запятой (двойная точность) d1, d2, ..., dn;.double d1, d2, ..., dn
выделить n байт в данном сегменте данных;.space n
выровнять все следующие данные до 2^n байт..align 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 mips\n"
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). Пример такой инструкции – сложение трёх регистров:
В данном случае в $t2 будет помещён результат сложения значений в $t0 и $t1.add $t2, $t0, $t1 - тип I (immediate). Операнды – два регистра и число. Пример инструкции типа I:
После выполнения в регистр $t3 будет помещён результат сложения $t2 и числа 12.addi $t3, $t2, 12 - Тип J (jump). Единственный операнд – 26-битный адрес, куда нужно перейти. Инструкция
перейдёт на адрес 128 вj 128.text.
Также существуют инструкции для сопроцессоров, но их мы рассмотрим позже.
Инструкция syscall
syscall – одна из самых простых, но в то же время одна из самых значимых инструкций MIPS-ассемблера. Это – служебная инструкция, поэтому она рассматривается отдельно от остальных. syscall используется для обращения к операционной системе для произведения действий, которые процессор сам не в состоянии выполнить. Перед вызовом этой инструкции нужно положить в регистр $v0 служебный код – натуральное число от 1 до 12. В зависимости от кода операционная система будет производить одно или другое действие. Вот список служебных кодов и соответствующие им действия операционной системы, которые поддрерживает MARS:
Весь ввод и вывод происходит через консоль MARS'a.
Арифметические инструкции
Итак, рассмотрим некоторые основные арифметические инструкции. Будут использованы некоторые сокращения:
rd – регистр, куда пишется результат, rs – первый аргумент, rt – второй аргумент. Также может встретиться imm16 – 16-битное целое число или imm5 – 5-битное натуральное число.
сумма rs и rt записывается в регистр rd. Аккуратно, может вызвать переполнение.add rd, rs, rt
rd = rs — rt. Также можно получить переполнение.sub rd, rs, rt
почти то же самое, что и предыдущая инструкция, но эта не может вызвать переполнение. Для арифметических вычислений предпочтительно использовать именно эту инструкцию.addu rd, rs, rt
rd = rs — rt. Также без переполнения, и поэтому рекомендуется к использованию.subu rd, rs, rt
rt = rs + 16-битное целое число. Как иaddi rd, rs, imm16add, может вызывать переполнение.
то же самое, но без возможности переполнения. Use it.addiu rd, rs, imm16
Кстати, 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;
Спасибо за внимание!
