Что делает программа компилятор

Что делает программа компилятор

Компиля́тор — программа или техническое средство, выполняющее компиляцию.

Компиляция — трансляция программы, составленной на исходном языке высокого уровня, в эквивалентную программу на низкоуровневом языке, близком машинному коду (абсолютный код, объектный модуль, иногда на язык ассемблера). Входной информацией для компилятора (исходный код) является описание алгоритма или программа напроблемно-ориентированном языке, а на выходе компилятора — эквивалентное описание алгоритма на машинно-ориентированном языке (объектный код).

Компилировать — проводить трансляцию машинной программы с проблемно-ориентированного языка на машинно-ориентированный язык.

Векторизующий. Транслирует исходный код в машинный код компьютеров, оснащённых векторным процессором.

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

Диалоговый. См.: диалоговый транслятор.

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

Интерпретирующий (пошаговый). Последовательно выполняет независимую компиляцию каждого отдельного оператора (команды) исходной программы.

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

Отладочный. Устраняет отдельные виды синтаксических ошибок.

Резидентный. Постоянно находится в оперативной памяти и доступен для повторного использования многими задачами.

Самокомпилируемый. Написан на том же языке, с которого осуществляется трансляция.

Универсальный. Основан на формальном описании синтаксиса и семантики входного языка. Составными частями такого компилятора являются: ядро, синтаксический исемантический загрузчики.

Пакетная. Компиляция нескольких исходных модулей в одном пункте задания.

Построчная. То же, что и интерпретация.

Условная. Компиляция, при которой транслируемый текст зависит от условий, заданных в исходной программе директивами компилятора. Так, в зависимости от значения некоторой константы, можно включать или выключать трансляцию части текста программы.

Процесс компиляции состоит из следующих этапов:

Лексический анализ. На этом этапе последовательность символов исходного файла преобразуется в последовательность лексем.

Синтаксический (грамматический) анализ. Последовательность лексем преобразуется в дерево разбора.

Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д. Результат обычно называется «промежуточным представлением/кодом», и может быть дополненным деревом разбора, новым деревом, абстрактным набором команд или чем-то ещё, удобным для дальнейшей обработки.

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

Генерация кода. Из промежуточного представления порождается код на целевом языке.

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

Генерация машинного кода

Большинство компиляторов переводит программу с некоторого высокоуровневого языка программирования в машинный код, который может быть непосредственно выполнен процессором. Как правило, этот код также ориентирован на исполнение в среде конкретной операционной системы, поскольку использует предоставляемые ею возможности (системные вызовы, библиотеки функций). Архитектура (набор программно-аппаратных средств), для которой производится компиляция, называется целевой машиной.

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

Для каждой целевой машины (IBM, Apple, Sun и т. д.) и каждой операционной системы или семейства операционных систем, работающих на целевой машине, требуется написание своего компилятора. Существуют также так называемые кросс-компиляторы, позволяющие на одной машине и в среде одной ОС генерировать код, предназначенный для выполнения на другой целевой машине и/или в среде другой ОС. Кроме того, компиляторы могут оптимизировать код под разные модели из одного семейства процессоров (путём поддержки специфичных для этих моделей особенностей или расширений наборов инструкций). Например, код, скомпилированный под процессоры семейства Pentium, может учитывать особенности распараллеливания инструкций и использовать их специфичные расширения — MMX, SSE и т. п.

Некоторые компиляторы переводят программу с языка высокого уровня не прямо в машинный код, а на язык ассемблера. Это делается для упрощения части компилятора, отвечающей за кодогенерацию, и повышения его переносимости (задача окончательной генерации кода и привязки его к требуемой целевой платформе перекладывается на ассемблер), либо для возможности контроля и исправления результата компиляции программистом.

Результатом работы компилятора может быть программа на специально созданном низкоуровневом языке, подлежащем интерпретации виртуальной машиной. Такой язык называется псевдокодом или байт-кодом. Как правило, он не является машинным кодом какого-либо компьютера и программы на нём могут исполняться на различных архитектурах, где имеется соответствующая виртуальная машина, но в некоторых случаях создаются аппаратные платформы, напрямую поддерживающие псевдокод какого-либо языка. Например, псевдокод языка Java называется байт-кодом Java (англ. Java bytecode) и выполняется в Java Virtual Machine, для его прямого исполнения была создана спецификация процессора picoJava. Для платформы .NET Framework псевдокод называется Common Intermediate Language (CIL), а среда исполнения — Common Language Runtime (CLR).

Читайте также:  Щетки стеклоочистителя avantech отзывы

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

Основная статья: JIT-компиляция

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

CIL-код также компилируется в код целевой машины JIT-компилятором, а библиотеки .NET Framework компилируются заранее.

Существуют программы, которые решают обратную задачу — перевод программы с низкоуровневого языка на высокоуровневый. Этот процесс называют декомпиляцией, а такие программы — декомпиляторами. Но поскольку компиляция — это процесс с потерями, точно восстановить исходный код, скажем, на C++, в общем случае невозможно. Более эффективно декомпилируются программы в байт-кодах — например, существует довольно надёжный декомпилятор для Flash. Разновидностью декомпилирования являетсядизассемблирование машинного кода в код на языке ассемблера, который почти всегда выполняется успешно (при этом сложность может представлять самомодифицирующийся код или код, в котором собственно код и данные не разделены). Связано это с тем, что между кодами машинных команд и командами ассемблера имеется практически взаимно-однозначное соответствие.

Раздельная компиляция (англ. separate compilation) — трансляция частей программы по отдельности с последующим объединением их компоновщиком в единый загрузочный модуль.

Исторически особенностью компилятора, отражённой в его названии (англ. compile — собирать вместе, составлять), являлось то, что он производил как трансляцию, так и компоновку, при этом компилятор мог порождать сразу абсолютный код. Однако позже, с ростом сложности и размера программ (и увеличением времени, затрачиваемого на перекомпиляцию), возникла необходимость разделять программы на части и выделять библиотеки, которые можно компилировать независимо друг от друга. При трансляции каждой части программы компилятор порождает объектный модуль, содержащий дополнительную информацию, которая потом, при компоновке частей в исполнимый модуль, используется для связывания и разрешения ссылок между частями.

Появление раздельной компиляции и выделение компоновки как отдельной стадии произошло значительно позже создания компиляторов. В связи с этим вместо термина «компилятор» иногда используют термин «транслятор» как его синоним: либо в старой литературе, либо когда хотят подчеркнуть его способность переводить программу в машинный код (и наоборот, используют термин «компилятор» для подчёркивания способности собирать из многих файлов один).

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

Конечно, во многих ВУЗах есть специальные курсы, посвящённые устройству различных компонентов современных компиляторов и интерпретаторов. И современные компиляторы слишком сложны для того, чтобы рассказать о них в одной статье достаточно подробно. Тем не менее, базовые механизмы работы компиляторов остались неизменными со времён выпуска первого компилятора языка Фортран.


С высоты птичьего полёта.

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

Итак, что же это за части? Это лексический анализатор, или сканер; синтаксический анализатор, или парсер; семантический анализатор; а также один или несколько генераторов кода и один или несколько оптимизаторов. Также к компилятору часто относят дополнительные инструменты, нужные для создания исполняемого файла — сборщик и компоновщик.

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

Читайте также:  Шрифт который легко читается

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

Следом за лексическим анализом может быть препроцессор. Тем, кто знаком с языками программирования C или С++, нет нужды объяснять, в чём состоит его функция. Для остальных же скажу, что основная задача препроцессора — это замена одних лексем другими, которые были заранее определены в тексте программы. Используется препроцессор также для условной компиляции (т.е. когда кусок кода должен быть откомпилирован только при выполнении определённых условий — для определённой платформы, только при отладочном билде и т.д. и т.п.), для выполнения определённых макросов (как в том же C/C++) и некоторых других подобных вещей. Препроцессор не является обязательной частью компилятора, поскольку многие языки программирования не нуждаются в нём.

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

Следом за синтаксическим анализом следует этап анализа семантического. Если синтаксический анализатор строил скелет нашей программы, то семантический помогает этому скелету обрасти плотью. Программа наполняется смыслом: переменные становятся переменными, объекты — объектами, а баги — багами. На самом деле, никакого волшебства не происходит — просто дерево разбора, терпеливо построенное парсером, дополняется семантической информацией о значении идентификаторов. Кстати, на этом этапе возникают и многие ошибки компиляции — например, такие, как несоответствие типов. Хотя, конечно, на парсинг тоже приходится изрядное количество ошибок, без которых, к сожалению, текст свеженаписанной программы обходится крайне редко даже у очень опытных программистов.

Дальше пути различных компиляторов расходятся. В большинстве компиляторов следом за этапом семантического анализа идёт перевод программы в некоторый промежуточный код, который может использоваться для генерации кода под разные аппаратные платформы. Если компилятор выполняет компиляцию только для какой-то одной аппаратной платформы, то программа может транслироваться в коды на языке Ассемблера соответствующей процессорной архитектуры, или, если компилятор трудится для какой-то виртуальной машины (как, например, в случае Java или Microsoft .NET), то переводиться программа может затем в специальный байт-код, понятный соответствующей виртуальной машине. Тем не менее, в большинстве современных компиляторов нет непосредственной трансляции в ассемблерный код — даже если в итоге компилятор не должен стараться для создания кросс-платформенных программ, всё равно, сначала идёт трансляция программы в какой-то промежуточный код, а только потом уже в исполняемый. Причина этого в оптимизации кода.

