В чем состоит назначение компоновщика. Компоновщик

Я хочу точно понять, на какую часть компилятора программы просматривается и на что ссылается компоновщик. Поэтому я написал следующий код:

#include using namespace std; #include void FunctionTemplate (paramType val) { i = val } }; void Test::DefinedCorrectFunction(int val) { i = val; } void Test::DefinedIncorrectFunction(int val) { i = val } void main() { Test testObject(1); //testObject.NonDefinedFunction(2); //testObject.FunctionTemplate(2); }

У меня есть три функции:

  • DefinedCorrectFunction - это нормальная функция, объявленная и определенная правильно.
  • DefinedIncorrectFunction - эта функция объявлена ​​правильно, но реализация неверна (отсутствует;)
  • NonDefinedFunction - только объявление. Нет определения.
  • FunctionTemplate - шаблон функции.

    Теперь, если я скомпилирую этот код, я получаю ошибку компилятора для отсутствующего ";" в DefinedIncorrectFunction.
    Предположим, я исправить это, а затем прокомментировать testObject.NonDefinedFunction(2). Теперь я получаю ошибку компоновщика. Теперь закомментируйте testObject.FunctionTemplate(2). Теперь я получаю ошибку компилятора для отсутствующих ";".

Для шаблонов функций я понимаю, что они не тронуты компилятором, если они не вызываются в коде. Итак, недостающие ";" не жалуется компилятором, пока я не вызвал testObject.FunctionTemplate(2).

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

Итак, я не совсем понимаю, что именно делает компилятор и что делает компоновщик. Мое понимание компонентов компоновщика ссылок со своими вызовами. Так что, когда NonDefinedFunction называется, он ищет скомпилированную реализацию NonDefinedFunction и жалуется. Но компилятор не заботился о реализации NonDefinedFunction, но это делалось для DefinedIncorrectFunction.

Я бы очень признателен, если кто-нибудь сможет объяснить это или дать некоторую ссылку.

8 ответов

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

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

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

Компилятор компилирует код и заполняет таблицу символов каждым символом, с которым он сталкивается. Символы относятся к переменным и функциям. Ответ на Этот вопрос объясняет таблицу символов.

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

Точка примечания

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

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

Итак, в вашем конкретном случае -

  • DefinedIncorrectFunction() - компилятор получает определение функции и начинает компилировать его для создания объектного кода и вставки соответствующей ссылки в таблицу символов. Ошибка компиляции из-за ошибки синтаксиса, поэтому компилятор прерывается с ошибкой.
  • NonDefinedFunction() - компилятор получает декларацию, но не имеет определения, поэтому добавляет запись в таблицу символов и помещает компоновщик для добавления соответствующих значений (поскольку компоновщик обрабатывает кучу объектных файлов, возможно, это определение присутствует в некоторых другой файл объекта). В вашем случае вы не указываете какой-либо другой файл, поэтому компоновщик прерывается с ошибкой undefined reference to NonDefinedFunction , потому что он не может найти ссылку на соответствующую запись в таблице символов.

Чтобы понять это, еще раз скажем, что ваш код структурирован следующим образом

#include #include class Test { private: int i; public: Test(int val) {i=val ;} void DefinedCorrectFunction(int val); void DefinedIncorrectFunction(int val); void NonDefinedFunction(int val); template void FunctionTemplate (paramType val) { i = val; } };

Файл try.cpp

#include "try.h" void Test::DefinedCorrectFunction(int val) { i = val; } void Test::DefinedIncorrectFunction(int val) { i = val; } int main() { Test testObject(1); testObject.NonDefinedFunction(2); //testObject.FunctionTemplate(2); return 0; }

Давайте сначала скопируем и собираем код, но не свяжем его

$g++ -c try.cpp -o try.o $

Этот шаг протекает без каких-либо проблем. Таким образом, у вас есть объектный код в try.o. Попробуйте и соедините его.

$g++ try.o try.o: In function `main": try.cpp:(.text+0x52): undefined reference to `Test::NonDefinedFunction(int)" collect2: ld returned 1 exit status

Вы забыли определить Test:: NonDefinedFunction. Пусть определите его в отдельном файле.

Файл-try1.cpp

#include "try.h" void Test::NonDefinedFunction(int val) { i = val; }

Скомпилируем его в объектный код

$ g++ -c try1.cpp -o try1.o $

Снова это успешно. Попробуем связать только этот файл

$ g++ try1.o /usr/lib/gcc/x86_64-redhat-linux/4.4.5/../../../../lib64/crt1.o: In function `_start": (.text+0x20): undefined reference to `main" collect2: ld returned 1 exit status

Нет основной так выигранной; t link!!

Теперь у вас есть два отдельных объектных кода, в которых есть все необходимые компоненты. Просто передайте ОБОИХ из них в компоновщик, и пусть это сделает остальные

$ g++ try.o try1.o $

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

Скажите, что вы хотите съесть какой-то суп, поэтому отправляйтесь в ресторан.

Вы ищете меню для супа. Если вы не найдете его в меню, вы покидаете ресторан. (вроде компилятора, жалующегося на то, что он не смог найти функцию). Если вы его найдете, что вы делаете?

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

Я считаю, что это ваш вопрос:

Где я запутался, когда компилятор жаловался на DefinedIncorrectFunction. Он не искал реализацию NonDefinedFunction, но прошел через DefinedIncorrectFunction.

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

Компилятор проверяет, соответствует ли исходный код языку и соответствует семантике языка. Вывод компилятора - это объектный код.

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

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

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

В случае DefinedIncorrectFunction вы предоставляете исходный текст который требуется для анализа. Этот текст содержит ошибка, для которой требуется диагностика. В случае NonDefinedFunction: если функция используется, отказ предоставить определение (или предоставление более одного определения) в полном программа является нарушением одного правила определения, которое undefined поведение. Диагностика не требуется (но я не могу представить которые не предусматривали какого-либо недостающего определения функция, которая была использована).

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

Это несколько изменено в таких случаях, как встроенные функции, где вы разрешено повторять определение в каждой единицы перевода и измененный шаблонами, поскольку многие ошибки не могут быть обнаружены до тех пор, пока конкретизации. В случае шаблонов стандартный лист реализаций большая свобода: по крайней мере, компилятор должен проанализируйте шаблон достаточно, чтобы определить, где заканчивается шаблон. добавленные стандартные вещи, такие как typename , тем не менее, позволяют значительно больше синтаксический анализ перед созданием. Однако в зависимых контекстах некоторые ошибки не могут быть обнаружены до создания экземпляра, что может место во время компиляции или время ссылки; ранние реализации предпочтительная компоновка времени ссылки; время компиляции сегодня, и используется VС++ и g++.

UTM-метки - набор данных, добавляемых к URL с целью получения дополнительной информации в рамках оценки продуктивности маркетинговых кампаний. UTM-tags были разработаны компанией Urchin Software, поглощенной Google. Пять предлагаемых этими тегами параметров позволяют оценить, насколько успешно то или иное объявление. Данные, получаемые в результате таких GET-запросов, обрабатываются в различных сервисах аналитики, среди которых самые востребованные - Google Analytics и Яндекс.Метрика.

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

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

  • источник перехода (Google, e-mail и т.д.);
  • тип трафика (например, PPC или КМС);
  • наименование кампании, обеспечившей переход;
  • ключ;
  • дополнительные сведения для различия объявлений.

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

  • utm_source;
  • utm_medium;
  • utm_campaign;
  • utm_term;

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

Теперь, собственно, образец:

domen.com/?utm_source =google&utm_medium =cpc&utm_campaign =my_sale

Значение устанавливается так, чтобы маркетолог при анализе с помощью систем аналитики имел возможность оперативно оценивать источники переходов (например, utm_ source =adwords или utm_ source = vk) , и типы трафика (utm-medium =cpc или utm-medium =ppc) .

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

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

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

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

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

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

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

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

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

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

Однако загрузчик не всегда является составной частью системы программирования, поскольку выполняемые им функции очень зависят от архитектуры целевой вычислительной системы, в которой выполняется результирующая программа, созданная системой программирования. На первых этапах развития ОС загрузчики существовали в виде отдельных модулей, которые выполняли трансляцию адресов и готовили программу к выполнению – создавали так называемый “образ задачи”. Такая схема была характерна для многих ОС (например, для ОСРВ на ЭВМ типа СМ-1, ОС RSX/11 или RAFOS на ЭВМ типа СМ-4 и т. п.). Образ задачи можно было сохранить на внешнем носителе или же создавать его вновь всякий раз при подготовке программы к выполнению.

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

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

Статьи к прочтению:

Как перейти на Miui 9 Stable Global с китайской прошивки? Разблокировка загрузчика

В типичной системе одновременно выполняется множество программ. Работа каждой программы зависит от множества функций, некоторые из которых входят в состав "стандартной" Си-библиотеки (например, printf() , malloc() , write() и т. д.).

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

Статическая компоновка Термин "статически скомпонованный" (statically linked) означает, что программа и некоторая библиотека были объединены с помощью компоновщика (linker) во время процесса компоновки. Таким образом, связь между программой и библиотекой является фиксированной и устанавливается во время процесса компоновки, т. е. до того, как программа будет работать. Кроме всего прочего, это также означает, можно изменить данную связь иначе, как посредством перекомпоновки программы с новой версией библиотеки.

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

Статически скомпонованные программы компонуются с архивами объектов (библиотеками ), которые обычно имеют расширение a. Примером такого набора объектов является стандартная Си-библиотека libc.a.

Динамическая компоновка Термин "динамически скомпонованный" (dynamically linked) означает, что программа и некоторая библиотека не были объединены с помощью компоновщика во время процесса компоновки. Вместо этого, компоновщик помещает информацию в исполняемый файл, который, в свою очередь, сообщает загрузчику, в каком разделяемом объектном модуле расположен код и какой динамический компоновщик (runtime linker) должен использоваться для поиска и компоновки ссылок. Это означает, что связь между программой и разделяемым объектом устанавливается во время выполнения программы, а именно, в самом начале выполнения производится поиск и компоновка необходимых разделяемых объектов.

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

Динамически скомпонованные программы компонуются с разделяемым объектами с расширением so. Примером такого объекта является разделяемая стандартная Си-библиотека libc.so.

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

Добавление кода в процессе работы программы При таком подходе вызываемые из программы функции будут определены только на этапе исполнения. Это предоставляет дополнительные возможности.

Рассмотрим пример работы драйвера диска. Драйвер запускается, тестирует оборудование и обнаруживает жёсткий диск. Затем драйвер динамически загружает модуль io-blk, предназначенный для обработки дисковых блоков, т.к. было обнаружено блок-ориентированное устройство. После того как драйвер получает доступ к диску на блочном уровне, он обнаруживает на диске два раздела: раздел DOS и раздел QNX4. Чтобы не увеличивать размер драйвера жёсткого диска, в него вообще не включаются драйверы файловых систем. Во время работы системы драйвер может обнаружить эти два раздела (DOS и QNX4) и только после этого загрузить соответствующие модули файловых систем fs-dos.so и fs-qnx4.so.

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

Как используются разделяемые объекты Для того чтобы понять, как программа использует разделяемые объекты, необходимо сначала рассмотреть формат исполняемого модуля, а затем последовательность тех стадий, через которые программа проходит при запуске. Формат ELF В ОС QNX Neutrino используется так называемый двоичный формат исполняемых и компонуемых модулей (Executable and Linkable Format, ELF), который в настоящее время принят в системах SVR4 Unix. Формат ELF не только упрощает создание разделяемых библиотек, но также расширяет возможности динамической загрузки модулей во время работы программы.

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

В процессе компоновки программа или библиотека строится посредством слияния секций, имеющих одинаковые атрибуты, и преобразования их в сегменты. Как правило, все секции, содержащие данные, которые предназначены для исполнения или "только для чтения", компонуются в один сегмент text , а данные и BSS компонуются в сегмент data . Эти сегменты называются загрузочными сегментами (load segments) , потому что они должны быть загружены в память при создании процесса. Другие секции, как, например, информация об идентификаторах и отладочная информация, объединяются в т. н. незагружаемые сегменты (nonload segments) .



 

Возможно, будет полезно почитать: