Чем компиляция отличается от сборки
Перейти к содержимому

Чем компиляция отличается от сборки

  • автор:

Сборка или компиляция

У меня есть теоретический вопрос о различии между компиляцией и сборкой. Я программирую в проекте С++, который занимает много времени для сборки, поэтому я сказал, что нужно делать сборку только в тех случаях, когда «я модифицировал любой заголовочный файл». Это правда? Если я добавлю новый атрибут в файл заголовка, тогда мне нужно построить? Недостаточно компиляции?

6 ответов

«Строительство» — это неопределенный термин, который обычно означает весь процесс, предварительную обработку, компиляцию и привязку. Какие части этих процессов нужно переделать после изменения источника, зависит от того, что изменилось. Если вы изменили только один источник .cpp , достаточно перекомпилировать его и снова связать объекты. Если вы измените заголовок .h , все исходные файлы, содержащие этот заголовок, должны быть перекомпилированы, что обычно дорого, поскольку заголовки, связанные с проектом, как правило, включены во многие исходные файлы.

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

Компиляция — это преобразование исходного кода в код объекта.

Связывание — это комбинация объектного кода с библиотеками в необработанный исполняемый файл.

Building — это последовательность, состоящая из компиляции и компоновки, возможно, с другими задачами, такими как создание установщика.

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

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

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

В случае изменения файла заголовка файл заголовка может повлиять на несколько файлов С++ и, следовательно, для получения окончательного исполняемого файла вам необходимо его создать

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

Build — это полный процесс преобразования исходного кода в исполняемый файл, поскольку компиляция С++ представляет собой преобразование исходного кода в объектный код. В сборке код С++ будет скомпилирован, а затем вам понадобятся другие этапы, включая фазу ссылок, для создания исполняемого файла. Сборка также может включать в себя другие этапы, например. preprocess или генерации файлов исходного кода перед компиляцией.

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

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

Построить или скомпилировать

У меня есть теоретический вопрос о разнице между компиляцией и сборкой. Я программирую проект на С++, сборка которого занимает много времени, поэтому я сказал делать сборку только в тех случаях, когда «я изменил любой заголовочный файл». Это правда? Если я добавлю новый атрибут в файл заголовка, нужно ли мне его создавать? Разве недостаточно компиляции?

задан 04 марта ’13, 09:03

Вы должны построить его. Но инструменты IDE или make просто создадут obj, lib и exe, которые необходимо собрать. — neohope

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

6 ответы

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

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

ответ дан 04 мар ’13, в 10:03

составление это акт превращения исходного кода в объектный код.

Связывающий это акт объединения объектного кода с библиотеками в необработанный исполняемый файл.

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

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

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

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

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

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

ответ дан 04 мар ’13, в 10:03

С последним не соглашусь — компиляция в одиночку ИМЕЕТ применение — иногда вам нужно скомпилировать, чтобы проверить, правильно ли компилируется код — вам не нужно собирать, потому что ваше приложение еще не готово. или эта часть просто не предназначена для непосредственного вывода exe. — jave.веб

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

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

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

ответ дан 04 мар ’13, в 10:03

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

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

ответ дан 04 мар ’13, в 10:03

Я не уверен, что полностью понял ваш вопрос.

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

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

ответ дан 04 мар ’13, в 10:03

Не тот ответ, который вы ищете? Просмотрите другие вопросы с метками c++ build compilation or задайте свой вопрос.

Сборка или компиляция

У меня есть теоретический вопрос о разнице между компиляцией и сборкой. Я программирую в проекте c ++, сборка которого занимает много времени, поэтому я сказал делать сборку только в тех случаях, когда «я изменил любой заголовочный файл». Это правда? Если я добавлю новый атрибут в файл заголовка, нужно ли мне его строить? Не достаточно компиляции?

Решение

«Сборка» — это неопределенный термин, который обычно означает весь процесс, предварительную обработку, компиляцию и компоновку. Какие части этих процессов должны быть переделаны после изменения источника, зависит от того, что изменилось. Если вы изменили только один .cpp source, достаточно перекомпилировать его и снова связать объекты. Если вы измените .h заголовок, все исходные файлы, содержащие этот заголовок, должны быть перекомпилированы, что, как правило, обходится дорого, поскольку заголовки, относящиеся к конкретному проекту, обычно включаются во многие исходные файлы.

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

Другие решения

составление это превращение исходного кода в объектный код.

соединение это процесс объединения объектного кода с библиотеками в сырой исполняемый файл.

Строительство это последовательность, составленная из компиляции и компоновки, возможно, с другими задачами, такими как создание установщика.

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

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

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

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

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

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

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

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

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

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

Я не уверен, что полностью понял ваш вопрос.

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

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

C2018/Сборка программ на C

В общем случае исполняемый файл (англ. executable file) — файл, содержащий программу в виде инструкций, которые могут быть исполнены компьютером. Исполняемым файлам противопоставляются файлы с данными (data file) — файлы, которые читаются и парсятся определённой программой, а сами по себе не могут быть исполнены.

Инструкции (код) — это:

  • либо машинные инструкции для выполнения на физическом процессоре;
  • либо исходный код (сценарий, скрипт, псевдокод), записанный на одном из интерпретируемых языков программирования (пример: bash-скрипты, Python-программы, bat-файлы);
  • либо байт-код виртуальной машины (пример: class-файлы JVM, pyc-файлы для Python).

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

Формат и отличительные особенности исполняемых файлов зависят от операционной системы. Однако концепция является общей.

Формат в UNIX

В UNIX-подобных системах (Linux, FreeBSD, . ) исполняемые файлы отличаются по специальному атрибуту execute в файловой системе, расширение файла значение не имеет (обычно не имеют расширения).

В современных UNIX-подобных системах используется формат ELF (Executable and Linkable Format).

Слово ELF в английском также имеет значение «эльф». В продолжение темы средневекового фэнтези, широко используемый формат представления отладочной информации был назван DWARF (англ. «гоблин», «гном»). Изначально проектировался вместе с ELF, название дано в том же стиле, и позже придумана расшифровка Debugging With Attributed Record Formats.

До ELF, в семидесятые годы использовался формат a.out (assembler output). Отсюда пошла традиция, что при компиляции в GCC по умолчанию выходной файл называется a.out, хотя имеет формат ELF.

Формат в Mac OS

В операционных системах от Apple (iOS и Mac OS X) свой формат — Mach-O (сокращение от Mach object).

Формат в Windows

В Windows исполняемые файлы имеют расширение exe.

Формат исполняемых файлов называется PE (Portable Executable). Первые два байта PE файла содержат сигнатуру 0x4D 0x5A — «MZ». PE представляет собой модифицированную версию COFF (Common Object File Format) формата файла для UNIX. PE/COFF — альтернативный термин при разработке Windows.

Сборка простейшей программы

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

На выходе a.out — исполняемый файл.

На выходе main.exe — исполняемый файл.

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

Стадии

Классический сценарий сборки кода на C включает четыре этапа.

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

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

Посмотреть результат препроцессинга можно через

Результат выводится на стандартный вывод.

Вывод может быть большим. Например, на этот 7-строчный файл gcc 5.4.0 генерирует 854 строки. Если инклудов много, там будут тысячи строк.

Компиляция

Обработка исходного кода (уже без директив препроцессора) и преобразование его в команды ассемблера для целевой платформы (например x86).

Результат записывается в файл main.s.

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

Преобразование кода на языке ассемблера в бинарный формат — в объектный файл.

Результат записывается в файл main.o.

Компоновка (linking)

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

Диаграмма

GCC-CompilationProcess.png

Сборка программ из нескольких C-файлов

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

Каждый C-файл можно компилировать независимо в свой объектный файл, а затем компоновать.

Это даёт следующие преимущества.

  • Разные C-файлы компилируются параллельно на разных ядрах процессора.
  • Если изменяется один C-файл, достаточно перекомпилировать только его.

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

Функции и переменные в C

Объявление и определение

Необходимо понимать разницу между объявлением и определением.

Определение (definition) связывает имя с реализацией:

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

Объявление (declaration) говорит компилятору, что определение функции или переменной (с конкретным именем) существует в другом месте программы (вероятно, в другом C-файле).

Заметьте, что определение также является объявлением — фактически это объявление, в котором «другое место» программы совпадает с текущим.

Классификация

Интуитивно понятным является понятие области видимости переменной (scope):

  1. глобальные переменнные,
  2. локальные переменные.

Глобальные объявляются вне функции и могут быть доступны из разных функций. Локальные переменные видимы только внутри функции (более того, в рамках блока, ограниченного фигурными скобками).

Мы уже рассматривали три класса хранения (storage class):

  1. статический,
  2. автоматический,
  3. динамический.

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

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

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

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

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

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

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

Содержание объектного файла — в сущности, две вещи:

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

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

Объектный код — это последовательность машинных инструкций для процессора, которая соответствует C-коду: if‘ы и while‘ы и пр. Эти инструкции должны манипулировать информацией определённого рода, а информация должна где-нибудь находиться — для этого нам и нужны переменные. Код может также ссылаться на другой код (в частности, на другие C-функции в программе).

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

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

По существу компилятор оставляет пустые места. Пустое место (ссылка) имеет имя, но значение, соответствующее этому имени, пока неизвестно.

Учитывая это, мы можем изобразить объектный файл, соответствующей программе, приведённой выше, следующим образом:

Linker1.png

Анализ объектного файла

Полезно посмотреть, как это работает на практике.

Объектный файл, хоть и не может быть запущен напрямую, имеет формат, схожий с форматом исполняемого файла.

На UNIX объектные файлы имеют расширение o. Формат — ELF.

На Windows используется расширение obj. Формат — COFF.

На платформе UNIX основным инструментом для нас будет команда nm, которая выдаёт информацию о символах объектного файла. Для Windows команда dumpbin с опцией /symbols является приблизительным эквивалентом. Также есть портированные под Windows инструменты GNU binutils, которые включают nm.exe.

Давайте посмотрим, что выдаёт nm для объектного файла, полученного из нашего примера выше.

Результат может выглядеть немного по разному на разных платформах.

Ключевыми сведениями являются класс каждого символа и его размер (если присутствует).

  • Класс U (от undefined) обозначает неопределённые ссылки, те самые «пустые места», упомянутые выше. Для этого класса существует два объекта: fn_a и z_global.
  • Классы t и T (от слова text) указывают на код, который определён; различие между t и T заключается в том, является ли функция статической (t) (локальной в файле) или нет (T), т.е. была ли функция объявлена как static.
  • Классы d и D (от слова data) содержат инициализированные глобальные переменные. При этом статические переменные принадлежат классу d.
  • Для неинициализированных глобальных переменных мы получаем b, если они статичные, и B или C иначе.

Что делает компоновщик

Компоновщик осуществляет заполнение пустых мест в объектных файлах. Проиллюстрируем это на примере, рассматривая ещё один C файл в дополнение к тому, что был приведён выше.

Linker2.png

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

Также компоновщик может заполнить все пустые места как показано здесь (на системах UNIX процесс компоновки обычно вызывается командой ld).

Linker3.png

Так же, как и для объектных файлов, мы можем использовать nm для исследования конечного исполняемого файла.

Сегменты и секции

В формате ELF есть понятие сегмента и секции.

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

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

Elf link vs exec view.jpg

Сегмент — группа секций. Сегмент описывается заголовком (program header). В одном сегменте 0 или более секций. Сегмент также содержит информацию, необходимую для исполнения файла (runtime). Куда необходимо загрузить данные в виртуальном адресном пространстве процесса, какие права установить на эти страницы памяти (чтение, запись, исполнение). Мы это более детально будем изучать применительно к архитектуре x86 на последующих занятиях.

Секция кода или секция текста содержит исполняемые инструкции для процессора. Как правило, доступна только для чтения.

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

.rodata

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

Название BSS объясняется историческими причинами. Изначально это сокращение от названия операции «Block Started by Symbol» на древней ЭВМ IBM 704 (1957 г.). Некоторые расшифровывают эту аббревиатуру как «Better Save Space».

Представление программы в памяти

Сегменты из исполняемого файла программы в современных ОС (как UNIX-подобных, так и Windows) отображаются в виртуальное адресное пространство процесса с помощью механизма memory mapping (проектирование файла в память).

Приведём иллюстрацию типичного расположения данных в памяти процесса на 32-битном Linux на архитектуре x86.

LinuxFlexibleAddressSpaceLayout.png

Можно заметить, что стек растёт вниз (чем глубже по стеку, тем меньше адрес), а куча растёт вверх.

Утилита pmap в UNIX-системах позволяет получить карту адресного пространства процесса по его PID (process id).

В реальной системе картина может сильно отличаться из-за применения технологии ASLR.

ASLR (англ. address space layout randomization — «рандомизация размещения адресного пространства») — технология, применяемая в операционных системах, при использовании которой случайным образом изменяется расположение в адресном пространстве процесса важных структур данных, а именно образов исполняемого файла, подгружаемых библиотек, кучи и стека.

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

Многопоточная программа

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

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

Проблема повторяющихся символов

В предыдущей главе было упомянуто, что компоновщик выдаёт сообщение об ошибке, если не может найти определение для символа, на который найдена ссылка. А что случится, если найдено два определения для символа во время компоновки?

В C++ решение прямолинейное. Язык имеет ограничение, известное как правило одного определения (ODR — one definition rule), которое гласит, что должно быть только одно определение для каждого символа, встречающегося во время компоновки, ни больше, ни меньше. (Соответствующая глава стандарта C++ также упоминает некоторые исключения, которые мы рассмотрим несколько позже.)

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

Однако, компоновщики должны уметь обходится также и с другими языками кроме C и C++, для которых правило одного определения не обязательно соблюдается.

Ключевое слово static

inline-функции

Язык C++

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

Перегрузка функций и декорирование имён

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

Такое положение вещей определённо затрудняет работу компоновщика: если какой-нибудь код обращается к функции max, какая именно имелась в виду?

Решение к этой проблеме названо декорированием имён (name mangling), потому что вся информация о сигнатуре функции переводится (to mangle — искажать, деформировать) в текстовую форму, которая становится собственно именем символа с точки зрения компоновщика. Различные сигнатуры переводятся в различные имена. Таким образом проблема уникальности имён решена.

Схемы отличаются от платформы к платформе.

Здесь мы видим три функции max, каждая из которых получила отличное имя в объектном файле, и мы можем проявить смекалку и предположить, что две следующие буквы после «max» обозначают типы входящих параметров — «i» как int, «f» как float и «d» как double (однако всё значительно усложняется, если классы, пространства имён, шаблоны и перегруженные операторы вступают в игру).

Также стоит отметить, что обычно есть способ конвертирования между именами, видимых программисту, и именами, видимых компоновщику. Это может быть и отдельная программа (например, c++filt) или опция в командной строке (например --demangle для GNU nm), которая выдаёт что-то похожее на это:

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

вокруг объявления и определения функций. По сути этим мы сообщаем C++-компилятору, что определённое имя не должно быть декорировано — либо потому что это определение C++-функции, которая будет вызываться кодом C, либо потом что это определение C-функции, которая будет вызываться кодом C++.

Инициализация статических объектов

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

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

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

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

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

Шаблоны

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

C++ вводит понятия шаблона (templates), который позволяет использовать код, приведённый ниже, сразу для всех случаев.

Этот написанный на C++ код использует max<int>(int,int) и max<double>(double,double). Однако, какой-нибудь другой код мог бы использовать и другие инстанции этого шаблона.

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

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *