Хабрахабр

[Из песочницы] Модель разработки на примере Stack-based CPU

Да-да, именно тот, который находится в вашем в ПК/ноутбуке/смартфоне. Возникал ли у вас когда-нибудь вопрос "как работает процессор?". Verilog — это не совсем тот язык программирования, на который он похож. В этой статье я хочу привести пример самостоятельно придуманного процессора с дизайном на языке Verilog. Написанный код не выполняется чем-либо (если вы не запускаете его в симуляторе, конечно), а превращается в дизайн физической схемы, либо в вид, воспринимаемый FPGA (Field Programmable Gate Array). Это — Hardware Description Language.

Дисклеймер: эта статья — результат работы над проектом в университете, поэтому время на работу было ограничено и многие части проекта находятся еще только в начальной стадии разработки.

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

Очень часто люди, изучающие программирование, долгое время находятся на первой стадии — они думают только о том, как работает язык и его компилятор. Чтобы по-настоящему понимать процесс программирования, надо представлять, как работает каждый из используемых инструментов: компилятор/интерпретатор языка, виртуальная машина, если она есть, промежуточный код, и, конечно же, сам процессор. Я сам видел несколько живых примеров, где ситуация была примерно как в описании выше, поэтому я решил попробовать исправить данную ситуацию и создать набор вещей, которые помогут понять начинающим программистам все этапы. Это часто ведет к ошибкам, пути решения которых неизвестны начинающему программисту, потому что он не имеет понятия, откуда растут корни этих проблем.

Этот набор состоит из:

  • Собственно придуманного языка
  • Плагина подсветки для VS Code
  • Компилятора к нему
  • Набора инструкций
  • Простого процессора, способного выполнять этот набор инструкций (написан на Verilog)

Еще раз напоминаю, что данная статья НЕ ОПИСЫВАЕТ НИЧЕГО ПОХОЖЕГО НА СОВРЕМЕННЫЙ РЕАЛЬНЫЙ ПРОЦЕССОР, она описывает модель, которую легко понять без углубления в детали.

Вещи, которые вам понадобятся, если вы хотите запустить все своими руками:

Чтобы запустить симуляцию CPU, необходим ModelSim, который вы можете скачать с сайта Intel.

Для запуска компилятора OurLang необходима Java версии >= 8.

Ссылки на проекты:
https://github.com/IamMaxim/OurCPU
https://github.com/IamMaxim/OurLang

Расширение:
https://github.com/IamMaxim/ourlang-vscode

Для сборки Verilog-части я обычно использую скрипт на bash:

#/bin/bash vlib work
vlog *.v vsim -c testbench_1 -do "run; exit"

Но это же можно повторить через GUI.

Главное — следите за тем, какие модули имеет в зависимостях нужный вам модуль. Для работы с компилятором удобно использовать Intellij IDEA. Я не стал выкладывать в открытый доступ готовый .jar, потому что я рассчитываю на то, что читатель будет читать исходный код компилятора.

С компилятором все понятно, Interpreter — просто симулятор OurCPU на Java, но мы не будем рассматривать его в этой статье. Запускаемые модули — Compiler и Interpreter.

Instruction set

Думаю, начать лучше с Instruction Set’а.

Существует несколько архитектур наборов инструкций:

  • Stack-based — то, что описывается в статье. Отличительная особенность — все операнды помещаются в стак и достаются из стака, что сразу исключает возможность распараллеливать выполнение, но при этом является одним из самых простых подходов к работе с данными.
  • Accumulator-based — суть в том, что имеется лишь один регистр, который хранит значение, которое модифицируется инструкциями.
  • Register-based — то, что используется в современных процессорах, потому что позволяет достичь максимальной производительности за счет применения различных оптимизаций, в том числе распараллеливания выполнения, pipelining’а и т.д.

Набор инструкций нашего процессора содержит 30 инструкций

Далее предлагаю взглянуть на реализацию процессора:

Код состоит из нескольких модулей:

  • CPU
  • RAM
  • Модули для каждой инструкции

RAM — модуль, содержащий непосредственно саму память, а также способ получить доступ к данным в ней.

CPU — модуль, который непосредственно управляет ходом выполнения программы: считывает инструкции, передает контроль нужной инструкции, хранит необходимые регистры (указатель на текущую инструкцию и т.д.).

Некоторые (например, putw, putb, jmp и jif) имеют дополнительный аргумент в самой инструкции. Практически все инструкции работают только со стаком, так что достаточно лишь выполнить их. Им необходимо передать всю инструкцию, чтобы они могли считать необходимые данные.

Вот схема, в общих чертах описывающая ход работы процессора:

Общие принципы устройства программ на уровне инструкций

Как видно из схемы выше, после выполнения каждой инструкции адрес переходит к следующей. Думаю, пришло время познакомиться с устройством непосредственно самих программ. Когда же появляется необходимость нарушить эту линейность (условие, цикл, и т.д.), используются branch-инструкции (в нашем наборе инструкций это jmp и jif). Это дает линейный ход программы.

Они никак не привязаны к самому процессору или инструкциям, это просто концепт, который используется компилятором при генерации кода. При вызове функций нам необходимо сохранить текущее состояние всего, и для этого имеются activation record’ы — записи, хранящие эту информацию. Activation record в OurLang имеет следующую структуру:

Как видно из этой схемы, локальные переменные также хранятся в activation record’е, что позволяет рассчитывать адрес переменной в памяти во время компиляции, а не во время выполнения, и, таким образом, ускоряется выполнение программы.

Для вызовов функции в нашем наборе инструкций предусмотрены способы работы с двумя регистрами, содержащимися в модуле CPU (operation pointer и activation address pointer) – putopa/popopa, putara/popara.

Компилятор

В целом, компилятор как программа состоит из 3 частей: А теперь взглянем на самую близкую к конечному программисту часть — компилятор.

  • Лексер
  • Парсер
  • Компилятор

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

Парсер строит из этих лексических единиц абстрактное синтаксическое дерево.

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

В компиляторе OurLang эти части представлены соответственно классами

  • Lexer.java
  • Parser.java
  • Compiler.java

Язык

Но для понимания сути работы компилятора текущего состояния уже достаточно. OurLang находится в зачаточном состоянии, то есть он работает, но в нем пока не так много вещей и не доведена до конца даже Core-часть языка.

Как пример программы для понимания синтаксиса предлагается этот фрагмент кода (он же используется для тестирования функционала):

// single-line comments /*
* Multi-line comments
*/ function print(int arg) { instr(putara, 0); instr(putw, 4); instr(add, 0); instr(lw, 0); instr(printword, 0);
} function func1(int arg1, int arg2): int else { return func1(arg1 - 1, arg2); };
} function main() { var i: int; i = func1(1, 10); if (i == 0) { i = 1; } else { i = 2; }; print(i);
}

Через код компилятора, естественно ;). Акцентировать внимание на языке я не буду, оставлю это на ваше изучение.

При его написании я пытался сделать self-explaining код, который понятен без комментариев, так что с пониманием кода компилятора проблем возникнуть не должно.

Благо, компилятор OurLang генерирует assembly-like код с комментариями,
что поможет не запутаться в том, что происходит внутри. Ну и естественно, самое интересное — писать код, а затем наблюдать за тем, во что он превращается.

Также рекомендую установить расширение для Visual Studio Code, оно облегчит работу с языком.

Удачи в изучении проекта!

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»