Что такое компилирование программы. Что такое компилятор - описание. Разделение текста программы на модули

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

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

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

Виды компиляторов

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

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

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

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

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

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

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

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

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

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

Виды компиляции

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

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

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

Структура компилятора

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

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

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

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

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

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

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

Генерация кода

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

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

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

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

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

Генерация байт-кода

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

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

Динамическая компиляция

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

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

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

Декомпиляция

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

Раздельная компиляция

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

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

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

Одной из ключевых характеристик PHP является то, что это интерпретируемый язык программирования. С другой стороны, языки программирования наподобие C , изначально разрабатывались для компиляции. Что это значит?

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

Рабочий цикл программы

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

Когда пишете программу, вы хотите, чтобы ее инструкции работали на компьютере. Компьютер обрабатывает информацию с помощью процессора, который поэтапно выполняет инструкции, закодированные в двоичном формате. Как из выражения «a = 3; » получить закодированные инструкции, которые процессор может понять?

Мы делаем это с помощью компиляции. Существует специальные приложения, известные как компиляторы. Они принимают программу, которую вы написали. Затем анализируют и разбирают каждую часть программы и строят машинный код для процессора. Часто его также называют объектным кодом.

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

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

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

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

Не все языки программирования учитывают это в своей концепции. Например, Java предназначался для запуска в «интерпретирующей » среде, а Python всегда должен интерпретироваться.

Интерпретация программы

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

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

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

Это ломает рабочий цикл, который был приведен на диаграмме выше. Теперь у нас есть новая диаграмма:


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

Например, так работают такие языки программирования, как Python . Вы пишете программу. Затем вводите код в интерпретатор Python , и он выполняет все описанные вами шаги. В командной строке вы можете ввести примерно следующее:

C:>python myprogram.py

В этой команде Python — это исполняемый файл. Вы вводите в него все, что находится в файле myprogram.py, и он выполняет эти инструкции. Компьютер не запустит myprogram.py без Python . Это не машинный код, который понимает процессор. Можно скомпилировать программы Python в объектный или машинный код и запустить его непосредственно в процессоре. Но эта процедура включает в себя компиляцию кода и добавление в качестве ее части всего интерпретатора Python .

Природа интерпретатора

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

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

Такой код быстрее обрабатывается, и его проще написать для исполнителя (части интерпретатора, которая исполняет ), который считывает байтовый код, а не код источника.

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

У меня есть эмулятор для игровой приставки NIntendo . Когда я загружаю ROM-файл Dragon Warrior , он форматируется в машинный код, который понимает только процессор NES . Но если я создаю виртуальный процессор, который интерпретирует байтовый код во время работы на другом процессоре, я могу запустить Dragon Warrior на любой машине с эмулятором.

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

За и против

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

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

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

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

Другим преимуществом интерпретаторов является то, что их проще переписать или перекомпилировать для новых платформ.

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

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

Это проблема для конкретных real-time приложений, таких как игры с высоким разрешением и симуляцией. Некоторые интерпретаторы содержат компоненты, которые называются just-in-time компиляторами (JIT ). Они компилируют программу непосредственно перед ее исполнением. Это специальные программы, вынесенные за рамки интерпретатора. Но поскольку процессоры становятся все более мощными, данная проблема становится менее актуальной.

Заключение

Имейте всегда в виду, что некоторые языки программирования специально предназначены для компиляции кода, например, C . В то время как другие языки всегда должны интерпретироваться, например Java .

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

Некоторые системы не предлагают технические условия для эффективного использования интерпретаторов. Поэтому вы должны запрограммировать их с помощью чего-то, что может быть непосредственно скомпилировано, например C . Иногда нужно выполнить вычисления настолько интенсивно, насколько это возможно. Например, при точном распознавании голоса роботом. В других случаях скорость или вычислительная мощность могут быть не столь критичными, и написать эмулятор на оригинальном языке может быть проще.

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

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

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

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

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

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

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

Гибкий компилятор был разработан на основе модульного принципа. Его управление осуществляется таблицами. Запрограммирован он на высокоуровневом языке. Также возможна его реализация при помощи компилятора компиляторов.

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

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

Который воспринимает формальное описание для языка программирования. Он способен самостоятельно генерировать компилятор для конкретного языка.

Отладочный компилятор может самостоятельно устранять некоторые виды ошибок синтаксиса.

Резидентному компилятору отведено постоянное место в оперативной памяти, и он доступен при повторном использовании широким спектром задач.

Существуют самокомпилируемые компиляторы. Они пишутся тем же языком, с которого происходит трансляция.

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

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

Компиляция Java реализовывается при использовании компиляторов, работающих на самых различных платформах. Это позволяет исходные коды перекомпилировать под потребности операционных систем от разных производителей.

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


Все действия будут производиться на Ubuntu версии 16.04 .
Используя компилятор g++ версии:


$ g++ --version g++ (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609

Состав компилятора g++

  • cpp - препроцессор
  • as - ассемблер
  • g++ - сам компилятор
  • ld - линкер

Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора - g++ .

Зачем нужно компилировать исходные файлы?

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

Этапы компиляции:

Перед тем, как приступать, давайте создадим исходный.cpp файл, с которым и будем работать в дальнейшем.


driver.cpp :


#include using namespace std; #define RETURN return 0 int main() { cout << "Hello, world!" << endl; RETURN; }

1) Препроцессинг

Самая первая стадия компиляции программы.


Препроцессор - это макро процессор , который преобразовывает вашу программу для дальнейшего компилирования. На данной стадии происходит происходит работа с препроцессорными директивами. Например, препроцессор добавляет хэдеры в код (#include ), убирает комментирования, заменяет макросы (#define ) их значениями, выбирает нужные куски кода в соответствии с условиями #if , #ifdef и #ifndef .
Хэдеры, включенные в программу с помощью директивы #include , рекурсивно проходят стадию препроцессинга и включаются в выпускаемый файл. Однако, каждый хэдер может быть открыт во время препроцессинга несколько раз, поэтому, обычно, используются специальные препроцессорные директивы, предохраняющие от циклической зависимости.


Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii ), используя флаг -E , который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:


g++ -E driver.cpp -o driver.ii

Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:


int main() { cout << "Hello, world!" << endl; return 0; }

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

2) Компиляция

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


Ассемблерный код - это доступное для понимания человеком представление машинного кода.


Используя флаг -S , который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s :


$ g++ -S driver.ii -o driver.s

driver.s

File "driver.cpp" .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .section .rodata .LC0: .string "Hello, world!" .text .globl main .type main, @function main: .LFB1021: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %esi movl $_ZSt4cout, %edi call _ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc movl $_ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, %esi movq %rax, %rdi call _ZNSolsEPFRSoS_E movl $0, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1021: .size main, .-main .type _Z41__static_initialization_and_destruction_0ii, @function _Z41__static_initialization_and_destruction_0ii: .LFB1030: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movl %edi, -4(%rbp) movl %esi, -8(%rbp) cmpl $1, -4(%rbp) jne .L5 cmpl $65535, -8(%rbp) jne .L5 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi call __cxa_atexit .L5: nop leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1030: .size _Z41__static_initialization_and_destruction_0ii, .-_Z41__static_initialization_and_destruction_0ii .type _GLOBAL__sub_I_main, @function _GLOBAL__sub_I_main: .LFB1031: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $65535, %esi movl $1, %edi call _Z41__static_initialization_and_destruction_0ii popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE1031: .size _GLOBAL__sub_I_main, .-_GLOBAL__sub_I_main .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I_main .hidden __dso_handle .ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.9) 5.4.0 20160609" .section .note.GNU-stack,"",@progbits


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

3) Ассемблирование

Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера .
Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле .


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


Получим машинный код с помощью ассемблера (as ) в выходной объектный файл driver.o :


$ as driver.s -o driver.o

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

4) Компоновка

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


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


Получим исполняемый файл driver :


$ g++ driver.o -o driver // также тут можно добавить и другие объектные файлы и библиотеки

5) Загрузка

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


Запустим нашу программу:


$ ./driver // Hello, world!

Заключение

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

Теги: c++, compiler, gcc, g++

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

Компилятор: определение и история возникновения

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

Компилятор: принцип работы

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

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

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

Компилятор и класс

Многие из вас наверняка слышали о таких языках программирования, как C++ и C. Это одни из наиболее распространенных и популярных языков. Такие серьезные языки программирования содержат мощные понятия, которые удобны для отображения понятий прикладных областей. Там, к примеру, присутствует такое понятие, как классы и функции. Они являются основополагающими для многих языков программирования, но для C++ они особенно характерны. Программисту намного удобнее будет создавать модели при помощи таких понятий. Компилятор C для любой операционной системы дает возможность отобразить такие высокоуровневые вещи в понятной для компьютера форме. Тогда компьютер легко сможет ими манипулировать. Любая вычислительная машина, какой бы сложной она не была, оперирует простыми понятиями. Однако понятие класса можно назвать трудным, поскольку с его помощью удобно отражать многие объекты реальной жизни. Задача компилятора заключается в том, чтобы превращать сложные понятия в примитивные.

Разработка компиляторов

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



Есть вопросы?

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: