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



    Как говорит Википедия, 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;
    

    Спасибо за внимание!
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 20

      +3
      Вот здесь — www.coursera.org/course/compilers компилятор генерирует код как раз для MIPS.
        +1
        ну наверное самый правильный вариант собрать gcc для MIPS самому что то типа
        ./configure --target=mips-elf --enable-languages=c ... make
        В общем вот здесь описано www.linux-mips.org/wiki/Toolchains
      • UFO just landed and posted this here
          0
          Нет. $ra будет иметь адрес уже следующей инструкции, это да. Но она не выполняется.
          • UFO just landed and posted this here
            • UFO just landed and posted this here
                0
                Ух ты. Вот что значит учиться на эмуляторах.
                Спасибо.
                  0
                  Эмуляторы тоже реализуют delay-slot'ы.
              +1
              Вот это мне всегда разрывало наглухо мозг в MIPS, особенно если пытаешься что-то дизассемблировать. Идет код, условие, джамп после джампа еще условие и попробуй угадаи — за первым джампом инструкция относится к первому условию или второму :-\ Я после x86 ассма ваще не могу так работать ))
                +1
                ну это вообще то во многих RISC архитекторах. сделано чтобы не сбрасывать конвеер, и соответственно не терять лишние такты процессора
                  0
                  Да это понятно. Просто если надо чето-то где-то посмотреть в x86 — открыл, посмотрел, все как на ладоне. На уровне автомата, не задумываясь даже. Когда встречаешься с сабжем и надо чото где-то посмотреть и понять — тут уже приходится гуглить синтаксис, и вообще вникать в полную суть. Не привычно, короче говоря. Не плохо, не хорошо — просто не привычно.
                    +2
                    Тут конечно согласен! Все дело привычки, сам когда то впервые столкнулся с delay слотами на SPARCe и долго не мог понять. И 86 ассемблер конечно более известен и привычен. Но в результате после некоторой медитации, risc и становятся более простыми и логичными чем 86, по крайней мере мне так кажется.
            • UFO just landed and posted this here
                0
                Да, верно, прошу прощения.
                Перезалил картинку.
                +1
                эх, времена Sony Playstation, Playstation 2
                  +1
                  На счет syscall не очень понял. Если я правильно понимаю то это команда генерирующая системный вызов. Тогда почему это " используется для обращения к операционной системе для произведения действий, которые процессор сам не в состоянии выполнить'. И привели номера конкретных системных вызовов, может эти номера вызовов записаны в ABI и конкретно MARS их симулирует?
                  Сам сейчас занимаюсь переносом кода на MIPS нашего проекта, поэтому данная тема очень интересна. Спасибо за статью!
                    0
                    MARS эмулирует системные вызовы, он не делает их в реальности. Но процессы подготовки параметров и вывода результата близки к железным имплементациям.

                    Если бы использовали настоящий процессор, то выполняли бы системные вызовы к операционной системе, работающей на нашем процессоре.
                      0
                      Может тут всё же более «хардварный» вызов. Вроде «вызвать функцию номер 1...12». А что именно делает эта функция — уже забота ОС. В MARS может быть так, как вы описали. но другие вроде как не обязаны в точности следовать этому описанию, не?
                        +1
                        Тут наверное нужно сказать так, что в отличие от вызова функции (по номеру или по имени) системный вызов транслируется ядру ОС, соответственно первое — не нужно знать адрес функции при линковке программы, второе — процессор переходит в защищенный режим (режим ядра). Если первое можно сделать программно то для второго и предназначена команда syscall
                      0
                      У вас неправильная информация о регистрах. не пойму откуда вы взяли описание. чтобы было понятней что я имею ввиду. регистр fp имеет $30 значение. а у вас это s8 — не порядок.

                      Only users with full accounts can post comments. Log in, please.