[Из песочницы] Написание простого процессора и окружения для него
В этой статье я расскажу какие шаги нужно пройти для создания простого процессора и окружения для него. Здравствуйте!
Важны такие параметры как: Для начала нужно определиться с тем, каким будет процессор.
Архитектуры процессоров можно разделить по размеру инструкций на 2 вида (на самом деле их больше, но другие варианты менее популярны):
Их инструкции простые и выполняются сравнительно быстро, тогда как CISC процессоры могут иметь разный размер инструкций, некоторые из которых могут выполняться достаточно продолжительное время. Основное их отличие в том, что RISC процессоры имеют одинаковый размер инструкций.
Я решил сделать RISC процессор во многом похожий на MIPS.
Я это сделал по целому ряду причин:
- Довольно просто создать прототип такого процессора.
- Вся сложность такого вида процессоров перекладывается на такие программы как ассемблер и/или компилятор.
Вот основные характеристики моего процессора:
- Машинное слово и размер регистров — 32 бита
- 64 регистра (включая счетчик команд)
- 2 типа инструкций
Регистровый тип) выглядит вот так: Register type(досл.
Особенность таких инструкций заключается в том, что они оперируют с тремя регистрами.
Немедленный тип): Immediate type(досл.
Инструкции этого типа оперируют с двумя регистрами и числом.
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, т.к.
Для написания процессора нужно понимать логику его работы:
- Получение инструкции по адресу Счетчика команд (PC)
- Декодирование инструкции
- Выполнение инструкции
- Прибавление к 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 байтов
Объявление должно начинаться с точки и названия типа данных, после же идут константы или аргументы.
Удобно парсить (сканировать) ассемблер файл в таком виде:
- Сначала сканируем сегмент
- Если это .data сегмент, то мы парсим разные типы данных или .text сегмент
- Если это .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 то перейди на метку
А во второй проход можно уже и генерировать файл.
В дальнейшем, можно запускать выходной файл из ассемблера на нашем процессоре и оценивать результат.
Но это уже позже. Также готовый ассемблер можно использовать в Си компиляторе.
Ссылки: