A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Cancel Create
Compiler-Development / docs / tutorial.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Cannot retrieve contributors at this time
161 lines (119 sloc) 8.66 KB
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents Copy raw contents
Copy raw contents
Кратчайшее введение в создание компилятора
Давайте сделаем компилятор арифметических выражений. Такой, который переведет исходный текст в обратной польской форме записи (ее еще называют RPN или ПОЛИЗ) в промежуточный код стековой машины. Но у нас не будет интерпретатора стекового кода, как вы, возможно, могли подумать. Далее мы сразу переведем результат в представление на языке Си. То есть у нас получится компилятор RPN в Си.
Написал язык программирования с нуля. Как работает компилятор и препроцессор — IT_школьник.
Кстати говоря, писать компилятор мы будем на Python. Но пусть это не останавливает тех, кто предпочитает какой-то иной язык программирования. Вот вам полезное упражнение: переведите приведенный код на ваш любимый язык. Или воспользуйтесь уже готовым переводом:
Начнем с синтаксического анализа
def scan(source): tokens = source.split() return [((‘Push’, int(x)) if x[0].isdigit() else (‘Op’, x)) for x in tokens]
Что мы здесь сделали? Функция scan получает от пользователя строку в обратной польской форме записи («2 2 +»).
А на выходе мы получаем промежуточное представление. Вот такое, например:
[(‘Push’, 2), (‘Push’, 2), (‘Op’, ‘+’)]
Итак, мы уже получили компилятор. Но уж очень он несерьезный. Вспомним, что изначально речь шла о коде на Си.
Займемся трансляцией в Си
def trans(ir): code = [] for (tag, val) in ir: if tag == ‘Push’: code.append(‘st[sp] = %d;’ % val) code.append(‘sp += 1;’) elif tag == ‘Op’: code.append(‘st[sp — 2] = st[sp — 2] %s st[sp — 1];’ % val) code.
append(‘sp -= 1;’) return ‘n’.join(code)
Что здесь происходит? Давайте посмотрим на вывод данной функции (на том же примере с «2 2 +»).
st[sp] = 2; sp += 1; st[sp] = 2; sp += 1; st[sp — 2] = st[sp — 2] + st[sp — 1]; sp -= 1;
Да, это уже похоже на код на Си. Массив st играет роль стека, а sp — его указатель.
Обычно с этими вещами работают виртуальные стековые машины. Вот только самой машины — интерпретатора у нас-то и нет. Есть компилятор. Что нам осталось? Надо добавить необходимое обрамление для программы на Си.
Наш первый компилятор в готовом виде
ST_SIZE = 100 C_CODE = r»»»#include int main(int argc, char** argv) int st[%d], sp = 0; %s printf(«%%dn», st[sp — 1]); return 0; >»»» def scan(source): tokens = source.split() return [((‘Push’, int(x)) if x[0].
isdigit() else (‘Op’, x)) for x in tokens] def trans(ir): code = [] for (tag, val) in ir: if tag == ‘Push’: code.append(‘st[sp] = %d;’ % val) code.append(‘sp += 1;’) elif tag == ‘Op’: code.
Что такое компилятор и интерпретатор ? Их основные отличия.
append(‘st[sp — 2] = st[sp — 2] %s st[sp — 1];’ % val) code.append(‘sp -= 1;’) return ‘n’.join(code) def rpn_to_c(source): return C_CODE % (ST_SIZE, trans(scan(source))) print rpn_to_c(‘2 2 +’)
Остается скомпилировать вывод данной программы компилятором Си.
Вы все еще готовы продолжать? Тогда давайте обсудим, что у нас получилось. Есть один сомнительный момент — наш компилятор транслирует константные выражения, а ведь их можно вычислить просто на этапе компиляции. Нет смысла переводить их в код. Но давайте пока считать, что какие-то аргументы могут попасть в стек извне. Например, из аргументов командной строки.
Остановимся на том, что практический смысл нашей разработке можно придать и позднее. Сейчас же важно получить общее представление о построении простейших компиляторов, верно?
Компилятор с использованием формы SSA
Вам нравится заголовок? SSA — это звучит очень солидно для любого компиляторщика. А мы уже сейчас будем использовать эту самую SSA. Что это такое? Давайте двигаться по порядку.
Как работает компилятор? Часть первая.
Теперь мы попробуем заинтересовать вас еще больше, показав примеры, как реальные компиляторы «старых лет» преобразуют привычные действия. Вы можете ничего не понимать в машинном коде, поэтому будем комментировать. С другой стороны, наивно думать, что какие-то там программы, пусть даже и компиляторы, способы использовать сотни видов команд, которые поддерживает процессор – это не так. Практически все из них до сих пор не только создают программы в расчете на Intel 286/386, но даже из того набора берется лишь 5-10% типов команд! Основные из них, конечно:
- Передача данных между ячейками (регистры и память) – MOV
- Сравнить данные – CMP
- Обнуление регистра – XOR. Можно было бы MOV регистр, 0, но XOR регистр, регистр занимает меньше памяти.
- Переходы по условию – JNE
- Безусловные переходы – JMP
- Загрузка в стек – PUSH
- Выгрузка из стека – POP
- Автоинкремент и автодекремент регистра – INC/DEC
- Вызов подпрограммы (ближний или дальний) – CALL
Вот, пожалуй, и все. Сам компилятор может использовать больший набор команд, чем генерирует для программы. Давайте начнем с любимого TURBO Pascal 7.1.
Var
J: integer;
Begin
For j:=1 to 4 do writeln(‘Строка’);
End.
Из программы даже неспециалист увидит, что программа 4 раза выведет на экран слово «Строка». Как это будет в коде (2208 байтов), лучше смотреть через программу HIEW. Чтобы получить такой же экран, нужно написать команду hiew , затем по F4 выбрать режим Decode, нажать F8 и F5 (указатель встанет на начало нашей программы). Хотите верьте, хотите нет, но это ВСЯ программа, которую мы написали, остальное – паскалевские настройки, стандартные библиотеки и т.д.
Если посмотреть внимательно, то можно увидеть вызовы типа call 0005:xxxx – это как раз вызов библиотек. Первое число может менять, но только не xxxx, которое является смещением вызова относительно начала библиотеки. И так во всех программах. Соответственно:
0000 – инициализация программы (туда лучше не смотреть, там все сложно);
02CD – тоже какая-то подготовка (наша программа еще не началась);
0670, 05DD, 0291 – идут часто вместе и обозначают вызов writeln;
0116 – завершение программы.
Наша единственная переменная хранится где-то в секции данных, в ячейке со смещением 52, причем видно, что программа к ней обращается как к слову ([w]ord ptr), то есть это либо тип integer, либо word – другого не дано. Если бы мы указали для j тип byte, то обращение бы шло к половинке ячейки – [b]yte ptr.
Команда mov w, [52], 1 откровенно навевает на мысль, что это начальное заполнение ячейки j. В самом деле, если бы хотели описать все действия в виде алгоритма, то придумали бы следующее:
- Записать в j единицу (mov w, [52], 1)
- Перейти к шагу 4. (jmp 72, т.е. безусловный переход на шаг 4)
- Прибавить к j единицу (счетчик работает). (inc [52])
- Вывести на экран строку. (с адреса от 72 до 8D включительно)
- Сравнить j с 4. (cmp w, [52], 4)
- Если меньше, то переход на шаг 3. (jne 6E, т.е. на шаг 3)
- Завершить программу. (с адреса 95 по 9С)
Обратите внимание, что по адресу 8E указано 16E, а мы написали 6E – это ошибка программы Hiew, которая не сумела определить переход назад. Бывает…
Как видите, программа реализована в точности по алгоритму. С ней можно поиграться, заменив в режиме редактора, к примеру, константу 1 или 4 на другие числа – программа будет выводить строку не 4 раза, а сколько скажете. Это называется «патчинг» – замена алгоритма в готовой программе без изменения исходной. Таким приемом часто пользуются хакеры для снятия защиты.
В самом деле, мы можем и не вызывать процедуру печати, забив адресное пространство с 72 до 8D однобайтовой командой nop (ничего не делающей), и тогда writeln не будет вызываться вообще. Или вставим в это место совсем другой код, подходящий по размеру (но только не вирусы, они уже надоели!).
Главное, что вы сейчас должны понять, состоит в следующем:
Компилятор всегда работает по одному принципу, использует готовые конструкции. То есть для for переменная:=значение1 to значение2 всегда будет создаваться конструкция:
1: Присвоить переменной значение1
2: Перейти к шагу 4
3: Увеличить переменную на единицу
4: Что-то сделать (не зря же работает цикл?)
5: Сравнить переменную со значением2
6: Если они не равны, то переход на шаг 3
Весь интерес, конечно, представляет блок в шаге 4, где может быть очень много команд. Но принцип ясен. Кстати, как вы знаете, в Бейсиках эта команда имеет дополнительный параметр STEP x, которая определяет шаг итерации в цикле.
Для Паскаля решили остановиться на единице, но можно представить, что значение шага хранилось бы в ячейке 54, и команда по адресу 6E выглядела бы иначе: add w, [52], bx, где в bx переписывалось бы значение из ячейки 54. А что делать, если в этом процессоре НЕЛЬЗЯ передавать данные из ячейки в ячейку напрямую – только через регистр? Зато компилятор бы усложнился, ведь пришлось бы ставить внутренний контроль за итерациями! Подумайте:
FOR J:=1 to 10 STEP 3 – сколько раз выполнится цикл? И кто будет следить за этим? А компилятор должен следить: и за тем, чтобы стек не кончился в результате рекурсий, и чтобы значение не вышло за рамки массива, и чтобы не было деления на ноль… Много чего.
Кстати, указанный выше шаблон почти так же работает для циклов repeat … until, while … do. Интересно, как располагают переменные в массиве? А что будет, если по адресу 96 не обнулить регистр ax? Об этом и другом читайте в следующей части.
Источник: dprogu.ru
Как работает компилятор языка C?
Мне нужно описание работы компилятора на естествeннoм языке составленное для того кто этого языка не знает. То есть учебник в духе «представь что ты компилятор, от этого двигаемся дальше.» Со вчерашнего дня учу этот язык. Синтаксис кажется хаосом, я не понимаю список действий которые проводит компилятор в процессе разбора, не понимаю к какой подсистеме языка относится выражение и что в результате передаётся элементам языка и функциям. Все эти символы которые непонятно как работают. Это очень сильно отличается от того к чему я привык в tcl в котором есть подстановщик,команды и их параметры.
Отслеживать
70.5k 12 12 золотых знаков 87 87 серебряных знаков 179 179 бронзовых знаков
задан 3 дек 2015 в 12:29
41 6 6 бронзовых знаков
Пример строки с «не понимаю к какой подсистеме языка относится выражение.» покажите.
3 дек 2015 в 12:35
printf(«Argument %d: %sn», i, argv[i]); Вот например. Не понимаю на каком уровне обрабатывается это вот всё. Это всё параметры для printf которые передаются без изменений и он внутри себя их обрабатывает?
3 дек 2015 в 12:36
Элементарно: вызов функции (someName();). Параметры три штуки: «Argument %d: %sn», i, argv[i] Открываете документацию на функцию, смотрите что значат параметры. Как-то так.
3 дек 2015 в 12:37
3 дек 2015 в 12:50
И ‘C’ хорош тем, что этих ‘элементов языка’ не особо много. собственно это if, while, for, do, switch, вроде ничего с круглыми скобками не забыл перечислить. Все остальное с круглыми скобками — функции
3 дек 2015 в 12:56
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Вы должны думать следующим образом.
- Общая картина. Компилируются C-файлы по отдельности, компилятор не знает ничего о других файлах, если это не указано в файле явно. Другие файлы «втягиваются» препроцессором. (Для пуристов, да, я могу скормить и .h компилятору через Makefile, не будем усложнять картину без надобности.)
- Препроцессор. Он проходится по коду и производит тупые текстовые макроподстановки. #define X(Y, Z) for (int i = 0; i < Y; i = i * Z) заставляет X(10, 2 + 1) превращаться в for (int i = 0; i < 10; i = i * 2 + 1) . Препроцессор, однако, знает о строках, и не проводит макроподстановки внутри них. Также он применяет #include путём механического включения в это место файла.
- Препроцессор строк. Внутри строковых и символьных литералов некоторые последовательности символов заменяются на другие. Например, n заменяется на символ с кодом 10. Также, для литералов широких строк ( wchar_t* ) может применяться перекодировка из character set’а исходного файла в UCS-2 или UCS-4, в зависимости от компилятора.
- Собственно компилятор. Никакой магии у компилятора нет. Есть ключевые слова ( for , if , etc.) и функции. Например, printf — это функция (из стандартной библиотеки), запись printf(«%dn», 15); производит в скомпилированном коде вызов функции printf и передачу ей параметров «%dn» и 15 . Точно так же вызов printf(«%dn», «»); производит в вызов функции printf с параметрами «%dn» и «» (этот вызов завершится с ошибкой времени выполнения). Компилятор знает точную семантику форматной строки printf и имеет право выдать подсказку, если он видит, что типы параметров не подходят к форматной строке.
- Оптимизатор. Он имеет право внутри заменить любую конструкцию на более эффективную, пользуясь правилом as if: если с точки зрения конечного вывода и видимых пользователю значений это не меняет результат, преобразование допустимо. Пример: если у вас есть длинное вычисление без побочных эффектов, результатом которого вы не пользуетесь (то есть, не выводите его), оптимизатор имеет право выкинуть его. И также имеет право и не выкидывать. Например, порядок вычисления слагаемых в выражении A() + B() не определён, и даже если функции A и B имеют побочные эффекты, оптимизатор имеет право вычислять их в любом порядке, может быть даже вперемешку. Если вы хотите гарантировать, что A() вычислится строго перед B() , пользуйтесь явной дополнительной переменной.
- Undefined behaviour. Here be dragons. Существует достаточно большой набор рантайм-ситуаций (например: разыменовние нулевого указателя, выход за границу массива (!) или знаковое переполнение), когда компилятор перестаёт нести ответственность за результат. Компилятор имеет право предполагать, что такого никогда не случится, делать из этого нетривиальные умозаключения, и применять их для упрощения кода. Например: для кода
int m[1]; if (cond) < printf(«Хе-хеn»); return; >for (int i = 0; i
компилятор имеет право предположить, что обращение m[1] никогда не происходит, поэтому цикл не выполняется, поэтому код должен выйти на раннем return , поэтому cond обязательно равно true , значит, его можно не вычислять, и упростить всю функцию до
printf(«Хе-хеn»);
Отслеживать
ответ дан 3 дек 2015 в 12:52
206k 27 27 золотых знаков 290 290 серебряных знаков 521 521 бронзовый знак
кстати, пример с printf вообще довольно любопытен. Я, например, каким образом printf НЕ противоречит синтаксису С. Там же нет функций с произвольным числом аргументов. Если кто даст ссылку с описанием ее устройства «изнутри» что-ли (но не на код, не уверен, что есть код printf на С), буду весьма признателен
3 дек 2015 в 13:13
Источник: ru.stackoverflow.com