Один год работы над компилятором

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

Согласитесь, с точки зрения семантики — язык Си один из самых сложных языков. Программируя много лет на Си, мы не  задумывается, насколько сложными могут быть его конструкции.

Для начала освежим в памяти этапы, пройдя которые исходный текст на языке Си становится исполняемым кодом процессора:

  1. Компиляция программы на языке Си начинается с работы препроцессора. На этом этапе анализируется исходный код  программы, подключаются заголовочные файлы с описанием констант, структур данных и прототипов функций. На этом этапе можно свернуть некоторые (но далеко не все) выражения.
  2. Лексический анализ — код, обработанный препроцессором, разбивается анализатором на лексемы — примитивы языка Си, которые будут использоваться для дальнейшего анализа. На этом этапе возможно обнаружение некоторых ошибок в исходном коде. Например, недопустимый символ или некоторая недопустимая комбинация символов может быть определена на этапе лексического анализа. Лексемы, полученные на этом этапе, передаются для анализа следующему уровню – синтаксическому анализатору.
  3. Синтаксический анализ — производит разбор лексем, распознавая в их последовательности конструкции языка. Структуры данных, функции и их описания, константы, выражения, операторы языка и блоки кода — всё это разбирается на этапе синтаксического анализа и преобразуется во внутренние структуры данных компилятора. Все возможные ошибки, не обнаруженные лексическим анализатором, выявляются на этапе синтаксического анализа. В процессе синтаксического анализа можно провести часть оптимизации. Например, упрощение арифметических выражений, которые не сумел упростить препроцессор. Хорошим примером являются выражения, использующие оператор sizeof — в общем случае препроцессор не знает о размерах структур данных, поэтому не может упростить такие выражения. При разборе структур данных во время  синтаксического анализа компилятор уже обладает информацией о структурах данных.
  4. Машинно-независимая оптимизация. На этом этапе компилятор пытается по возможности упростить код — избавиться от неиспользуемых переменных и неиспользуемых статически определённых функций, вынос инвариантов за пределы циклов и т.д.
  5. Генерация кода и машинно-зависимая оптимизация. На этом этапе компилятор старается сформировать код, способный наиболее эффективно исполняться на конкретном процессоре. Результатом работы этого этапа, как правило, является ассемблерный код, который на следующем этапе преобразуется в объектный файл. Многие современные компиляторы «скрывают» от программиста промежуточный ассемблерный код, генерируя объектный файл. При этом программист имеет возможность  указать компилятору на необходимость генерации ассемблерного кода.
  6. Генерация объектного кода — из сформированного на предыдущем этапе ассемблерного файла, компилятор с помощью встроенного или внешнего ассемблера формирует объектный файл — т.е. «сырой» файл с инструкциями процессора и множеством таблиц для последующего связывания этого файла с другими объектными и библиотечными файлами.
  7. Генерация исполняемого файла — один или несколько объектных файлов линкуются с библиотечными файлами. При этом происходит настройка всех условных и безусловных переходов и вызов внутри объектного файла, а так же связывание взаимных вызовов функций между объектными файлами и внешними библиотеками. В результате получается исполняемый файл, готовый для загрузки и исполнения операционной системой.

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

 

На момент написания настоящего сообщения наша команда реализовала три этапа — препроцессинга кода, лексический анализ и синтаксический разбор. Результатом являются два исполняемых файла – препроцессор, с совмещённым лексическим анализатором, и синтаксический анализатор.

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

В текущем состоянии нам удалось скомпилировать тестовую программу, восстановленную из дерева синтаксического разбора, с помощью заголовочных файлов и библиотек трех компиляторов — 32-битный gcc из Debian Linux, 32-битный Си компилятор из Microsoft Visual Studio 2005 и 32-битный Си компилятор из Embarcadero RAD Studio 2010.

Следующим этапом мы планируем генерировать ассемблерные файлы для их последующей компиляции и линковки с помощью средств из Microsoft Visual Studio 2005. Использование сторонних средств разработки – вынужденная мера на данном этапе, поскольку реализовать компилятор в полном объёме не по силам небольшой творческой команде.

Мы трезво оцениваем свои возможности и не собираемся конкурировать с мэтрами индустрии – нам всего лишь необходим «карманный» компилятор для микропроцессора с программной поддержкой микроядра.

 


Оставить комментарий