Розробка

Модель розробки на прикладі Stack-based CPU

Чи виникало у вас коли-небудь питання “як працює процесор?”. Так-так, саме той, який знаходиться у вашому ПК/ноутбуці/смартфоні. У цій статті я хочу навести приклад самостійно придуманого процесора з дизайном на мові Verilog. Verilog — це не зовсім той, мова програмування, на який він схожий. Це — Hardware Description Language. Написаний код не виконується чим-небудь (якщо ви не запустите його в симуляторі, звичайно), а перетворюється в дизайн фізичної схеми, або у вигляд, що сприймається FPGA (Field Programmable Gate Array).

Дисклеймер: ця стаття — результат роботи над проектом в університеті, тому час на роботу було обмежено і багато частині проекту перебувають ще тільки на початковій стадії розробки.

Прошу звернути увагу, що створюється в даній статті процесор має мало чого спільного з сучасними широко поширеними процесорами, але його створенням я намагався досягти трохи іншої мети.

Щоб по-справжньому розуміти процес програмування, треба уявляти, як працює кожен з використовуваних інструментів: компілятор/інтерпретатор мови, віртуальна машина, якщо вона є, проміжний код, і, звичайно ж, сам процесор. Дуже часто люди, які вивчають програмування, довгий час знаходяться на першій стадії — вони думають тільки про те, як працює мова і його компілятор. Це часто веде до помилок, шляхи вирішення яких невідомі починаючому програмісту, тому що він не має поняття, звідки ростуть корені цих проблем. Я сам бачив кілька живих прикладів, де ситуація була приблизно в описі вище, тому я вирішив спробувати виправити дану ситуацію і створити набір речей, які допоможуть зрозуміти починаючим програмістам всі етапи.

 

Цей набір складається з:

  • Власне придуманого мови
  • Плагіна підсвічування для 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. З компілятором все зрозуміло, Interpreter — просто симулятор OurCPU на Java, але ми не будемо розглядати в цій статті.

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 {
print(arg1);
print(arg2);

 if (arg1 == 0) {
 return arg2;
 } 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, воно полегшить роботу з мовою.

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

Related Articles

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *

Close