В линкере нет никакой магии
(В оригинале - The Linker Is not a Magical Program)
Удручающе часто (в последний раз – буквально перед написанием этой статьи) программисты представляют процесс преобразования исходного кода в исполняемый файл как-то вот так:
- Написать (или исправить) исходный код
- Скомпилировать исходный код в объектные файлы
- Происходит какое-то чудо
- Появляется исполняемый файл
На шаге 3, конечно же, происходит линковка. Что же меня в этом так раздражает? Я работал в техподдержке десятилетиями, и постоянно получаю вопросы вроде:
- Линкер сообщает что переменная определена более одного раза
- Линкер сообщает что переменная является неразрешимым символом
- Почему исполняемый файл такой большой.
И все это в перемешку со словами «кажется», «каким-то образом» и с ощущением непередаваемой сложности в голосе. Словно процесс линковки – это что-то столь магическое, что понять его могут только маги и чародеи. Процесс компиляции не вызывает столько вопросов – похоже, что программисты более осведомлены о том, как работают компиляторы, или по крайней мере знают, для чего они нужны.
Линкер – на удивление простая и незатейливая программа. Все, что она делает – это соединяет вместе секции кода и данных из объектных файлов, связывает символьные ссылки с их определениями, выбрасывает неиспользуемые символы из библиотеки и записывает исполняемый файл. И все. Никакой магии. Самая большая и сложная часть – это декодирование и генерирование ужасно переусложненных форматов файлов, что, однако, не меняет самой сущности линкера.
Пусть, например, линкер сообщает, что переменная определена более одного раза. Многие языки имеют как декларации, так и определения. Декларации обычно помещаются в заголовочные файлы:
extern int iii;
Таким образом создаются внешние ссылки на символ iii. Определение же создает само хранилище для этого символа, появляясь обычно непосредственно в коде, и выглядит примерно вот так:
int iii = 3;
И таких определений может быть только одно. А что, если определение появится в более чем одном файле?
// File a.c
int iii = 3;
// File b.c
double iii(int x) { return 3.7; }
Линкер сообщит о том, что символ iii определен более одного раза.
Если же iii встречается только в виде деклараций, то линкер сообщит о том, что iii является неразрешенной ссылкой.
Чтобы определить, почему размер исполняемого файла такой, каким получился, нужно взглянуть на файл .map, обычно генерируемый линкером. Этот файл – ничего более, чем список всех символов в исполняемом файле вместе с их адресами в памяти. Это покажет вам, какие модули были добавлены, и размеры каждого модуля. Теперь вы знаете, что «раздуло» ваш исполняемый файл. Часто вы не будете представлять, какая часть и где использует тот или иной «непонятный» модуль. Чтобы выяснить это, просто удалите подозрительный модуль из библиотеки и еще раз запустите сборку. Неразрешенные символы покажут, кто ссылается на этот модуль.
И хотя не всегда сообщения линкера столь просты и очевидны, в них нет ничего магического. Механизм всего этого очень простой, вам всего лишь нужно уточнить детали в каждом конкретном случае.
Автор оригинала - Walter Bright