Перейти к содержимому

Как написать компилятор ассемблера

  • автор:

Как компилятор может быть написан на том же языке который он компилирует?

Прочитал несколько подобных вопросов но так и не нашел полноценного ответа. Как компилятор Си может быть написан на Си, ведь кто-то должен скомпилировать сам компилятор в машинный код. И будет ли трансляция в ассемблер по сути равна компиляции? (за исключением того что команды ассемблера нужно будет потом заменить на двоичный код)

Отслеживать
задан 18 апр 2021 в 7:57
425 2 2 серебряных знака 12 12 бронзовых знаков

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

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

Но вот с плюсовым компилятором все проще — вначале был написан cfront — программа на си, которая переводила «плюсовый код» в сишный, а потом уже компилировала.

С другими языками также такое происходило. К примеру, go был вначале написан на си, а потом код так модифицировали, что бы он был максимально похож на go код (https://go-review.googlesource.com/c/go/+/5652).

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

Но как появился самый первый компилятор тогда? Есть хорошая статья https://jameshfisher.com/2018/01/11/bootstrapping-a-c-compiler/ которая описывает, как поэтапно можно это сделать.

То есть, вначале все пишется ручками в памяти, в ноликах и единицах, потом постепенно наращиваются инструменты и в конце концов, такими итерациями можно дойти до современного мира. Вот люди пишут простейший си компилятор https://github.com/rdtscp/c-bootstrap

И будет ли трансляция в ассемблер по сути равна компиляции

если у нас есть компилятор ассемблера, то да. Почему «компилятор»? потому что ассемблеры бывают разные. Тот же FLAT assembler обладает таким разувесистым синтаксисом, что я бы побоялся его называть его компилятор «программой для замены на двоичный код».

Да, процесс, когда компилятор компилирует сам себя называется bootstrapping. и это не имеет отношения к html.

Архитектура компьютера

  1. Постановка задачи. Написать программу, которая выводит на экран строчку «Привет!».
  2. Разработка алгоритма программы. Алгоритм линейный, разработки не требует.
  3. Формализация (запись) алгоритма
    В текстовом редакторе создаем файл privet.asm и записываем в него следующий код (без номеров строк) :

1 data segment ;описание сегмента данных
2 mes db ‘Привет!$’ ;строка для вывода на экран. ‘$’ — признак конца строки
3 data ends ;конец сегмента данных
4
5 code segment ;начало сегмента кода
6 start: ;метка start — начало нашей программы
7 assume cs:code, ds: data ;директива компилятора
8 mov ax, data ;настройка сегмента данных
9 mov ds, ax
10
11 mov ah, 9 ;функция №9 — вывод строки на экран
12 lea dx, mes ;берём адрес строки
13 int 21h ;вызов прерывания для вывода строки
14
15 mov ax, 4c00h ;функция завершения программы
16 int 21h ;завершаем программу
17 code ends ;конец сегмента кода
18 end start ;конец программы с точкой входа start

Описание программы privet.asm

Строки 1 — 3 программы privet.asm содержат описание сегмента данных. Сегмент данных — область память, в которой будут храниться данные для наших программ.
Строки 5 — 17 — это код программы, её исполняемая часть.
В 8 и 9 строках выполняется настройка сегмента данных программы.
Строки 11 — 13 — вывод строки на экран при помощи функции №9 прерывания 21h (подробнее о функциях и работе с ними на следующей лабораторной работе).
15 и 16 строки — стандартное завершение программы.
После символа ‘;’ пишутся комментарии, они не обрабатываются компилятором.

Переход на новую строку

Для организации перехода на новую строку достаточно вывести на экран символы перевода строки и возврата каретки (CR/LF). Эти символы имеют коды 10 и 13. Если в нашей программе необходимо после вывода строки перейти на новую, то для этого достаточно переписать вторую строку программы:

mes2 db ‘Выводим строку и переходим на новую. ‘, 10 , 13 , ‘$’

Переход на новую строку можно выполнить и до вывода сообщения на экран:

mes3 db 10 , 13 , ‘Выводим с новой строки. $’

Задание для выполнения

Написать программу, которая выводит одно под другим следующие сообщения:
Привет!
Меня зовут компьютер!
До свидания!

Как написать компилятор ассемблера

Мне нужно написать свой компилятор с языка ассмблера.
Я никогда раньше такими вещами не занимался.
Насколько я понимаю, начать надо с перевода текста программы в некоторую внутренную форму. Из нее потои уже будет генерится исполняемый код.С генерацией я как=нибудь разберусь. Мне бы счас про первую фазу что-нибудь узнать Ассемблер должен поддерживать примерно те же средства, что и FASM/TASM/MASM. Т.е. структуры, макросы и пр.
Подскажите, что почитать.
Заранее спасибо.

Re: Компилятор ассемблера

От: Пётр Седов
Дата: 20.06.07 22:34
Оценка: +1

Здравствуйте, Аноним, Вы писали:
А>Насколько я понимаю, начать надо с перевода текста программы в некоторую внутренную форму.
Да, эта задача называется «разбор» («parsing»). Классическая книга на эту тему – Альфред Ахо, Рави Сети, Джеффри Ульман «Компиляторы. Принципы, технологии, инструменты» (также известна как «книга дракона», там на обложке красный дракон). Там описаны лексический анализ (последовательность char-ов -> последовательность token-ов), синтаксический анализ (последовательность token-ов -> древовидная структура данных) и много чего ещё.
Также поищите по слову «NASM» (netwide assembler). У него, вроде, исходники открыты.

Пётр Седов (ушёл с RSDN)
Re[2]: Компилятор ассемблера

От: Кодт
Дата: 21.06.07 09:23
Оценка: 1 (1)

Здравствуйте, Пётр Седов, Вы писали:

А>>Насколько я понимаю, начать надо с перевода текста программы в некоторую внутренную форму.
ПС>Да, эта задача называется «разбор» («parsing»).

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

Перекуём баги на фичи!
Re[3]: Компилятор ассемблера

От: Пётр Седов
Дата: 21.06.07 09:49
Оценка:

Здравствуйте, Кодт, Вы писали:
К>На первый план вылезает не синтаксис, а семантика.
На то есть форум «Низкоуровневое программирование».

2 Аноним 842:
В этом форуме обсуждалась задача выбора между ближним переходом и дальним переходом (здесь

Автор: TiLex
Дата: 08.01.07
). Возможно, найдёте что-нибудь полезное.
Пётр Седов (ушёл с RSDN)
Re: Компилятор ассемблера

От: LaptevVV
Дата: 21.06.07 15:58
Оценка:

Здравствуйте, Аноним, Вы писали:

А>Привет всем!

А>Мне нужно написать свой компилятор с языка ассмблера.
А>Я никогда раньше такими вещами не занимался.
А>Насколько я понимаю, начать надо с перевода текста программы в некоторую внутренную форму. Из нее потои уже будет генерится исполняемый код.С генерацией я как=нибудь разберусь. Мне бы счас про первую фазу что-нибудь узнать Ассемблер должен поддерживать примерно те же средства, что и FASM/TASM/MASM. Т.е. структуры, макросы и пр.
А>Подскажите, что почитать.
Если мыло сообщишь — пришлю главу книжки про ассемблеры.
Про трансляцию ассемблера только в старых книжках попсистемному программированию найти можно.
Еще Была книжка Ассемблеры и загрузчики» — в сети болтается английский pdf-файл

Хочешь быть счастливым — будь им!
Без булдырабыз.
Re: Компилятор ассемблера

От: chukichuki
Дата: 22.06.07 08:31
Оценка: 1 (1)

Здравствуйте, Аноним, Вы писали:

А>Привет всем!

А>Мне нужно написать свой компилятор с языка ассмблера.
А>Я никогда раньше такими вещами не занимался.
А>Насколько я понимаю, начать надо с перевода текста программы в некоторую внутренную форму. Из нее потои уже будет генерится исполняемый код.С генерацией я как=нибудь разберусь. Мне бы счас про первую фазу что-нибудь узнать Ассемблер должен поддерживать примерно те же средства, что и FASM/TASM/MASM. Т.е. структуры, макросы и пр.
А>Подскажите, что почитать.
А>Заранее спасибо.

Примитивный ассемблер пишется достаточно просто. Хороший пример есть в книге Свердлова «Языки программирования и методы трансляции». Там рассмотрен пример ассемблера для вирутальной стековой машины. Если надо у меня есть игрушечный ассемблер для стековой машины, написанный на OCaml. Могу дать безд возд мездно С макроассемблером чуть сложнее, но тоже не беда.

Re[3]: Компилятор ассемблера

От: chukichuki
Дата: 22.06.07 13:24
Оценка:

К>Что-то мне подсказывает, что парсинг — не самая существенная задача для ассемблера.
К>Некоторые джигиты делали ассемблер на основе форта, так там парсер вообще состоит только из лексера
К>На первый план вылезает не синтаксис, а семантика.

Поскольку ассемблерная программа это текстовая запись машинного кода, то и с семантикой заморочек не много. Вся семантика ложится на плечи программиста , который пишет на ассемблере, компилятор же реализует тупое преобразование «текстовое обозначение команды -> обозначение команды в машинных кодах».

Re[4]: Компилятор ассемблера

От: Кодт
Дата: 22.06.07 14:37
Оценка:

Здравствуйте, chukichuki, Вы писали:

C>Поскольку ассемблерная программа это текстовая запись машинного кода, то и с семантикой заморочек не много. Вся семантика ложится на плечи программиста , который пишет на ассемблере, компилятор же реализует тупое преобразование «текстовое обозначение команды -> обозначение команды в машинных кодах».

В макроассемблере это не совсем тупое преобразование.
Хотя, конечно, смотря какой степени навороченность макропроцессора.

Перекуём баги на фичи!
Re[5]: Компилятор ассемблера

От: chukichuki
Дата: 25.06.07 07:54
Оценка:

К>В макроассемблере это не совсем тупое преобразование.
К>Хотя, конечно, смотря какой степени навороченность макропроцессора.

Ну да. Всегда можно сказать, что Си++ это просто навороченный и местами ограниченный макроассемблер.

Пишем свой язык программирования, часть 2: промежуточное представление программ

image

Приветствую всех, кто заглянул почитать мою очередную статью.

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

В первой части (линк: habr.com/post/435202) я описал этапы проектирования и написания языковой ВМ, которая будет выполнять наши будущие приложения на нашем будущем языке.
В этой статье я планирую описать основные этапы создания промежуточного языка программирования, который будет собираться в абстрактный байткод для уже непосредственного выполнения на нашей ВМ.

Думаю, что не помешает сразу привести ссылки на сайт проекта и его репозиторий.

Сайт
Репозиторий

Сразу скажу, что весь код написан на FPC и примеры буду приводить на нем же.

Итак, начнем наше просветление.

Зачем нам сдался промежуточный ЯП?

Стоит понимать, что преобразовать программу с языка высокого уровня сразу с исполняемый байткод, который состоит из ограниченного набора инструкций — задача настолько тривиальная, что её лучше на порядок упростить, добавив в проект промежуточный язык. Гораздо лучше упрощать код постепенно, чем сразу представлять математические выражения, структуры и классы набором опкодов. К слову, именно таким образом работает большинство сторонних трансляторов и компиляторов.

В моей предыдущей статье я писал о том, как можно реализовать языковую ВМ. Сейчас нужно реализовать ассемблер-подобный ЯП для неё и функционал для дальнейшего написания транслятора. На этих этапах мы закладываем фундамент будущему проекту. Стоит понимать, чем лучше фундамент — тем круче здание.

Делаем первый шаг к реализации сего чуда

Для начала стоит поставить цель. Что мы собственно будем писать? Какими характеристиками должен обладать конечный код и что он должен делать?

Я могу сформировать список основных функциональных частей из которых должна состоять эта часть проекта:

  • Простой ассемблер. Преобразует простые инструкции в набор опкодов для ВМ.
  • Базовая реализация функционала для реализации переменных.
  • Базовая реализация функционала для работы с константами.
  • Функционал для поддержки точек входа в методы и вычисление их адресов на этапе трансляции.
  • Возможно ещё пара функциональных плюшек.

Итак, цели поставлены, приступим к реализации.

Пишем простой ассемблер

Спрашиваем себя, что такое ассемблер?

По сути — это программа, выполняющая подстановку опкодов вместо их текстовых описаний.

Рассмотрим этот код:

push 0 push 1 add peek 2 pop

После обработки кода ассемблером мы получим исполняемый код для ВМ.

Видим, что инструкции могут быть односложные и двусложные. Более сложных инструкций для стековой ВМ не требуется.

Нам нужен код, который мог бы выделить из строки токены (учитываем, что среди них могут быть строки).

function Tk(s: string; w: word): string; begin Result := ''; while (length(s) > 0) and (w > 0) do begin if s[1] = '"' then begin Delete(s, 1, 1); Result := copy(s, 1, pos('"', s) - 1); Delete(s, 1, pos('"', s)); s := trim(s); end else if Pos(' ', s) > 0 then begin Result := copy(s, 1, pos(' ', s) - 1); Delete(s, 1, pos(' ', s)); s := trim(s); end else begin Result := s; s := ''; end; Dec(w); end; end;

Ок, теперь нужно реализовать что-то вроде switch-case конструкции для каждого оператора и наш простой ассемблер готов.

Переменные

Вспомним, что у нашей ВМ для поддержки переменных имеется массив указателей и соответственно — статичная адресация. Значит, что функционал для работы с переменными можно представить как TStringList, в котором строки — имена переменных, а их индексы — их статические адреса. Стоит понимать, что недопустимо дублирование имен переменных в этом списке. Думаю, что нужный код вы сможете представить себе и/или даже написать сами.

Если хотите взглянуть на готовую реализацию, то милости прошу: /lang/u_variables.pas

Константы

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

Точки входа в методы

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

Рассмотрим пример кода:

Summ: peek 0 pop peek 1 pop push 0 new peek 2 mov push 2 push 0 add jr

Выше описан пример трансляции метода Summ:

func Summ(a, b): return a + b end

Стоит понимать, что опкоды для точек входа отсутствуют. Что вообще собой представляет точка входа в метод Summ? Это простое число — смещение следующего за точкой входа опкода. (смещение опкода — это номер опкода относительно начала исполняемого абстрактного байткода). Теперь перед нами стоит задача — нужно вычислить это смещение на этапе компиляции и, как вариант — объявить константу Summ этим числом.

Напишем для этого некий счетчик веса каждых операторов. У нас есть простые односложные операторы, например «pop». Они занимают 1 байт. Есть и более сложные, например «push 123» — они занимают 5 байт, 1 на опкод и 4 на unsigned int тип.

Суть кода для добавления поддержки точек входа ассемблером:

  1. Имеем счетчик, допустим i = 0.
  2. Пробегаемся по коду, если перед нами конструкция вида «push 123», то прибавляем к нему 5, если простой опкод — 1. Если перед нами точка входа, то удаляем её из кода и объявляем соответствующую константу со значением счетчика и именем точки входа.

Прочий функционал

Это, например, простое преобразование кода перед обработкой.

Итоги

Мы реализовали наш небольшой ассемблер. Он понадобится нам для реализации на его основе более сложного транслятора. Теперь можем писать небольшие программы для нашей ВМ. Соответственно, в дальнейших статьях будет описан процесс написания более сложного транслятора.

Спасибо, что дочитали до конца, если вы это сделали.

Если вам что-то не понятно, то я жду ваших комментариев.

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

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