Pull to refresh

Изучаем MIPS-ассемблер

Reading time 7 min
Views 64K


Как говорит Википедия, 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 основывается только на объёме памяти, который будет занимать эта переменная. Обратите внимание, что 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 в памяти, не добавляя нулевой символ (\0) в конец;
  • .asciiz str
    сохраняет строку str и добавляет в конец нулевой символ (\0);
  • .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 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). Пример такой инструкции – сложение трёх регистров:
    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:

Таблица syscall

Весь ввод и вывод происходит через консоль 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 rd, rs, imm16
    rt = rs + 16-битное целое число. Как и add, может вызывать переполнение.
  • addiu rd, 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:

MARS Assemble operation

Теперь снимите галочку с пункта Hexadecimal Values, чтобы увидеть десятичные значения в регистрах, и выберите Run -> Go:

MARS Run operation

Значение в $s0 после выполнения программы должно быть равно 12.

Registers after execution

Продолжение следует


В следующей статье рассмотрим логические инструкции, а также умножение и деление целых чисел. В ней же попробуем работать с памятью и стеком. А пока предлагаю вам попробовать переписать вот этот код на MIPS-ассемблер:
int a = b + c;
int d = e + f;
int g = a + b;
int h = g + d;

Спасибо за внимание!
Tags:
Hubs:
+63
Comments 20
Comments Comments 20

Articles