Наши партнеры

UnixForum






Книги по Linux (с отзывами читателей)

Библиотека сайта rus-linux.net

На главную -> MyLDP -> Тематический каталог -> Программирование и алгоритмические языки в Linux

Компоновщики и загрузчики. Часть 2

Оригинал: Linkers and Loaders
Автор: Sandeep Grover
Дата: 26 ноября 2002
Перевод: Александр Тарасов aka oioki
Дата перевода: 4 декабря 2008

Это продолжение. Первую часть статьи можно прочесть здесь.

Связывание со статическими библиотеками

Статическая библиотека - это набор нескольких объектных файлов одного и того же типа. Библиотеки хранятся на диске в виде архива. Помимо самих объектных файлов этот архив содержит индексную информацию, по которой впоследствии будет легче найти определенные символы. Каждый ELF-архив начинается с магической последовательности из 8 символов: !<arch>\n, здесь \n - символ новой строки.

Статические библиотеки можно передавать компоновщику в виде аргументов командной строки, при этом из библиотек будут взяты лишь требуемые объектные модули, а лишние проигнорированы. В UNIX-системах библиотека libc.a содержит все стандартные функции языка C, включая printf и fopen, используемые большинством программ.

gcc foo.o bar.o /usr/lib/libc.a /usr/lib/libm.a

Библиотека libm.a - это стандартная математическая библиотека UNIX-систем. Она содержит объектные модули для вычисления квадратного корня, тригонометрических функций и т.д.

При использовании статических библиотек, в процессе разрешения символа компоновщик просматривает перемещаемые объектные файлы и архивы слева направо, как указано в командной строке. При этом компоновщик модифицирует три множества: множество O, перемещаемые объектные файлы, которые попадут в исполняемый файл; множество U, неразрешенные символы; и множество D, состоящее из символов, определенных в одном из обработанных модулей. В самом начале все три множества пусты.

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

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

Релокация

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

  • Релокация секций и определений символов. Компоновщик соединяет все секции одного типа в одну новую секцию. К примеру, компоновщик соединяет все секции .data всех входных перемещаемых объектных файлов в одну секцию .data итогового исполняемого файла. То же самое проделывается для секции .code. Далее компоновщик назначает адреса памяти для новых объединенных секций, для каждой секции, указанной во входном модуле и для каждого отдельного символа. После этого каждая инструкция и глобальная переменная в программе имеет свой уникальный адрес.
  • Релокация ссылок на символы внутри секций. На этом этапе компоновщик модифицирует каждую ссылку на символ, содержащуюся в секциях кода и данных так, чтобы они указывали на верные адреса.

Когда ассемблер встречает неразрешенный символ, он создает для него запись релокации и помещает ее в секции .relo.text/.relo.data. Запись релокации содержит информацию о том, как разрешить ссылку. Типичная запись релокации ELF содержит следующие элементы:

  • Смещение (Offset) - смещение релоцируемой ссылки внутри секции. Для перемещаемого объектного файла это означает байтовое смещение от начала секции до единицы хранения, которая подвергается релокации.
  • Символ - символ, на который должна будет указывать изменяемая ссылка. Это индекс производимой релокации в таблице символов.
  • Тип - тип релокации, обычно это R_386_PC32, что означает относительную PC-адресацию. Значение R_386_32 означает абсолютную адресацию.

Компоновщик проходит по всем записям релокации, присутствующим в модулях и переразмещает неразрешенные символы, в зависимости от их типа. К примеру, для типа R_386_PC32 адрес релокации вычисляется как S+A-P; для типа R_386_32 адрес вычисляется как S+A. Здесь S означает значение символа из записи релокации, P - смещение секции либо адрес единицы хранения, которую переразмещаем (он вычисляется по значению смещения из записи релокации), а A - это адрес, который требуется для вычисления значения переразмещаемого поля.

Динамическое связывание: разделяемые библиотеки

У статических библиотек есть один существенный недостаток. Рассмотрим, к примеру, стандартные функции, такие как printf и scanf. Они используются во многих программах. Теперь представьте, что у вас работает 50-100 процессов, и каждый процесс хранит в памяти свою копию исполняемого кода для printf и scanf. Таким образом оперативная память расходуется по сути впустую. Здесь нам и пригодятся разделяемые библиотеки, устраняющие этот недостаток статических библиотек. Разделяемая библиотека - это объектный модуль, загружаемый по произвольному адресу памяти при запуске программы. Разделяемые библиотеки иногда называют разделяемыми объектами. В большинстве UNIX-систем они имеют расширение .so; в системе HP-UX используется расширение .sl, а в ОС от фирмы Microsoft они называются DLL (dynamic link libraries - библиотеки динамического связывания).

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

gcc -shared -fPIC -o libfoo.so a.o b.o

Эта команда указывает компилятору, что нужно создать разделяемую библиотеку, libfoo.so, состоящую из объектных модулей a.o и b.o. Ключ -fPIC указывает компилятору на то, что нужно создать код, независимый от адреса (position independent code, PIC).

Теперь представьте, что объектный модуль с функцией main называется bar.o, и он зависит от объектных файлов a.o и b.o. В этом случае компоновщик нужно вызывать с помощью команды:

gcc bar.o ./libfoo.so

Будет создан исполняемый файл a.out, причем таким образом, что связывание с библиотекой libfoo.so будет происходить при запуске программы. Иными словами, a.out не будет содержать в себе кода объектных модулей a.o и b.o, в отличие от предыдущего случая, когда мы компилировали их в статическую библиотеку, а не в разделяемую. Исполняемый файл содержит информацию о релокации и таблицу символов, что позволяет программе разрешить ссылки на код и данные библиотеки libfoo.so во время ее запуска. Поэтому a.out можно назвать частично исполняемым файлом, ведь у него есть зависимость от libfoo.so. Также в файле хранится секция .interp, содержащая имя динамического компоновщика, который в Linux-системах сам по себе является разделяемым объектом (ld-linux.so). При загрузке программы в память загрузчик передает управление динамическому компоновщику. Динамический компоновщик содержит стартовый код, отображающий разделяемые библиотеки на адресное пространство программ. Далее выполняются следующие шаги:

  • переразмещает текст и данные библиотеки libfoo.so в сегмент памяти;
  • переразмещает все ссылки в a.out на символы, определенные в libfoo.so.

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

Загрузка разделяемых библиотек из приложений

Разделяемые библиотеки могут быть загружены из приложения не сразу, а в процессе ее выполнения. Приложение посылает запрос динамическому компоновщику на загрузку и связывание разделяемой библиотеки. При этом приложение даже может не содержать в себе вкомпилированных ссылок на эти разделяемые библиотеки. В Linux, Solaris и в других системах есть ряд функций, позволяющих таким образом загружать разделяемые объекты. В Linux существуют системные вызовы dlopen, dlsym и dlclose, позволяющие соответственно загрузить разделяемый объект, получить указатель на какой-либо содержащийся в нем символ, закрыть объект. В системах Windows есть функции LoadLibrary и GetProcAddress - это соответствующие замены вызовам dlopen и dlsym.

Программы для работы с объектными файлами

Ниже приведен список программ Linux, с помощью которых можно исследовать объектные/исполняемые файлы.

  • ar: создает статические библиотеки.
  • objdump: самый важный инструмент; предназначен для вывода всей инфорамции об объектном бинарном файле.
  • strings: выводит все печатаемые строки, хранящиеся в бинарном файле.
  • nm: выводит список символов, определенных в таблице символов объекта.
  • ldd: выводит список разделяемых библиотек, от которых зависит объект.
  • strip: удаляет таблицу символов.

Что еще можно почитать