Как проходит процесс компиляции срр файлов в бинарный файл
Когда вы компилируете свой код, вы можете ожидать, что компилятор компилирует код именно в том виде, как вы его написали. На самом деле это не так.
Перед компиляцией файл кода проходит этап, известный как трансляция. На этапе трансляции происходит много всего, чтобы подготовить ваш код к компиляции (если вам интересно, здесь вы можете найти список этапов трансляции). Файл кода с примененными к нему трансляциями называется единицей трансляции.
Самый примечательный из этапов трансляции связан с препроцессором. Препроцессор лучше всего рассматривать как отдельную программу, которая манипулирует текстом в каждом файле кода.
Выходные данные препроцессора проходят еще несколько этапов трансляции, а затем компилируются. Обратите внимание, что препроцессор никоим образом не изменяет исходные файлы кода – скорее, все изменения текста, сделанные препроцессором, временно размещаются в памяти при каждой компиляции файла кода.
В этом уроке мы обсудим, что делают некоторые из наиболее распространенных директив препроцессора.
В качестве отступления.
Директивы using (представленные в уроке «2.8 – Конфликты имен и пространства имен») не являются директивами препроцессора (и, следовательно, препроцессором не обрабатываются). Таким образом, хотя термин директива обычно означает директиву препроцессора, это не всегда так.
Включения
Рассмотрим следующую программу:
Определения макросов
Существует два основных типа макросов: макросы, подобные объектам, и макросы, подобные функциям.
Макросы, подобные функциям, действуют как функции и служат той же цели. Мы не будем здесь их обсуждать, потому что их использование обычно считается опасным, и почти всё, что они могут сделать, можно сделать и с помощью обычных функций.
Макросы, подобные объектам, можно определить одним из двух способов:
В первом определении нет подставляемого при замене текста, а во втором есть. Поскольку это директивы препроцессора (а не инструкции), обратите внимание, что ни одна из форм не заканчивается точкой с запятой.
Макросы, подобные объектам, с подставляемым текстом
Когда препроцессор встречает эту директиву, любое дальнейшее появление идентификатора заменяется подставляемым текстом. Идентификатор традиционно набирается заглавными буквами с использованием подчеркивания для обозначения пробелов.
Рассмотрим следующую программу:
Препроцессор преобразует приведенный выше код в следующее:
Этот код при запуске печатает: My name is: Alex .
Объектоподобные макросы использовались как более дешевая альтернатива постоянным переменным. Те времена давно прошли, поскольку компиляторы стали умнее, а язык вырос. Теперь объектоподобные макросы можно увидеть только в устаревшем коде.
Мы рекомендуем вообще избегать таких макросов, так как существуют более эффективные способы сделать аналогичные вещи. Мы обсудим это более подробно в уроке «4.14 – const, constexpr и символьные константы».
Макросы, подобные объектам, без подставляемого текста
Макросы, подобные объектам, также могут быть определены без подставляемого при замене текста.
Макросы этого типа работают так, как и следовало ожидать: любое дальнейшее появление идентификатора удаляется и ничем не заменяется!
Это может показаться довольно бесполезным, и, да, это бесполезно для замены текста. Однако эта форма директивы обычно используется для другого. Мы обсудим использование этой формы чуть позже.
В отличие от объектоподобных макросов с заменяющим текстом, макросы этой формы обычно считаются приемлемыми для использования.
Условная компиляция
Рассмотрим следующую программу:
Это обеспечивает удобный способ «закомментировать» код, содержащий многострочные комментарии.
Макросы, подобные объектам, не влияют на другие директивы препроцессора.
Теперь вам может быть это интересно:
На самом деле вывод препроцессора вообще не содержит директив – все они разрешаются/удаляются перед компиляцией, потому что компилятор не знает, что с ними делать.
Директивы разрешаются перед компиляцией сверху вниз для каждого файла.
Рассмотрим следующую программу:
После завершения препроцессора все определенные в данном файле идентификаторы отбрасываются. Это означает, что директивы действительны только с точки определения до конца файла, в котором они определены. Директивы, определенные в одном файле кода, не влияют на другие файлы кода в том же проекте.
В данной статье я хочу рассказать о том, как происходит компиляция программ, написанных на языке C++, и описать каждый этап компиляции. Я не преследую цель рассказать обо всем подробно в деталях, а только дать общее видение. Также данная статья — это необходимое введение перед следующей статьей про статические и динамические библиотеки, так как процесс компиляции крайне важен для понимания перед дальнейшим повествованием о библиотеках.
Все действия будут производиться на Ubuntu версии 16.04.
Используя компилятор g++ версии:
Состав компилятора g++
Мы не будем вызывать данные компоненты напрямую, так как для того, чтобы работать с C++ кодом, требуются дополнительные библиотеки, позволив все необходимые подгрузки делать основному компоненту компилятора — g++.
Зачем нужно компилировать исходные файлы?
Исходный C++ файл — это всего лишь код, но его невозможно запустить как программу или использовать как библиотеку. Поэтому каждый исходный файл требуется скомпилировать в исполняемый файл, динамическую или статическую библиотеки (данные библиотеки будут рассмотрены в следующей статье).
Этапы компиляции:
Перед тем, как приступать, давайте создадим исходный .cpp файл, с которым и будем работать в дальнейшем.
driver.cpp:
1) Препроцессинг
Самая первая стадия компиляции программы.
Получим препроцессированный код в выходной файл driver.ii (прошедшие через стадию препроцессинга C++ файлы имеют расширение .ii), используя флаг -E, который сообщает компилятору, что компилировать (об этом далее) файл не нужно, а только провести его препроцессинг:
Взглянув на тело функции main в новом сгенерированном файле, можно заметить, что макрос RETURN был заменен:
В новом сгенерированном файле также можно увидеть огромное количество новых строк, это различные библиотеки и хэдер iostream.
2) Компиляция
На данном шаге g++ выполняет свою главную задачу — компилирует, то есть преобразует полученный на прошлом шаге код без директив в ассемблерный код. Это промежуточный шаг между высокоуровневым языком и машинным (бинарным) кодом.
Ассемблерный код — это доступное для понимания человеком представление машинного кода.
Используя флаг -S, который сообщает компилятору остановиться после стадии компиляции, получим ассемблерный код в выходном файле driver.s:
Мы можем все также посмотреть и прочесть полученный результат. Но для того, чтобы машина поняла наш код, требуется преобразовать его в машинный код, который мы и получим на следующем шаге.
3) Ассемблирование
Так как x86 процессоры исполняют команды на бинарном коде, необходимо перевести ассемблерный код в машинный с помощью ассемблера.
Ассемблер преобразовывает ассемблерный код в машинный код, сохраняя его в объектном файле.
Объектный файл — это созданный ассемблером промежуточный файл, хранящий кусок машинного кода. Этот кусок машинного кода, который еще не был связан вместе с другими кусками машинного кода в конечную выполняемую программу, называется объектным кодом.
Далее возможно сохранение данного объектного кода в статические библиотеки для того, чтобы не компилировать данный код снова.
Получим машинный код с помощью ассемблера (as) в выходной объектный файл driver.o:
Но на данном шаге еще ничего не закончено, ведь объектных файлов может быть много и нужно их всех соединить в единый исполняемый файл с помощью компоновщика (линкера). Поэтому мы переходим к следующей стадии.
4) Компоновка
Компоновщик (линкер) связывает все объектные файлы и статические библиотеки в единый исполняемый файл, который мы и сможем запустить в дальнейшем. Для того, чтобы понять как происходит связка, следует рассказать о таблице символов.
Таблица символов — это структура данных, создаваемая самим компилятором и хранящаяся в самих объектных файлах. Таблица символов хранит имена переменных, функций, классов, объектов и т.д., где каждому идентификатору (символу) соотносится его тип, область видимости. Также таблица символов хранит адреса ссылок на данные и процедуры в других объектных файлах.
Именно с помощью таблицы символов и хранящихся в них ссылок линкер будет способен в дальнейшем построить связи между данными среди множества других объектных файлов и создать единый исполняемый файл из них.
Получим исполняемый файл driver:
5) Загрузка
Последний этап, который предстоит пройти нашей программе — вызвать загрузчик для загрузки нашей программы в память. На данной стадии также возможна подгрузка динамических библиотек.
Запустим нашу программу:
Заключение
В данной статье были рассмотрены основы процесса компиляции, понимание которых будет довольно полезно каждому начинающему программисту. В скором времени будет опубликована вторая статья про статические и динамические библиотеки.
Князев Алексей Александрович. Независимый программист и консультант.
Обзор компиляторов
Существует множество компиляторов с языка C++, которые можно использовать для создания исполняемого кода под разные платформы. Проекты компиляторов можно классифицировать по следующим критериям.
- Коммерческие и некоммерческие проекты
- Уровень поддержки современных тенденций и стандартов языка
- Эффективность результирующего кода
Если на использование коммерческих компиляторов нет особых причин, то имеет смысл использовать компилятор с языка C++ из GNU коллекции компиляторов (GNU Compiler Collection). Этот компилятор есть в любом дистрибутиве Linux, и, он, также, доступен для платформы Windows как часть проекта MinGW (Minumum GNU for Windows). Для работы с компилятором удобнее всего использовать какой-нибудь дистрибутив Linux, но если вы твердо решили учиться программировать под Windows, то удобнее всего будет установить некоммерческую версию среды разработки QtCreator вместе с QtSDK ориентированную на MinGW. Обычно, на сайте производителя Qt можно найти инсталлятор под Windows, который сразу включает в себя среду разработки QtCreator и QtSDK. Следует только быть внимательным и выбрать ту версию, которая ориентирована на MinGW. Мы, возможно, за исключением особо оговариваемых случаев, будем использовать компилятор из дистрибутива Linux.
GNU коллекция компиляторов включает в себя несколько языков. Из них, группу языков Си составляет три компилятора.
Этапы компиляции
Процесс обработки текстовых файлов с кодом на языке C++, который упрощенно называют "компиляцией", на самом деле, состоит из четырех этапов.
Рассмотрим подробнее упомянутые выше стадии обработки текстовых файлов на языке C++.
Препроцессинг
Препроцессинг, это процедура ставшая традиционной для многих обработчиков разного рода описаний, в том числе и текстов с кодами программ. В общем случае, везде, где возникает необходимость в предварительной обработке текстов реализуется некоторый язык препроцессинга элементы которого ищутся препроцессором при обработке файла.
На входе препроцессора мы имеем исходный файл с текстом на языке C++ включающим в себя элементы языка препроцессора.
На выходе препроцессора получаются так называемые компиляционные листы, состоящие исключительно из выражений языка C++, которых должно быть достаточно для создания объектных файлов на следующих этапах обработки. Последнее означает, что на момент использования каких-либо символов языка из других файлов, объявления этих символов должны присутствовать в компиляционном листе выше. Именно такие подстановки и призван осуществлять препроцессор. Часто, на вход препроцессора поступает файл размером в несколько десятков строк, а на выходе получается компиляционный лист из десятков тысяч строк.
Ассемблирование
Процесс ассемблирования с одной стороны достаточно прост для понимания и с другой стороны является наиболее сложным в реализации. По своей сути это процесс трансляции выражений одного языка в другой. Более конкретно, в данном случае, мы имеем на входе утилиты ассемблера файл с текстом на языке C++ (компиляционный лист), а на выходе мы получаем файл с текстом на языке Ассемблера. Язык Ассемблера это низкоуровневый язык который практически напрямую отображается на коды инструкций процессора целевой системы. Отличие только в том, что вместо числовых кодов инструкций используется англоязычная мнемоника и кроме непосредственно кодов инструкций присутствуют еще директивы описания сегментов и низкоуровневых данных, описываемых в терминологии байтов.
Ассемблирование не является обязательным процессом обработки файлов на языке C++. В данном случае, мы наблюдаем лишь общий подход в архитектуре проекта коллекции компиляторов GNU. Чтобы максимально объеденить разные языки в одну коллекцию, для каждого из языков реализуется свой транслятор на язык ассемблера и, при необходимости, препроцессор, а компилятор с языка ассемблера и линковщик делаются общими для всех языков коллекции.
Компиляция
В данном случае, мы имеем компилятор с языка ассемблера. Результатом его работы является объектный файл полученный на основе всего того текста, что был предоставлен в компиляционном листе. Поэтому можно говорить, что каждый объектный файл проекта соответствует одному компиляционному листу проекта.
Исходя из этих определений, в компиляционном листе перед компиляцией должны существовать все объявления (declaration) всех тех сущностей, что используются в этом компиляционном листе. Причем их объявления должны находится до момента использования этих сущностей. Иначе, компилятор не сможет подготовить обращение к соответствующей сущности. Например, не сможет оформить передачу параметров через стек вызова функции и подготовиться к приему возвращаемого функцией значения.
Линковка
На этапе линковки выполняется объединение всех объектных файлов проекта, откомпилированных по соответствующим компиляционным листам проекта в единую сущность. Это может быть приложение, статическая или динамическая библиотека. Разница в бинарных заголовках целевых файлов и несколько различной внутренней организацией. Первичной задачей линковки следует назвать задачу по подстановке адресов вызова внешних объектов, которые были образованы в объектных файлах проекта. Соответствующие реализации сущностей с адресами их размещения должны находится в видимости линковщика. Эти сущности должны быть либо в объектных файлах, тогда они должны быть указаны в списке линковки, либо во внешних библиотеках функций, статических или динамических, тогда они должны быть указаны в списке внешних библиотек.
Средства сборки проекта
Традиционно, программа на языке C++ собирается средствами утилиты make исполняющей сценарий из файла Makefile. Сценарий сборки можно писать самостоятельно,
а можно создавать его автоматически с помощью всевозможных средств организации проекта. Среди наиболее известных средств организации проекта можно указать следующие.
Современные версии QtCreator могут работать с проектами, которые используют как систему сборки QMake, так и систему сборки CMake.
Простой пример компиляции
Рассмотрим простейший проект "Hello world" на языке C++. Для его компиляции мы будет использовать консоль, в которой будем писать прямые команды компиляции. Это позволит нам максимально прочувствовать описанные выше этапы компиляции. Создадим файл с именем main.cpp и поместим в него следующий текст программы.
В представленом примере выполнена нумерация строк, чтобы упростить пояснения по коду. В реальном коде нумерации не должно быть, так как она не входит в синтаксическое описание конструкций языка C++.
В третьей строке программы описана функция main(). В контексте операционной системы, каждое приложение должно иметь точку входа. Такой точкой входа в операционных системах *nix является функция main(). Именно с нее начинается исполнение приложения после его загрузки в память вычислительной системы. Так как операционная система Windows имеет корни тесно переплетенные с историей *nix, и, фактически, является далеким проприентарным клоном *nix, то и для нее справедливо данное правило. Поэтому, если вы пишете приложение, то начинается оно всегда с функции main().
В пятой строке мы обращаемся к предопределенному объекту cout из пространства имен std, который связан с потоком вывода приложения. Используя синтаксис операций, определенных для указанного объекта, мы передаем в него строку "Hello world" и символ возврата каретки и переноса строки.
В седьмой строке мы возвращаем код 0, как код возврата функции main(). В организации процессов в операционной системы, это число будет восприниматься как код возврата приложения.
Следующим шагом проведения эксперимента выполним останов компиляции файла main.cpp после этапа ассемблирования. Для этого воспользуемся ключом -S для компилятора g++. Здесь и далее, знак доллара ($) обозначает стандартное приглашение к вводу команды в консоли *nix. Писать знак доллара не требуется.
Выполнив остановку компиляции после этапа ассемблирование, возможно будет интересно выполнить остановку компиляции и после этапа, который собственно, и выполняет компиляцию, т.е. превращение ассемблерного кода в объектный файл, который впоследствии надо будет слинковать с библиотеками, в которых будет найдено реализация объекта cout, который используется в нашей программе как некий библиотечный объект.
Для остановки компиляции после, собственно, компиляции следует воспользоваться ключом -c для компилятора g++.
Наконец, если нас не интересуют эксперименты с остановками компиляции на разных этапах и если мы просто хотим получить из нашего файла на языке C++ исполняемую программу, то следует выполнить следующую команду.
Программа на C / C ++ представляет собой сложный процесс из текста в исполняемый файл. Для файлов с исходным кодом (.c / .cpp) мы не можем запустить его напрямую. Мы должны пройти серию обработки, чтобы преобразовать ее в машинный язык, а затем Связать соответствующие файлы в исполняемые программы. Этот процесс называется процессом компиляции и компоновки. Длина этой статьи длинная. Я хочу увидеть анализ непосредственно.здесь
Вот процесс компиляции и связывания исходного кода с исполняемым файлом:
Процесс компиляции
Процесс компиляции файла C можно разделить на:Compile икомпиляция
1.1 сборник
Компиляция означает, что компилятор читает исходную программу (поток символов), анализирует ее лексически и грамматически и преобразует инструкции языка высокого уровня в функционально эквивалентный ассемблерный код.
Короче говоря, он переводит код языка высокого уровня (здесь мы говорим об исходных файлах C) в код сборки
Преобразование инструкций языка высокого уровня в код сборки требует двух процессов:
1.1.1 Предварительная обработка
Процесс предварительной обработки проходитпрепроцессорДля этого препроцессор - это программа, которая обрабатывает входные данные в программе и создает данные, которые можно использовать для входа в другие программы. Вывод называется предварительно обработанной формой входных данных и часто используется в последующих программах, таких как компиляторы.
В этой статье обсуждается только препроцессор C, который является препроцессором для языков C и C ++. Используется для предварительного сканирования исходного кода перед обработкой компиляторомВключение заголовочных файлов, Макро-расширение, Условная компиляция, Линия управления И так далее.
Для языка C / C ++ предварительная обработка обычно делится на следующие процессы:
1.1.1.1 Включить файлы
1.1.1.2 Условная компиляция
Эти инструкции могут указывать разные макросы для определения того, какой код выполняется, а какой нет. В процессе предварительной обработки те программы, которые не выполняются, фильтруются. Препроцессор только выводит исполняемый код в компилятор.
1.1.1.3 Определение и расширение макроса
1.1.1.4 Специальные макросы и инструкции
Некоторые специальные макросы заменяются непосредственно во время предварительной обработки, например, язык C / C ++ определяет стандартные макросы.____LINE, ____FILE Заменить непосредственно на текущий номер строки и файл
1.1.1.5 Подключение токена
1.1.1.6 Определяемые пользователем ошибки и предупреждения компиляции
1.1.1.7 Особенности предварительной обработки для конкретного компилятора
1.1.2 Компиляция и оптимизация
Этот процесс осуществляется через лексический анализ и грамматический анализ.После подтверждения того, что все инструкции соответствуют грамматическим правилам, они переводятся в эквивалентные представления промежуточного кода или ассемблерного кода.
Процесс сборки здесь выполняется компилятором, а программа, введенная препроцессором под действием компилятора (Compiler), компилируется в код сборки.
1.2 сборник
Процесс сборки на самом деле представляет собой процесс перевода кода сборки на целевой машинный язык.Сгенерированный объектный файл является кодом машинного языка, который логически эквивалентен исходной программе.
Сгенерированный код машинного языка называетсяКод объектаСгенерированный двоичный файл называется объектным файлом и также становится двоичным файлом.
Тип данных типичного сгенерированного объектного файла:
Процесс связывания
Процесс связывания выполняетсяLinkerЛинкер (англ .: Linker), также переводится как линкер, линкер, это программа, которая связывает один или несколько объектных файлов, созданных компилятором или ассемблером, плюс библиотеку в исполняемый файл.
Исполняемый файл был получен в процессе компиляции. Здесь мы в основном обсуждаем дополнительную ссылку на библиотеку. Большинство операционных систем теперь предоставляютСтатическая ссылкаиДинамическая ссылкаЭти два способа связыванияВот процесс подключения:
2.1 Ссылки
2.1.1 статическое связывание (время компиляции)
Компоновщик копирует код функции из своего местоположения (объектный файл или статически связанная библиотека) в конечную исполняемую программу. Этот код затем загружается в виртуальное адресное пространство процесса при выполнении программы. Статически связанная библиотека на самом деле представляет собой набор объектных файлов, каждый из которых содержит код одной или набора связанных функций в библиотеке.
Преимущества: вам нужно только убедиться, что на компьютере разработчика установлен правильный файл библиотеки. При публикации в двоичном формате вам не нужно учитывать существование и версию файла библиотеки на компьютере пользователя.
Недостатки: сгенерированный исполняемый файл является относительно большим. Чтобы избежать этой проблемы, изначально была разработана технология динамических библиотек.
2.1.1 Динамическое связывание (загрузка, время выполнения)
Так называемое динамическое связывание - это превращение некоторого часто используемого кода (статически связанной библиотеки программ OBJ) в файл DLL. Когда исполняемый файл вызывает функции из файла DLL, операционная система загружает файл DLL в память. Структура самого файла - исполняемый файл, функции связываются, когда они нужны программе. Благодаря динамическому связыванию потери памяти могут быть значительно уменьшены. Библиотеки статических ссылок связаны непосредственно с исполняемыми файлами.
Сам файл DLL также является исполняемым файлом, который может быть вызван напрямую динамически при выполнении программы.
2.1.3 Сравнение статической ссылки и динамической ссылки
Статическая ссылка | Динамическая ссылка |
---|---|
Время компиляции | Загрузка, время выполнения |
lib собирается в exe-файл во время компиляции | EXE-файл может динамически загружать DLL при запуске программы |
Независимо от версии файла компьютерной библиотеки | Сохранить память и ремонтопригодность |
Весь пакет имеет только EXE-файлы | Exe и DLL в пакете |
Файл lib является внешней функцией и переменной, которая копируется в целевую программу во время компиляции с суффиксом .a | Сам файл dll является исполняемым, динамически связанным во время выполнения и может содержать несколько комбинаций исходного кода, данных и ресурсов с суффиксом .so |
После того, как функция компоновщика сформирует исполняемый файл, требуется выполнить одну заключительную операцию для его упаковки. Создаемый файл исполняемого файла (.exe, .dll, .lib) упакован. Он может быть доставлен на компьютер и запущен.
3. Компилировать и связывать анализ процесса
Мы создаем три файла какtest.h, test.cиmain.c
3.1 Операция предварительной обработки
Можно обнаружить, что операция предварительной обработки расширяет все определенные макросы, включая заголовочные файлы, удаление комментариев и т. Д.1.1.1
3.2 сборник
Компилировать для генерации .sКод сборкиВыньте основную функцию для анализа следующего
3.2 сборник
Компилировать и генерировать машинный код .o файлы, мы не можем понять.
main.o иtest.o
3.2 Ссылки
Мы связываем файлы .o, полученные из предыдущей компиляции.
Это потому, что файл test.o не упакован в нужный нам файл формата lib, то есть файл с суффиксом .a. Упаковка. Упакованныйtest.a
Ссылка успешно, выполнить main.exe
Код в моем личном блоге, пожалуйста, посетитеYancyKahn
Выше приведен весь процесс компиляции и компоновки. Если вам это нравится, нравится.
Читайте также: