[Из песочницы] Написание простого процессора и окружения для него

Здравствуйте! В этой статье я расскажу какие шаги нужно пройти для создания простого процессора и окружения для него.

Для начала нужно определиться с тем, каким будет процессор. Важны такие параметры как:

Архитектуры процессоров можно разделить по размеру инструкций на 2 вида (на самом деле их больше, но другие варианты менее популярны):

Основное их отличие в том, что RISC процессоры имеют одинаковый размер инструкций. Их инструкции простые и выполняются сравнительно быстро, тогда как CISC процессоры могут иметь разный размер инструкций, некоторые из которых могут выполняться достаточно продолжительное время.

Я решил сделать RISC процессор во многом похожий на MIPS.

Я это сделал по целому ряду причин:


  • Довольно просто создать прототип такого процессора.
  • Вся сложность такого вида процессоров перекладывается на такие программы как ассемблер и/или компилятор.

Вот основные характеристики моего процессора:


  • Машинное слово и размер регистров — 32 бита
  • 64 регистра (включая счетчик команд)
  • 2 типа инструкций

Register type(досл. Регистровый тип) выглядит вот так:

rtype

Особенность таких инструкций заключается в том, что они оперируют с тремя регистрами.

Immediate type(досл. Немедленный тип):

itype

Инструкции этого типа оперируют с двумя регистрами и числом.

OP — это номер инструкции, которую нужно выполнить (или же для указания, что эта инструкция Register type).

R0, R1, R2 — это номера регистров, которые служат операндами для инструкции.

Func — это дополнительное поле, которое служит для указания вида Register type инструкций.

Imm — это поле куда записывается то значение, которое мы хотим явно предоставить инструкции в качестве операнда.


  • Всего 28 инструкций

Полный список инструкций можно посмотреть в github репозитории.

Вот лишь пару из них:

nor r0, r1, r2

NOR это Register type инструкция, которая делает логическое ИЛИ НЕ на регистрах r1 и r2, после записывает результат в регистр r0.

Для того, чтобы использовать эту инструкцию нужно изменить поле OP на 0000 и поле Func на 0000000111 в двоичной системе счисления.

lw r0, n(r1)

LW это Immediate type инструкция, которая загружает значение памяти по адресу r1 + n в регистр r0.

Для того, чтобы использовать эту инструкцию в свою очередь нужно изменить поле OP на 0111, а в поле IMM записать число n.

После создания ISA можно приступить к написанию процессора.

Для этого нам нужно знание какого нибудь языка описания оборудования. Вот некоторые из них:


  • Verilog
  • VHDL (не путать с предыдущим!)

Я выбрал Verilog, т.к. программирование на нем было частью моего учебного курса в университете.

Для написания процессора нужно понимать логику его работы:


  1. Получение инструкции по адресу Счетчика команд (PC)
  2. Декодирование инструкции
  3. Выполнение инструкции
  4. Прибавление к Cчетчику команды размера выполненной инструкции

И так до бесконечности.

Получается нужно создать несколько модулей:

Разберем по отдельности каждый модуль.


Регистровый файл

Регистровый файл предоставляет доступ к регистрам. С его помощью нужно получать значения каких то регистров, или изменять их.

В моем случае у меня 64 регистра. В один из регистров записывается результат операции над двумя другими, так что мне нужно предоставить возможность изменять только один, а получать значения из двух других.


Декодер

Декодер это тот блок, который отвечает за декодирование инструкций. Он указывает какие операции нужно выполнить АЛУ и другим блокам.

Например, инструкция addi должна сложить значение регистра $zero(Он всегда хранит 0) и 20 и положить результат в регистр $t0.

addi $t0, $zero, 20

На этом этапе декодер определяет, что эта инструкция:


  • Immediate type
  • Должна записать результат в регистр

И передает эти сведения следующим блокам.


АЛУ

После управление переходит в АЛУ. В нем обычно выполняются все математические, логические операции, а также операции сравнения чисел.

То есть, если рассмотреть ту же инструкцию addi, то на этом этапе происходит сложение 0 и 20.


Другие

По мимо вышеперечисленных блоков, процессор должен уметь:


  • Получать и изменять значения в памяти
  • Выполнять условные переходы

Тут и там можно увидеть как это выглядит в коде.

После написания процессора нам нужна программа, которая бы преобразовывала текстовые команды в машинный код, чтобы не делать этого вручную. Поэтому нужно написать ассемблер.

Я решил реализовать его на языке программирования Си.

Так как мой процессор имеет RISC архитектуру, то для того, чтобы упростить себе жизнь, я решил спроектировать ассемблер так, чтобы в него можно было легко добавлять свои псевдоинструкции (комбинации из нескольких элементарных инструкций или из других псевдоинструкций).

Можно реализовать это с помощью структуры данных, хранящей в себе тип инструкции, ее формат, указатель на функцию, которая возвращает машинные коды инструкции, и ее название.

Обычная программа начинается с объявления сегмента.

Для нас достаточно двух сегментов .text — в котором будет храниться исходный код наших программ — и .data — в котором будет хранится наши данные и константы.

Инструкция может выглядеть вот так:

.text
    jie $zero, $zero, $zero # Ветвление
    addi $t1, $zero, 2 # $t1 = $zero + 2
    lw $t1, 5($t2) # $t1 = *($t2 + 5)
    syscall 0, $zero, $zero # syscall(0, 0, 0)
    la $t1, label# $t1 = label

Сначала указывается название инструкции, потом операнды.

В .data же указываются объявления данных.

.data
    .byte 23        # Константа размером 1 байт
    .half 1337      # Константа размером 2 байта
    .word 69000, 25000  # Константы размером 4 байта
    .asciiz "Hello World!"  # Константная нуль терминируемая строка (Си строка)
    .ascii  "12312009"  # Константная строка (без терминатора)
    .space 45       # Пропуск 45 байтов

Объявление должно начинаться с точки и названия типа данных, после же идут константы или аргументы.

Удобно парсить (сканировать) ассемблер файл в таком виде:


  1. Сначала сканируем сегмент
  2. Если это .data сегмент, то мы парсим разные типы данных или .text сегмент
  3. Если это .text сегмент, то мы парсим команды или .data сегмент

Для работы ассемблеру нужно проходить исходный файл 2 раза. В первый раз он считает по каким смещениям находятся ссылки (они служат для), они обычно выглядят вот так:

    la  $s4,    loop          # Загружаем адрес loop в s4

loop:   # Ссылка!

    mul $s2, $s2, $s1   # s2 = s2 * s1
    addi $s1, $s1, -1    # s1 = s1 - 1
    jil $s3, $s1, $s4   # если s3 < s1 то перейди на метку 

А во второй проход можно уже и генерировать файл.

В дальнейшем, можно запускать выходной файл из ассемблера на нашем процессоре и оценивать результат.

Также готовый ассемблер можно использовать в Си компиляторе. Но это уже позже.

Ссылки:


© Habrahabr.ru