Современные компиляторы, даже самые слабенькие и плохонькие, поддерживают хотя бы базовую оптимизацию кода. Более мощные коммерческие компиляторы содержат в себе очень мощные алгоритмы оптимизации кода, которые позволяют при некоторых условиях сделать её в разы быстрее. Особенно мощными в плане оптимизации давным-давно тому назад считались компиляторы производства Watcom, сейчас, вроде бы, постепенно восстанавливающие свою былую славу в виде open-source продукта. Потом пальма первенства перешла к компиляторам Intel, и сейчас именно они считаются самыми лучшими компиляторами в плане оптимизации. Что ж, это довольно логично — кому, как ни создателям процессоров, знать, как лучше всего оптимизировать программы для работы на них. Впрочем, не важно, плоха оптимизация в компиляторе или нет — главное, что в любом оптимизирующем компиляторе есть модуль, называемый оптимизатором, который начинает свою работу после генератора промежуточного кода. Справедливости ради стоит сказать, что оптимизатор может работать и после генерации уже исполняемого кода, но в наши дни такая схема встречается уже редко, поскольку производители компиляторов, как правило, выпускают целую линейку подобных продуктов для разных языков программирования и стараются делать оптимизаторы, которые можно встроить в любой из этих компиляторов. Какими именно методами оптимизатор может повышать скорость работы программы — это тема отдельной статьи, которую, возможно, вы когда-нибудь и сможете увидеть на страницах "Компьютерных вестей".

Читайте также:  Как оформить возврат билета на сайте ржд

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

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

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

Ассемблер и компоновщик в ряде случаев встроены прямо в компилятор, в других же случаях они выделяются в отдельные программы, которые запускаются после завершения работы самого компилятора. Их может и вовсе не быть, как, например, для компиляторов, преобразующих программы с одного языка высокого уровня на другой высокоуровневый язык (так называемых фронт-эндов), или они могут присутствовать только в виде, например, специфического сборщика, создающего код для виртуальной машины и помещающего его в какую-то специальную оболочку компоновщика (классический пример — Java с её JAR-файлами). Стоит, тем не менее, сказать пару слов о назначении этих двух инструментов.

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

Компоновщик создаёт из того кода, который сгенерировал ассемблер, исполняемые файлы. Даже для одной и той же процессорной архитектуры исполняемые файлы будут отличаться в зависимости от операционной системы. Например, для Windows формат исполняемых файлов — это Portable Executable (PE), а для Linux — Executable Linked File (EXE).

Как видите, если смотреть на компиляторы со стороны, то всё в них просто и не вызывает никаких особенно заковыристых вопросов. На практике всё, мягко говоря, немного сложнее. И если вы вдруг решите написать собственный компилятор, то не стоит заранее пугаться, хотя к определённым сложностям нужно, как и в любой новой работе, быть готовым. Я бы лично рекомендовал начинать знакомство с предметом с написания какого-нибудь простого эзотерического языка вроде Brainfuck или Whitespace. Поскольку сам я в своё время интересовался благодаря своему знакомому Марату Духану первым из них, то и вам рекомендую его.

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

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

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

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

Схема действия компилятора:

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

Основные задачи компилятора:

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

  1. Производит проверку текста исходной программы на содержание или отсутствие в ней всевозможных синтаксических ошибок.
  2. Разрабатывает исполняемую программу — преобразует исходную программу в машинный код.

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

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

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

Ссылка на основную публикацию
Чему равна сумма бесконечно убывающей геометрической прогрессии
ЧИСЛОВЫЕ ПОСЛЕДОВАТЕЛЬНОСТИ VI § l48. Сумма бесконечно убывающей геометрической прогрессии До сих пор, говоря о суммах, мы всегда предполагали, что...
Хорошие дешевые жесткие диски
Лучший жесткий диск далек от SSD в плане скорости передачи данных, однако есть причина, по которой данные устройства все еще...
Хорошие ноутбуки за 20000 для игр
Если вам необходим хороший ноутбук для работы, то вам придется потратится как минимум 20 тысяч рублей. За эти деньги вы...
Чем стереть автомобильную краску
Автомобили настолько плотно вошли в нашу жизнь, что большинство людей не представляет свою жизнь без персонального транспортного средства. Машина —...
Adblock detector