
Как говорит Википедия, 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, imm16
add
, может вызывать переполнение.
то же самое, но без возможности переполнения. 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;
Спасибо за внимание!