Руководство новичка по эксплуатации компоновщика. Функции компоновщика и загрузчика Функции компоновщика

[из методы]

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

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

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

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

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

Компоновщик (также реда́ктор свя́зей, линкер - от англ. link editor, linker) - программа, которая производит компоновку - принимает на вход один или несколько объектных модулей и собирает по ним исполнимый модуль.

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

Определённые или экспортируемые имена - функции и переменные, определённые в данном модуле и предоставляемые для использования другим модулям

Неопределённые или импортируемые имена - функции и переменные, на которые ссылается модуль, но не определяет их внутри себя

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

#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++.



разбиение программы на модули c++ (7)

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

#include using namespace std ; #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 < class paramType > 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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Ах, но вы можете иметь NonDefinedFunction (int) в другом компиляционном блоке.

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

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

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

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

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

David Drysdale, Beginner"s guide to linkers

(http://www.lurklurk.org/linkers/linkers.html).

Цель данной статьи - помочь C и C++ программистам понять сущность того, чем занимается компоновщик. За последние несколько лет я объяснил это большому количеству коллег и наконец решил, что настало время перенести этот материал на бумагу, чтоб он стал более доступным (и чтоб мне не пришлось объяснять его снова). [Обновление в марте 2009: добавлена дополнительная информация об особенностях компоновки в Windows, а также более подробно расписано правило одного определения (one-definition rule).

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

g++ -o test1 test1a.o test1b.o

test1a.o(.text+0x18): In function `main":

: undefined reference to `findmax(int, int)"

collect2: ld returned 1 exit status

Если Ваша реакция - "наверняка забыл extern «C»", то Вы скорее всего знаете всё, что приведено в этой статье.

  • Определения: что находится в C файле?
  • Что делает C компилятор
  • Что делает компоновщик: часть 1
  • Что делает операционная система
  • Что делает компоновщик: часть 2
  • C++ для дополнения картины
  • Динамически загружаемые библиотеки
  • Дополнительно

Определения: что находится в C файле?

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

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

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

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

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

Для переменных существует определения двух видов:

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

При этом под термином «доступны» следует понимать «можно обратиться по имени, ассоциированным с переменной в момент определения».

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

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

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

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

И наконец, мы можем сохранять информацию в памяти, которая динамически выделена по средством malloc или new . В данном случае нет возможности обратится к выделенной памяти по имени, поэтому необходимо использовать указатели - именованные переменные, содержащие адрес неименованной области памяти. Эта область памяти может быть также освобождена с помощью free или delete . В этом случае мы имеем дело с «динамическим размещением».

Подытожим:

Глобальные

Локальные

Динамические

Неинициа-

Неинициа-

Объяв-ление

int fn(int x);

extern int x;

extern int x;

Опреде-ление

int fn(int x)

{ ... }

int x = 1;

(область действия

Файл)

int x;

(область действия - файл)

int x = 1;

(область действия - функция)

int x;

(область действия - функция)

int* p = malloc(sizeof(int));

Вероятно более лёгкий путь усвоить - это просто посмотреть на пример программы.

/* Определение неинициализированной глобальной переменной */

int x_global_uninit;

/* Определение инициализированной глобальной переменной */

int x_global_init = 1;

/* Определение неинициализированной глобальной переменной, к которой

static int y_global_uninit;

/* Определение инициализированной глобальной переменной, к которой

* можно обратиться по имени только в пределах этого C файла */

static int y_global_init = 2;

/* Объявление глобальной переменной, которая определена где-нибудь

* в другом месте программы */

extern int z_global;

/* Объявлени функции, которая определена где-нибудь другом месте

* программы (Вы можете добавить впереди "extern", однако это

* необязательно) */

int fn_a(int x, int y);

/* Определение функции. Однако будучи помеченной как static, её можно

* вызвать по имени только в пределах этого C файла. */

static int fn_b(int x)

Return x+1;

/* Определение функции. */

/* Параметр функции считается локальной переменной. */

int fn_c(int x_local)

/* Определение неинициализированной локальной переменной */

Int y_local_uninit;

/* Определение инициализированной локальной переменной */

Int y_local_init = 3;

/* Код, который обращается к локальным и глобальным переменным,

* а также функциям по имени */

X_global_uninit = fn_a(x_local, x_global_init);

Y_local_uninit = fn_a(x_local, y_local_init);

Y_local_uninit += fn_b(z_global);

Return (x_global_uninit + y_local_uninit);

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

Работа компилятора C заключается в конвертировании текста, (обычно) понятному человеку, в нечто, что понимает компьютер. На выходе компилятор выдаёт объектный файл. На платформах UNIX эти файлы имеют обычно суффикс.o; в Windows - суффикс.obj. Содержание объектного файла - в сущности две вещи:

код, соответствующий определению функции в C файле

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

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

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

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

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

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

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

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

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

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

Symbols from c_parts.o:

Name Value Class Type Size Line Section

fn_a | | U | NOTYPE| | |*UND*

z_global | | U | NOTYPE| | |*UND*

fn_b |00000000| t | FUNC|00000009| |.text

x_global_init |00000000| D | OBJECT|00000004| |.data

y_global_uninit |00000000| b | OBJECT|00000004| |.bss

x_global_uninit |00000004| C | OBJECT|00000004| |*COM*

y_global_init |00000004| d | OBJECT|00000004| |.data

fn_c |00000009| T | FUNC|00000055| |.text

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

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

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